From 139d83b33f26c1508ca37d4e5673a52f543e59fd Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Tue, 20 Oct 2026 16:23:30 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20management=20dimensions=20=E2=80=94=20g?= =?UTF-8?q?et=5Fdimensions,=20create=5Fdimension,=20update=5Fdimension,=20?= =?UTF-8?q?sliced=20trial=20balance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_013UXnatpnbiixt1ZVHfdZAP --- .../live_financial_statement.py | 24 +- .../create_dimension.py | 270 ++++++++++++++++++ .../update_dimension.py | 270 ++++++++++++++++++ .../update_journal_entry.py | 80 +++--- robosystems_client/clients/ledger_client.py | 84 +++++- .../graphql/generated/__init__.py | 14 + .../graphql/generated/client.py | 25 ++ .../generated/get_ledger_transaction.py | 9 + .../generated/get_ledger_trial_balance.py | 1 + .../generated/list_ledger_dimensions.py | 29 ++ .../generated/list_ledger_journal_entries.py | 8 + .../graphql/generated/operations.py | 41 ++- .../ledger/GetLedgerTransaction.graphql | 3 +- .../ledger/GetLedgerTrialBalance.graphql | 17 +- .../ledger/ListLedgerDimensions.graphql | 15 + .../ledger/ListLedgerJournalEntries.graphql | 1 + robosystems_client/graphql/schema.graphql | 85 ++++++ robosystems_client/models/__init__.py | 20 ++ .../models/create_dimension_request.py | 94 ++++++ .../models/create_event_block_request.py | 4 +- .../models/dimension_response.py | 147 ++++++++++ .../models/entry_template_request.py | 15 +- .../models/journal_entry_line_dimensions.py | 71 +++++ .../models/journal_entry_line_item_input.py | 33 +++ .../journal_entry_line_item_response.py | 11 + .../live_financial_statement_request.py | 45 ++- .../live_financial_statement_response.py | 34 +++ .../operation_envelope_dimension_response.py | 158 ++++++++++ ...tion_envelope_dimension_response_status.py | 10 + .../models/statement_dimension.py | 86 ++++++ .../models/statement_dimension_column.py | 85 ++++++ .../models/statement_dimension_response.py | 145 ++++++++++ .../models/update_dimension_request.py | 106 +++++++ .../models/update_journal_entry_request.py | 54 +++- tests/test_ledger_client.py | 85 ++++++ 35 files changed, 2121 insertions(+), 58 deletions(-) create mode 100644 robosystems_client/api/robo_ledger_ledger_events/create_dimension.py create mode 100644 robosystems_client/api/robo_ledger_ledger_events/update_dimension.py create mode 100644 robosystems_client/graphql/generated/list_ledger_dimensions.py create mode 100644 robosystems_client/graphql/operations/ledger/ListLedgerDimensions.graphql create mode 100644 robosystems_client/models/create_dimension_request.py create mode 100644 robosystems_client/models/dimension_response.py create mode 100644 robosystems_client/models/journal_entry_line_dimensions.py create mode 100644 robosystems_client/models/operation_envelope_dimension_response.py create mode 100644 robosystems_client/models/operation_envelope_dimension_response_status.py create mode 100644 robosystems_client/models/statement_dimension.py create mode 100644 robosystems_client/models/statement_dimension_column.py create mode 100644 robosystems_client/models/statement_dimension_response.py create mode 100644 robosystems_client/models/update_dimension_request.py diff --git a/robosystems_client/api/robo_ledger_analytical_views/live_financial_statement.py b/robosystems_client/api/robo_ledger_analytical_views/live_financial_statement.py index d9da5088..5fa2b9dc 100644 --- a/robosystems_client/api/robo_ledger_analytical_views/live_financial_statement.py +++ b/robosystems_client/api/robo_ledger_analytical_views/live_financial_statement.py @@ -120,8 +120,10 @@ def sync_detailed( materialization required. One entity's books: `entity_id` names a subsidiary, omitted means the group parent. On the parent, `scope: combined` sums every member consolidated in full at the rs-gaap concepts for the months each was a member, nothing eliminated, and `scope: consolidated` adds the - consolidation entity's entries — the eliminations and noncontrolling interest. Rejected on shared- - repository graphs; those should use `financial-statement-analysis` instead. + consolidation entity's entries — the eliminations and noncontrolling interest. `dimension` slices an + income statement by department, class, location or project: one member's lines, or a column per + member with Unallocated and Total. Rejected on shared-repository graphs; those should use + `financial-statement-analysis` instead. **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. @@ -167,8 +169,10 @@ def sync( materialization required. One entity's books: `entity_id` names a subsidiary, omitted means the group parent. On the parent, `scope: combined` sums every member consolidated in full at the rs-gaap concepts for the months each was a member, nothing eliminated, and `scope: consolidated` adds the - consolidation entity's entries — the eliminations and noncontrolling interest. Rejected on shared- - repository graphs; those should use `financial-statement-analysis` instead. + consolidation entity's entries — the eliminations and noncontrolling interest. `dimension` slices an + income statement by department, class, location or project: one member's lines, or a column per + member with Unallocated and Total. Rejected on shared-repository graphs; those should use + `financial-statement-analysis` instead. **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. @@ -209,8 +213,10 @@ async def asyncio_detailed( materialization required. One entity's books: `entity_id` names a subsidiary, omitted means the group parent. On the parent, `scope: combined` sums every member consolidated in full at the rs-gaap concepts for the months each was a member, nothing eliminated, and `scope: consolidated` adds the - consolidation entity's entries — the eliminations and noncontrolling interest. Rejected on shared- - repository graphs; those should use `financial-statement-analysis` instead. + consolidation entity's entries — the eliminations and noncontrolling interest. `dimension` slices an + income statement by department, class, location or project: one member's lines, or a column per + member with Unallocated and Total. Rejected on shared-repository graphs; those should use + `financial-statement-analysis` instead. **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. @@ -254,8 +260,10 @@ async def asyncio( materialization required. One entity's books: `entity_id` names a subsidiary, omitted means the group parent. On the parent, `scope: combined` sums every member consolidated in full at the rs-gaap concepts for the months each was a member, nothing eliminated, and `scope: consolidated` adds the - consolidation entity's entries — the eliminations and noncontrolling interest. Rejected on shared- - repository graphs; those should use `financial-statement-analysis` instead. + consolidation entity's entries — the eliminations and noncontrolling interest. `dimension` slices an + income statement by department, class, location or project: one member's lines, or a column per + member with Unallocated and Total. Rejected on shared-repository graphs; those should use + `financial-statement-analysis` instead. **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. diff --git a/robosystems_client/api/robo_ledger_ledger_events/create_dimension.py b/robosystems_client/api/robo_ledger_ledger_events/create_dimension.py new file mode 100644 index 00000000..da90cba7 --- /dev/null +++ b/robosystems_client/api/robo_ledger_ledger_events/create_dimension.py @@ -0,0 +1,270 @@ +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.create_dimension_request import CreateDimensionRequest +from ...models.error_response import ErrorResponse +from ...models.operation_envelope_dimension_response import ( + OperationEnvelopeDimensionResponse, +) +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + graph_id: str, + *, + body: CreateDimensionRequest, + 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/create-dimension".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 | OperationEnvelopeDimensionResponse | None: + if response.status_code == 200: + response_200 = OperationEnvelopeDimensionResponse.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 | OperationEnvelopeDimensionResponse]: + 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: CreateDimensionRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeDimensionResponse]: + """Create Dimension + + Add a member to a management dimension: a department, class, location or project (or an axis of your + own) that ledger lines can carry, one member per axis per line. `value` is the member's stable key + within its axis, a slug of the name when omitted; a second member with the same key returns 409. Tag + lines with its id through `dimension_ids` on a journal line, and read the income statement by it + with `live-financial-statement`'s `dimension`. + + **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 (CreateDimensionRequest): Add a member to a management dimension. + + 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 | OperationEnvelopeDimensionResponse] + """ + + 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: CreateDimensionRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeDimensionResponse | None: + """Create Dimension + + Add a member to a management dimension: a department, class, location or project (or an axis of your + own) that ledger lines can carry, one member per axis per line. `value` is the member's stable key + within its axis, a slug of the name when omitted; a second member with the same key returns 409. Tag + lines with its id through `dimension_ids` on a journal line, and read the income statement by it + with `live-financial-statement`'s `dimension`. + + **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 (CreateDimensionRequest): Add a member to a management dimension. + + 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 | OperationEnvelopeDimensionResponse + """ + + 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: CreateDimensionRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeDimensionResponse]: + """Create Dimension + + Add a member to a management dimension: a department, class, location or project (or an axis of your + own) that ledger lines can carry, one member per axis per line. `value` is the member's stable key + within its axis, a slug of the name when omitted; a second member with the same key returns 409. Tag + lines with its id through `dimension_ids` on a journal line, and read the income statement by it + with `live-financial-statement`'s `dimension`. + + **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 (CreateDimensionRequest): Add a member to a management dimension. + + 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 | OperationEnvelopeDimensionResponse] + """ + + 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: CreateDimensionRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeDimensionResponse | None: + """Create Dimension + + Add a member to a management dimension: a department, class, location or project (or an axis of your + own) that ledger lines can carry, one member per axis per line. `value` is the member's stable key + within its axis, a slug of the name when omitted; a second member with the same key returns 409. Tag + lines with its id through `dimension_ids` on a journal line, and read the income statement by it + with `live-financial-statement`'s `dimension`. + + **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 (CreateDimensionRequest): Add a member to a management dimension. + + 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 | OperationEnvelopeDimensionResponse + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + client=client, + body=body, + idempotency_key=idempotency_key, + ) + ).parsed diff --git a/robosystems_client/api/robo_ledger_ledger_events/update_dimension.py b/robosystems_client/api/robo_ledger_ledger_events/update_dimension.py new file mode 100644 index 00000000..90214334 --- /dev/null +++ b/robosystems_client/api/robo_ledger_ledger_events/update_dimension.py @@ -0,0 +1,270 @@ +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_dimension_response import ( + OperationEnvelopeDimensionResponse, +) +from ...models.update_dimension_request import UpdateDimensionRequest +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + graph_id: str, + *, + body: UpdateDimensionRequest, + 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/update-dimension".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 | OperationEnvelopeDimensionResponse | None: + if response.status_code == 200: + response_200 = OperationEnvelopeDimensionResponse.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 | OperationEnvelopeDimensionResponse]: + 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: UpdateDimensionRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeDimensionResponse]: + """Update Dimension + + Rename a management dimension member, or retire it with `is_active=false`. Members are never + deleted: a retired member stays on the lines that carry it and goes on no new ones. A member synced + from QuickBooks keeps QuickBooks's name. + + **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 (UpdateDimensionRequest): Rename or retire a member. Members are never deleted: lines + reference + them. + + 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 | OperationEnvelopeDimensionResponse] + """ + + 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: UpdateDimensionRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeDimensionResponse | None: + """Update Dimension + + Rename a management dimension member, or retire it with `is_active=false`. Members are never + deleted: a retired member stays on the lines that carry it and goes on no new ones. A member synced + from QuickBooks keeps QuickBooks's name. + + **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 (UpdateDimensionRequest): Rename or retire a member. Members are never deleted: lines + reference + them. + + 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 | OperationEnvelopeDimensionResponse + """ + + 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: UpdateDimensionRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeDimensionResponse]: + """Update Dimension + + Rename a management dimension member, or retire it with `is_active=false`. Members are never + deleted: a retired member stays on the lines that carry it and goes on no new ones. A member synced + from QuickBooks keeps QuickBooks's name. + + **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 (UpdateDimensionRequest): Rename or retire a member. Members are never deleted: lines + reference + them. + + 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 | OperationEnvelopeDimensionResponse] + """ + + 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: UpdateDimensionRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeDimensionResponse | None: + """Update Dimension + + Rename a management dimension member, or retire it with `is_active=false`. Members are never + deleted: a retired member stays on the lines that carry it and goes on no new ones. A member synced + from QuickBooks keeps QuickBooks's name. + + **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 (UpdateDimensionRequest): Rename or retire a member. Members are never deleted: lines + reference + them. + + 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 | OperationEnvelopeDimensionResponse + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + client=client, + body=body, + idempotency_key=idempotency_key, + ) + ).parsed diff --git a/robosystems_client/api/robo_ledger_ledger_events/update_journal_entry.py b/robosystems_client/api/robo_ledger_ledger_events/update_journal_entry.py index dc343f42..95dae1c3 100644 --- a/robosystems_client/api/robo_ledger_ledger_events/update_journal_entry.py +++ b/robosystems_client/api/robo_ledger_ledger_events/update_journal_entry.py @@ -113,12 +113,14 @@ def sync_detailed( ) -> Response[ErrorResponse | OperationEnvelopeJournalEntryResponse]: """Update Journal Entry - Update a draft journal entry, or re-tag any entry's flows. If line_items is provided, existing line - items are replaced atomically, the new set must balance, and a line on a retired (`is_active=false`) - account is refused. `line_flows` sets the cash flow row of the named lines in place, on a posted - entry too, closed month included, since a flow moves no balance; the lines on the other side of the - entry that were in step with it follow. Posted entries accept nothing else and are corrected via - `create-event-block(event_type='journal_entry_reversed')`. + Update a draft journal entry, or re-tag any entry's flows or management dimensions. If line_items is + provided, existing line items are replaced atomically, the new set must balance, and a line on a + retired (`is_active=false`) account is refused. `line_flows` sets the cash flow row of the named + lines in place, on a posted entry too, closed month included, since a flow moves no balance; the + lines on the other side of the entry that were in step with it follow. `line_dimensions` sets the + department, class, location or project members the named lines carry, in place, on a posted entry + and a closed month too, since a dimension moves no balance either. Posted entries accept nothing + else and are corrected via `create-event-block(event_type='journal_entry_reversed')`. **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. @@ -129,9 +131,9 @@ def sync_detailed( body (UpdateJournalEntryRequest): Update a draft journal entry, or re-tag any entry's flows. - Posted entries accept only `line_flows`: a flow moves no balance, so it - changes in place, closed month or not. Anything else on a posted entry is - a reversal and a corrected entry. + Posted entries accept only `line_flows` and `line_dimensions`: neither + moves a balance, so each changes in place, closed month or not. Anything + else on a posted entry is a reversal and a corrected entry. Omitted fields are left unchanged. If `line_items` is provided, the existing line items are replaced entirely (atomic delete + insert) @@ -168,12 +170,14 @@ def sync( ) -> ErrorResponse | OperationEnvelopeJournalEntryResponse | None: """Update Journal Entry - Update a draft journal entry, or re-tag any entry's flows. If line_items is provided, existing line - items are replaced atomically, the new set must balance, and a line on a retired (`is_active=false`) - account is refused. `line_flows` sets the cash flow row of the named lines in place, on a posted - entry too, closed month included, since a flow moves no balance; the lines on the other side of the - entry that were in step with it follow. Posted entries accept nothing else and are corrected via - `create-event-block(event_type='journal_entry_reversed')`. + Update a draft journal entry, or re-tag any entry's flows or management dimensions. If line_items is + provided, existing line items are replaced atomically, the new set must balance, and a line on a + retired (`is_active=false`) account is refused. `line_flows` sets the cash flow row of the named + lines in place, on a posted entry too, closed month included, since a flow moves no balance; the + lines on the other side of the entry that were in step with it follow. `line_dimensions` sets the + department, class, location or project members the named lines carry, in place, on a posted entry + and a closed month too, since a dimension moves no balance either. Posted entries accept nothing + else and are corrected via `create-event-block(event_type='journal_entry_reversed')`. **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. @@ -184,9 +188,9 @@ def sync( body (UpdateJournalEntryRequest): Update a draft journal entry, or re-tag any entry's flows. - Posted entries accept only `line_flows`: a flow moves no balance, so it - changes in place, closed month or not. Anything else on a posted entry is - a reversal and a corrected entry. + Posted entries accept only `line_flows` and `line_dimensions`: neither + moves a balance, so each changes in place, closed month or not. Anything + else on a posted entry is a reversal and a corrected entry. Omitted fields are left unchanged. If `line_items` is provided, the existing line items are replaced entirely (atomic delete + insert) @@ -218,12 +222,14 @@ async def asyncio_detailed( ) -> Response[ErrorResponse | OperationEnvelopeJournalEntryResponse]: """Update Journal Entry - Update a draft journal entry, or re-tag any entry's flows. If line_items is provided, existing line - items are replaced atomically, the new set must balance, and a line on a retired (`is_active=false`) - account is refused. `line_flows` sets the cash flow row of the named lines in place, on a posted - entry too, closed month included, since a flow moves no balance; the lines on the other side of the - entry that were in step with it follow. Posted entries accept nothing else and are corrected via - `create-event-block(event_type='journal_entry_reversed')`. + Update a draft journal entry, or re-tag any entry's flows or management dimensions. If line_items is + provided, existing line items are replaced atomically, the new set must balance, and a line on a + retired (`is_active=false`) account is refused. `line_flows` sets the cash flow row of the named + lines in place, on a posted entry too, closed month included, since a flow moves no balance; the + lines on the other side of the entry that were in step with it follow. `line_dimensions` sets the + department, class, location or project members the named lines carry, in place, on a posted entry + and a closed month too, since a dimension moves no balance either. Posted entries accept nothing + else and are corrected via `create-event-block(event_type='journal_entry_reversed')`. **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. @@ -234,9 +240,9 @@ async def asyncio_detailed( body (UpdateJournalEntryRequest): Update a draft journal entry, or re-tag any entry's flows. - Posted entries accept only `line_flows`: a flow moves no balance, so it - changes in place, closed month or not. Anything else on a posted entry is - a reversal and a corrected entry. + Posted entries accept only `line_flows` and `line_dimensions`: neither + moves a balance, so each changes in place, closed month or not. Anything + else on a posted entry is a reversal and a corrected entry. Omitted fields are left unchanged. If `line_items` is provided, the existing line items are replaced entirely (atomic delete + insert) @@ -271,12 +277,14 @@ async def asyncio( ) -> ErrorResponse | OperationEnvelopeJournalEntryResponse | None: """Update Journal Entry - Update a draft journal entry, or re-tag any entry's flows. If line_items is provided, existing line - items are replaced atomically, the new set must balance, and a line on a retired (`is_active=false`) - account is refused. `line_flows` sets the cash flow row of the named lines in place, on a posted - entry too, closed month included, since a flow moves no balance; the lines on the other side of the - entry that were in step with it follow. Posted entries accept nothing else and are corrected via - `create-event-block(event_type='journal_entry_reversed')`. + Update a draft journal entry, or re-tag any entry's flows or management dimensions. If line_items is + provided, existing line items are replaced atomically, the new set must balance, and a line on a + retired (`is_active=false`) account is refused. `line_flows` sets the cash flow row of the named + lines in place, on a posted entry too, closed month included, since a flow moves no balance; the + lines on the other side of the entry that were in step with it follow. `line_dimensions` sets the + department, class, location or project members the named lines carry, in place, on a posted entry + and a closed month too, since a dimension moves no balance either. Posted entries accept nothing + else and are corrected via `create-event-block(event_type='journal_entry_reversed')`. **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. @@ -287,9 +295,9 @@ async def asyncio( body (UpdateJournalEntryRequest): Update a draft journal entry, or re-tag any entry's flows. - Posted entries accept only `line_flows`: a flow moves no balance, so it - changes in place, closed month or not. Anything else on a posted entry is - a reversal and a corrected entry. + Posted entries accept only `line_flows` and `line_dimensions`: neither + moves a balance, so each changes in place, closed month or not. Anything + else on a posted entry is a reversal and a corrected entry. Omitted fields are left unchanged. If `line_items` is provided, the existing line items are replaced entirely (atomic delete + insert) diff --git a/robosystems_client/clients/ledger_client.py b/robosystems_client/clients/ledger_client.py index d3f2ba57..bd897bb0 100644 --- a/robosystems_client/clients/ledger_client.py +++ b/robosystems_client/clients/ledger_client.py @@ -81,6 +81,9 @@ from ..api.robo_ledger_ledger_events.create_agent import ( sync_detailed as op_create_agent, ) +from ..api.robo_ledger_ledger_events.create_dimension import ( + sync_detailed as op_create_dimension, +) from ..api.robo_ledger_ledger_events.create_event_block import ( sync_detailed as op_create_event_block, ) @@ -111,6 +114,9 @@ from ..api.robo_ledger_ledger_events.update_agent import ( sync_detailed as op_update_agent, ) +from ..api.robo_ledger_ledger_events.update_dimension import ( + sync_detailed as op_update_dimension, +) from ..api.robo_ledger_ledger_events.update_event_block import ( sync_detailed as op_update_event_block, ) @@ -477,6 +483,10 @@ DischargeCandidates, DischargeCandidatesDischargeCandidates, ) +from ..graphql.generated.list_ledger_dimensions import ( + ListLedgerDimensions, + ListLedgerDimensionsDimensions, +) from ..graphql.generated.open_items_for_agent import ( OpenItemsForAgent, OpenItemsForAgentOpenItemsForAgent, @@ -512,6 +522,7 @@ LIST_LEDGER_ACCOUNTS_GQL, LIST_LEDGER_BLOCKED_SOURCE_GRAPHS_GQL, LIST_LEDGER_AGENTS_GQL, + LIST_LEDGER_DIMENSIONS_GQL, LIST_LEDGER_ELEMENTS_GQL, LIST_LEDGER_ENTITIES_GQL, LIST_LEDGER_EVENT_BLOCKS_GQL, @@ -534,6 +545,9 @@ from ..models.add_publish_list_members_operation import AddPublishListMembersOperation from ..models.auto_map_elements_operation import AutoMapElementsOperation from ..models.create_agent_request import CreateAgentRequest +from ..models.create_dimension_request import CreateDimensionRequest +from ..models.dimension_response import DimensionResponse +from ..models.update_dimension_request import UpdateDimensionRequest from ..models.create_entity_request import CreateEntityRequest from ..models.link_bank_account_request import LinkBankAccountRequest from ..models.link_bank_account_response import LinkBankAccountResponse @@ -1261,15 +1275,81 @@ def get_trial_balance( start_date: str | None = None, end_date: str | None = None, entity_id: str | None = None, + *, + dimension_type: str | None = None, + dimension_member_id: str | None = None, ) -> TrialBalance | None: - """Trial balance by raw CoA account.""" + """Trial balance by raw CoA account. ``dimension_type`` with + ``dimension_member_id`` sums one management dimension member's lines + (``"unallocated"`` for the lines no member of the axis tags); such a + balance need not foot, and says so in ``sliced``.""" data = self._query( graph_id, GET_LEDGER_TRIAL_BALANCE_GQL, - {"startDate": start_date, "endDate": end_date, "entityId": entity_id}, + { + "startDate": start_date, + "endDate": end_date, + "entityId": entity_id, + "dimensionType": dimension_type, + "dimensionMemberId": dimension_member_id, + }, ) return GetLedgerTrialBalance.model_validate(data).trial_balance + # ── Management dimensions ───────────────────────────────────────────── + + def get_dimensions( + self, + graph_id: str, + *, + dimension_type: str | None = None, + include_inactive: bool = False, + ) -> ListLedgerDimensionsDimensions | None: + """The graph's management dimension members (departments, classes, + locations, projects), by axis then name, each with how many lines carry + it. ``types`` lists the axes the graph has members on; retired members + are left out unless ``include_inactive``.""" + data = self._query( + graph_id, + LIST_LEDGER_DIMENSIONS_GQL, + {"dimensionType": dimension_type, "includeInactive": include_inactive}, + ) + return ListLedgerDimensions.model_validate(data).dimensions + + def create_dimension( + self, + graph_id: str, + body: dict[str, Any], + idempotency_key: str | None = None, + ) -> DimensionResponse: + """Add a member to a management dimension (``dimension_type``: + department, class, location, project, or an axis of your own). ``value`` + defaults to a slug of the name; a duplicate key is refused (409).""" + request = CreateDimensionRequest.from_dict(body) + response = op_create_dimension( + graph_id=graph_id, + body=request, + client=self._get_client(), + idempotency_key=idempotency_key if idempotency_key is not None else UNSET, + ) + envelope = self._call_op("Create dimension", response) + return self._typed_result("Create dimension", envelope, DimensionResponse) + + def update_dimension( + self, + graph_id: str, + body: dict[str, Any], + ) -> DimensionResponse: + """Rename a member, or retire it with ``is_active=False``: it stays on the + lines that carry it and goes on no new ones. A member synced from + QuickBooks keeps QuickBooks's name.""" + request = UpdateDimensionRequest.from_dict(body) + response = op_update_dimension( + graph_id=graph_id, body=request, client=self._get_client() + ) + envelope = self._call_op("Update dimension", response) + return self._typed_result("Update dimension", envelope, DimensionResponse) + def get_mapped_trial_balance( self, graph_id: str, diff --git a/robosystems_client/graphql/generated/__init__.py b/robosystems_client/graphql/generated/__init__.py index 631934f1..94f5c1cd 100644 --- a/robosystems_client/graphql/generated/__init__.py +++ b/robosystems_client/graphql/generated/__init__.py @@ -197,6 +197,7 @@ GetLedgerTransactionTransaction, GetLedgerTransactionTransactionEntries, GetLedgerTransactionTransactionEntriesLineItems, + GetLedgerTransactionTransactionEntriesLineItemsDimensions, ) from .get_ledger_trial_balance import ( GetLedgerTrialBalance, @@ -292,6 +293,11 @@ ListLedgerBlockedSourceGraphsBlockedSourceGraphsBlockedSourceGraphs, ListLedgerBlockedSourceGraphsBlockedSourceGraphsPagination, ) +from .list_ledger_dimensions import ( + ListLedgerDimensions, + ListLedgerDimensionsDimensions, + ListLedgerDimensionsDimensionsDimensions, +) from .list_ledger_elements import ( ListLedgerElements, ListLedgerElementsElements, @@ -308,6 +314,7 @@ ListLedgerJournalEntriesJournalEntries, ListLedgerJournalEntriesJournalEntriesEntries, ListLedgerJournalEntriesJournalEntriesEntriesLineItems, + ListLedgerJournalEntriesJournalEntriesEntriesLineItemsDimensions, ListLedgerJournalEntriesJournalEntriesPagination, ) from .list_ledger_mappings import ( @@ -426,6 +433,7 @@ LIST_LEDGER_AGENTS_GQL, LIST_LEDGER_BANK_ACCOUNTS_GQL, LIST_LEDGER_BLOCKED_SOURCE_GRAPHS_GQL, + LIST_LEDGER_DIMENSIONS_GQL, LIST_LEDGER_ELEMENTS_GQL, LIST_LEDGER_ENTITIES_GQL, LIST_LEDGER_EVENT_BLOCKS_GQL, @@ -640,6 +648,7 @@ "GetLedgerTransactionTransaction", "GetLedgerTransactionTransactionEntries", "GetLedgerTransactionTransactionEntriesLineItems", + "GetLedgerTransactionTransactionEntriesLineItemsDimensions", "GetLedgerTrialBalance", "GetLedgerTrialBalanceTrialBalance", "GetLedgerTrialBalanceTrialBalanceRows", @@ -673,6 +682,7 @@ "LIST_LEDGER_AGENTS_GQL", "LIST_LEDGER_BANK_ACCOUNTS_GQL", "LIST_LEDGER_BLOCKED_SOURCE_GRAPHS_GQL", + "LIST_LEDGER_DIMENSIONS_GQL", "LIST_LEDGER_ELEMENTS_GQL", "LIST_LEDGER_ENTITIES_GQL", "LIST_LEDGER_EVENT_BLOCKS_GQL", @@ -739,6 +749,9 @@ "ListLedgerBlockedSourceGraphsBlockedSourceGraphs", "ListLedgerBlockedSourceGraphsBlockedSourceGraphsBlockedSourceGraphs", "ListLedgerBlockedSourceGraphsBlockedSourceGraphsPagination", + "ListLedgerDimensions", + "ListLedgerDimensionsDimensions", + "ListLedgerDimensionsDimensionsDimensions", "ListLedgerElements", "ListLedgerElementsElements", "ListLedgerElementsElementsElements", @@ -751,6 +764,7 @@ "ListLedgerJournalEntriesJournalEntries", "ListLedgerJournalEntriesJournalEntriesEntries", "ListLedgerJournalEntriesJournalEntriesEntriesLineItems", + "ListLedgerJournalEntriesJournalEntriesEntriesLineItemsDimensions", "ListLedgerJournalEntriesJournalEntriesPagination", "ListLedgerMappings", "ListLedgerMappingsMappings", diff --git a/robosystems_client/graphql/generated/client.py b/robosystems_client/graphql/generated/client.py index 71202e3a..1eb103f3 100644 --- a/robosystems_client/graphql/generated/client.py +++ b/robosystems_client/graphql/generated/client.py @@ -49,6 +49,7 @@ from .list_ledger_agents import ListLedgerAgents from .list_ledger_bank_accounts import ListLedgerBankAccounts from .list_ledger_blocked_source_graphs import ListLedgerBlockedSourceGraphs +from .list_ledger_dimensions import ListLedgerDimensions from .list_ledger_elements import ListLedgerElements from .list_ledger_entities import ListLedgerEntities from .list_ledger_event_blocks import ListLedgerEventBlocks @@ -114,6 +115,7 @@ LIST_LEDGER_AGENTS_GQL, LIST_LEDGER_BANK_ACCOUNTS_GQL, LIST_LEDGER_BLOCKED_SOURCE_GRAPHS_GQL, + LIST_LEDGER_DIMENSIONS_GQL, LIST_LEDGER_ELEMENTS_GQL, LIST_LEDGER_ENTITIES_GQL, LIST_LEDGER_EVENT_BLOCKS_GQL, @@ -652,12 +654,16 @@ def get_ledger_trial_balance( start_date: Union[Optional[str], UnsetType] = UNSET, end_date: Union[Optional[str], UnsetType] = UNSET, entity_id: Union[Optional[str], UnsetType] = UNSET, + dimension_type: Union[Optional[str], UnsetType] = UNSET, + dimension_member_id: Union[Optional[str], UnsetType] = UNSET, **kwargs: Any, ) -> GetLedgerTrialBalance: variables: dict[str, object] = { "startDate": start_date, "endDate": end_date, "entityId": entity_id, + "dimensionType": dimension_type, + "dimensionMemberId": dimension_member_id, } response = self.execute( query=GET_LEDGER_TRIAL_BALANCE_GQL, @@ -809,6 +815,25 @@ def list_ledger_blocked_source_graphs( data = self.get_data(response) return ListLedgerBlockedSourceGraphs.model_validate(data) + def list_ledger_dimensions( + self, + include_inactive: bool, + dimension_type: Union[Optional[str], UnsetType] = UNSET, + **kwargs: Any, + ) -> ListLedgerDimensions: + variables: dict[str, object] = { + "dimensionType": dimension_type, + "includeInactive": include_inactive, + } + response = self.execute( + query=LIST_LEDGER_DIMENSIONS_GQL, + operation_name="ListLedgerDimensions", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return ListLedgerDimensions.model_validate(data) + def list_ledger_elements( self, limit: int, diff --git a/robosystems_client/graphql/generated/get_ledger_transaction.py b/robosystems_client/graphql/generated/get_ledger_transaction.py index a7f57b27..95ca390a 100644 --- a/robosystems_client/graphql/generated/get_ledger_transaction.py +++ b/robosystems_client/graphql/generated/get_ledger_transaction.py @@ -50,8 +50,17 @@ class GetLedgerTransactionTransactionEntriesLineItems(BaseModel): credit_amount: float = Field(alias="creditAmount") description: Optional[str] line_order: int = Field(alias="lineOrder") + flow_qname: Optional[str] = Field(alias="flowQname") + dimensions: list["GetLedgerTransactionTransactionEntriesLineItemsDimensions"] + + +class GetLedgerTransactionTransactionEntriesLineItemsDimensions(BaseModel): + id: str + dimension_type: str = Field(alias="dimensionType") + name: str GetLedgerTransaction.model_rebuild() GetLedgerTransactionTransaction.model_rebuild() GetLedgerTransactionTransactionEntries.model_rebuild() +GetLedgerTransactionTransactionEntriesLineItems.model_rebuild() diff --git a/robosystems_client/graphql/generated/get_ledger_trial_balance.py b/robosystems_client/graphql/generated/get_ledger_trial_balance.py index 08fd9c8c..d96e21fd 100644 --- a/robosystems_client/graphql/generated/get_ledger_trial_balance.py +++ b/robosystems_client/graphql/generated/get_ledger_trial_balance.py @@ -14,6 +14,7 @@ class GetLedgerTrialBalance(BaseModel): class GetLedgerTrialBalanceTrialBalance(BaseModel): total_debits: float = Field(alias="totalDebits") total_credits: float = Field(alias="totalCredits") + sliced: bool rows: list["GetLedgerTrialBalanceTrialBalanceRows"] diff --git a/robosystems_client/graphql/generated/list_ledger_dimensions.py b/robosystems_client/graphql/generated/list_ledger_dimensions.py new file mode 100644 index 00000000..6ec30d56 --- /dev/null +++ b/robosystems_client/graphql/generated/list_ledger_dimensions.py @@ -0,0 +1,29 @@ +from typing import Optional + +from pydantic import Field + +from .base_model import BaseModel + + +class ListLedgerDimensions(BaseModel): + dimensions: Optional["ListLedgerDimensionsDimensions"] + + +class ListLedgerDimensionsDimensions(BaseModel): + types: list[str] + dimensions: list["ListLedgerDimensionsDimensionsDimensions"] + + +class ListLedgerDimensionsDimensionsDimensions(BaseModel): + id: str + dimension_type: str = Field(alias="dimensionType") + name: str + value: str + source: str + is_active: bool = Field(alias="isActive") + line_count: int = Field(alias="lineCount") + created_at: Optional[str] = Field(alias="createdAt") + + +ListLedgerDimensions.model_rebuild() +ListLedgerDimensionsDimensions.model_rebuild() diff --git a/robosystems_client/graphql/generated/list_ledger_journal_entries.py b/robosystems_client/graphql/generated/list_ledger_journal_entries.py index fe038633..2a2c7477 100644 --- a/robosystems_client/graphql/generated/list_ledger_journal_entries.py +++ b/robosystems_client/graphql/generated/list_ledger_journal_entries.py @@ -48,6 +48,13 @@ class ListLedgerJournalEntriesJournalEntriesEntriesLineItems(BaseModel): description: Optional[str] line_order: int = Field(alias="lineOrder") flow_qname: Optional[str] = Field(alias="flowQname") + dimensions: list["ListLedgerJournalEntriesJournalEntriesEntriesLineItemsDimensions"] + + +class ListLedgerJournalEntriesJournalEntriesEntriesLineItemsDimensions(BaseModel): + id: str + dimension_type: str = Field(alias="dimensionType") + name: str class ListLedgerJournalEntriesJournalEntriesPagination(BaseModel): @@ -60,3 +67,4 @@ class ListLedgerJournalEntriesJournalEntriesPagination(BaseModel): ListLedgerJournalEntries.model_rebuild() ListLedgerJournalEntriesJournalEntries.model_rebuild() ListLedgerJournalEntriesJournalEntriesEntries.model_rebuild() +ListLedgerJournalEntriesJournalEntriesEntriesLineItems.model_rebuild() diff --git a/robosystems_client/graphql/generated/operations.py b/robosystems_client/graphql/generated/operations.py index 11cf2766..4eb6840a 100644 --- a/robosystems_client/graphql/generated/operations.py +++ b/robosystems_client/graphql/generated/operations.py @@ -45,6 +45,7 @@ "LIST_LEDGER_AGENTS_GQL", "LIST_LEDGER_BANK_ACCOUNTS_GQL", "LIST_LEDGER_BLOCKED_SOURCE_GRAPHS_GQL", + "LIST_LEDGER_DIMENSIONS_GQL", "LIST_LEDGER_ELEMENTS_GQL", "LIST_LEDGER_ENTITIES_GQL", "LIST_LEDGER_EVENT_BLOCKS_GQL", @@ -1400,6 +1401,12 @@ creditAmount description lineOrder + flowQname + dimensions { + id + dimensionType + name + } } } } @@ -1407,10 +1414,17 @@ """ GET_LEDGER_TRIAL_BALANCE_GQL = """ -query GetLedgerTrialBalance($startDate: Date, $endDate: Date, $entityId: String) { - trialBalance(startDate: $startDate, endDate: $endDate, entityId: $entityId) { +query GetLedgerTrialBalance($startDate: Date, $endDate: Date, $entityId: String, $dimensionType: String, $dimensionMemberId: String) { + trialBalance( + startDate: $startDate + endDate: $endDate + entityId: $entityId + dimensionType: $dimensionType + dimensionMemberId: $dimensionMemberId + ) { totalDebits totalCredits + sliced rows { accountId accountCode @@ -1744,6 +1758,24 @@ } """ +LIST_LEDGER_DIMENSIONS_GQL = """ +query ListLedgerDimensions($dimensionType: String, $includeInactive: Boolean! = false) { + dimensions(dimensionType: $dimensionType, includeInactive: $includeInactive) { + types + dimensions { + id + dimensionType + name + value + source + isActive + lineCount + createdAt + } + } +} +""" + LIST_LEDGER_ELEMENTS_GQL = """ query ListLedgerElements($taxonomyId: String, $source: String, $classification: String, $isAbstract: Boolean, $limit: Int! = 100, $offset: Int! = 0) { elements( @@ -1894,6 +1926,11 @@ description lineOrder flowQname + dimensions { + id + dimensionType + name + } } } pagination { diff --git a/robosystems_client/graphql/operations/ledger/GetLedgerTransaction.graphql b/robosystems_client/graphql/operations/ledger/GetLedgerTransaction.graphql index 16c768e9..324b92a5 100644 --- a/robosystems_client/graphql/operations/ledger/GetLedgerTransaction.graphql +++ b/robosystems_client/graphql/operations/ledger/GetLedgerTransaction.graphql @@ -6,7 +6,8 @@ query GetLedgerTransaction($transactionId: String!) { id number type postingDate memo status postedAt lineItems { id accountId accountName accountCode - debitAmount creditAmount description lineOrder + debitAmount creditAmount description lineOrder flowQname + dimensions { id dimensionType name } } } } diff --git a/robosystems_client/graphql/operations/ledger/GetLedgerTrialBalance.graphql b/robosystems_client/graphql/operations/ledger/GetLedgerTrialBalance.graphql index ccbbce3e..4df10a1e 100644 --- a/robosystems_client/graphql/operations/ledger/GetLedgerTrialBalance.graphql +++ b/robosystems_client/graphql/operations/ledger/GetLedgerTrialBalance.graphql @@ -1,7 +1,20 @@ -query GetLedgerTrialBalance($startDate: Date, $endDate: Date, $entityId: String) { - trialBalance(startDate: $startDate, endDate: $endDate, entityId: $entityId) { +query GetLedgerTrialBalance( + $startDate: Date + $endDate: Date + $entityId: String + $dimensionType: String + $dimensionMemberId: String +) { + trialBalance( + startDate: $startDate + endDate: $endDate + entityId: $entityId + dimensionType: $dimensionType + dimensionMemberId: $dimensionMemberId + ) { totalDebits totalCredits + sliced rows { accountId accountCode diff --git a/robosystems_client/graphql/operations/ledger/ListLedgerDimensions.graphql b/robosystems_client/graphql/operations/ledger/ListLedgerDimensions.graphql new file mode 100644 index 00000000..82b663c4 --- /dev/null +++ b/robosystems_client/graphql/operations/ledger/ListLedgerDimensions.graphql @@ -0,0 +1,15 @@ +query ListLedgerDimensions($dimensionType: String, $includeInactive: Boolean! = false) { + dimensions(dimensionType: $dimensionType, includeInactive: $includeInactive) { + types + dimensions { + id + dimensionType + name + value + source + isActive + lineCount + createdAt + } + } +} diff --git a/robosystems_client/graphql/operations/ledger/ListLedgerJournalEntries.graphql b/robosystems_client/graphql/operations/ledger/ListLedgerJournalEntries.graphql index ae310bc8..4802b31a 100644 --- a/robosystems_client/graphql/operations/ledger/ListLedgerJournalEntries.graphql +++ b/robosystems_client/graphql/operations/ledger/ListLedgerJournalEntries.graphql @@ -28,6 +28,7 @@ query ListLedgerJournalEntries( lineItems { id accountId accountName accountCode debitAmount creditAmount description lineOrder flowQname + dimensions { id dimensionType name } } } pagination { total limit offset hasMore } diff --git a/robosystems_client/graphql/schema.graphql b/robosystems_client/graphql/schema.graphql index b6602024..7611f803 100644 --- a/robosystems_client/graphql/schema.graphql +++ b/robosystems_client/graphql/schema.graphql @@ -384,8 +384,31 @@ type Query { """The entity whose books to read. Omit for the group parent.""" entityId: String = null + + """ + A management dimension axis (department, class, location, project) to slice by, with dimension_member_id. + """ + dimensionType: String = null + + """ + The one member whose lines to sum, or `unallocated` for the lines no member of the axis tags. + """ + dimensionMemberId: String = null ): TrialBalance + """ + The graph's management dimension members (departments, classes, + locations, projects), by axis then name, each with how many lines carry + it. + """ + dimensions( + """One axis; omit for all.""" + dimensionType: String = null + + """Also list retired members.""" + includeInactive: Boolean! = false + ): DimensionList + """Paginated list of one entity's transactions.""" transactions( """Filter by transaction type.""" @@ -2514,6 +2537,11 @@ type TrialBalance { rows: [TrialBalanceRow!]! totalDebits: Float! totalCredits: Float! + + """ + True when read for one management dimension member (or its axis's Unallocated). Such a balance need not foot: an entry's other side may carry another member or none. + """ + sliced: Boolean! } """One CoA account's debit/credit totals over the trial-balance window.""" @@ -2528,6 +2556,46 @@ type TrialBalanceRow { netBalance: Float! } +"""The graph's members of its management dimensions, by axis then name.""" +type DimensionList { + """The members, ordered by axis and then name.""" + dimensions: [Dimension!]! + + """The axes the graph has members on, in order.""" + types: [String!]! +} + +"""One member of a management dimension.""" +type Dimension { + """The member's id, as lines carry it.""" + id: String! + + """The axis: department, class, location, project, or another.""" + dimensionType: String! + + """The member's label.""" + name: String! + + """ + The member's stable key within its axis: the source's id for a synced member, a slug of the name for one created here. + """ + value: String! + + """Where the member came from: `native` or `quickbooks`.""" + source: String! + + """ + False once retired: the member stays on the lines that carry it and can no longer be put on new ones. + """ + isActive: Boolean! + + """How many ledger lines carry the member.""" + lineCount: Int! + + """When it was created.""" + createdAt: DateTime +} + """Paginated transaction listing — header view.""" type LedgerTransactionList { transactions: [LedgerTransactionSummary!]! @@ -2614,6 +2682,23 @@ type LedgerLineItem { description: String lineOrder: Int! flowQname: String + + """ + The management dimension members the line carries (department, class, location, project); empty when untagged. + """ + dimensions: [LineDimension!]! +} + +"""A member a ledger line carries.""" +type LineDimension { + """The member's id.""" + id: String! + + """The member's axis.""" + dimensionType: String! + + """The member's label.""" + name: String! } """ diff --git a/robosystems_client/models/__init__.py b/robosystems_client/models/__init__.py index 9ccf69f2..a0c48fc9 100644 --- a/robosystems_client/models/__init__.py +++ b/robosystems_client/models/__init__.py @@ -110,6 +110,7 @@ from .create_checkout_request_resource_config import CreateCheckoutRequestResourceConfig from .create_connection_request import CreateConnectionRequest from .create_connection_request_provider import CreateConnectionRequestProvider +from .create_dimension_request import CreateDimensionRequest from .create_document_upload_op import CreateDocumentUploadOp from .create_document_upload_op_content_type import CreateDocumentUploadOpContentType from .create_entity_request import CreateEntityRequest @@ -229,6 +230,7 @@ ) from .detailed_transactions_response_summary import DetailedTransactionsResponseSummary from .dimension import Dimension +from .dimension_response import DimensionResponse from .dimension_type import DimensionType from .discharge_allocation import DischargeAllocation from .disclosures_request import DisclosuresRequest @@ -388,6 +390,7 @@ from .invoice import Invoice from .invoice_line_item import InvoiceLineItem from .invoices_response import InvoicesResponse +from .journal_entry_line_dimensions import JournalEntryLineDimensions from .journal_entry_line_flow import JournalEntryLineFlow from .journal_entry_line_item_input import JournalEntryLineItemInput from .journal_entry_line_item_input_metadata_type_0 import ( @@ -581,6 +584,10 @@ from .operation_envelope_describe_filing_response_status import ( OperationEnvelopeDescribeFilingResponseStatus, ) +from .operation_envelope_dimension_response import OperationEnvelopeDimensionResponse +from .operation_envelope_dimension_response_status import ( + OperationEnvelopeDimensionResponseStatus, +) from .operation_envelope_disclosures_response import ( OperationEnvelopeDisclosuresResponse, ) @@ -1052,6 +1059,9 @@ from .sso_exchange_request import SSOExchangeRequest from .sso_exchange_response import SSOExchangeResponse from .sso_token_response import SSOTokenResponse +from .statement_dimension import StatementDimension +from .statement_dimension_column import StatementDimensionColumn +from .statement_dimension_response import StatementDimensionResponse from .statement_mechanics import StatementMechanics from .storage_item import StorageItem from .storage_limits import StorageLimits @@ -1135,6 +1145,7 @@ ) from .update_agent_request_metadata_patch import UpdateAgentRequestMetadataPatch from .update_api_key_request import UpdateAPIKeyRequest +from .update_dimension_request import UpdateDimensionRequest from .update_entity_request import UpdateEntityRequest from .update_entity_request_consolidation_method_type_0 import ( UpdateEntityRequestConsolidationMethodType0, @@ -1312,6 +1323,7 @@ "CreateCheckoutRequestResourceConfig", "CreateConnectionRequest", "CreateConnectionRequestProvider", + "CreateDimensionRequest", "CreateDocumentUploadOp", "CreateDocumentUploadOpContentType", "CreateEntityRequest", @@ -1405,6 +1417,7 @@ "DetailedTransactionsResponseDateRange", "DetailedTransactionsResponseSummary", "Dimension", + "DimensionResponse", "DimensionType", "DischargeAllocation", "DisclosuresRequest", @@ -1526,6 +1539,7 @@ "Invoice", "InvoiceLineItem", "InvoicesResponse", + "JournalEntryLineDimensions", "JournalEntryLineFlow", "JournalEntryLineItemInput", "JournalEntryLineItemInputMetadataType0", @@ -1633,6 +1647,8 @@ "OperationEnvelopeDeleteTaxonomyBlockResponseStatus", "OperationEnvelopeDescribeFilingResponse", "OperationEnvelopeDescribeFilingResponseStatus", + "OperationEnvelopeDimensionResponse", + "OperationEnvelopeDimensionResponseStatus", "OperationEnvelopeDisclosuresResponse", "OperationEnvelopeDisclosuresResponseStatus", "OperationEnvelopeEntityTaxonomyResponse", @@ -1910,6 +1926,9 @@ "SSOExchangeRequest", "SSOExchangeResponse", "SSOTokenResponse", + "StatementDimension", + "StatementDimensionColumn", + "StatementDimensionResponse", "StatementMechanics", "StorageItem", "StorageLimits", @@ -1969,6 +1988,7 @@ "UpdateAgentRequestClassificationModeType0", "UpdateAgentRequestMetadataPatch", "UpdateAPIKeyRequest", + "UpdateDimensionRequest", "UpdateEntityRequest", "UpdateEntityRequestConsolidationMethodType0", "UpdateEntityRequestEntityRoleType0", diff --git a/robosystems_client/models/create_dimension_request.py b/robosystems_client/models/create_dimension_request.py new file mode 100644 index 00000000..fb3ad193 --- /dev/null +++ b/robosystems_client/models/create_dimension_request.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CreateDimensionRequest") + + +@_attrs_define +class CreateDimensionRequest: + """Add a member to a management dimension. + + Attributes: + dimension_type (str): The axis: `department`, `class`, `location` or `project`, or any other lowercase name, + which starts an axis of its own. `scenario` is reserved for forecasts. + name (str): The member's label. + value (None | str | Unset): The member's stable key within the axis; omitted, a slug of the name. Refused (409) + when the axis already has it. + """ + + dimension_type: str + name: str + value: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + dimension_type = self.dimension_type + + name = self.name + + value: None | str | Unset + if isinstance(self.value, Unset): + value = UNSET + else: + value = self.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "dimension_type": dimension_type, + "name": name, + } + ) + if value is not UNSET: + field_dict["value"] = value + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + dimension_type = d.pop("dimension_type") + + name = d.pop("name") + + def _parse_value(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + value = _parse_value(d.pop("value", UNSET)) + + create_dimension_request = cls( + dimension_type=dimension_type, + name=name, + value=value, + ) + + create_dimension_request.additional_properties = d + return create_dimension_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/robosystems_client/models/create_event_block_request.py b/robosystems_client/models/create_event_block_request.py index 39771916..c76e2bca 100644 --- a/robosystems_client/models/create_event_block_request.py +++ b/robosystems_client/models/create_event_block_request.py @@ -74,8 +74,8 @@ class CreateEventBlockRequest: metadata (CreateEventBlockRequestMetadata | Unset): Free-form payload, opaque to the event surface. When `apply_handlers=True`, the matched handler's metadata schema validates this dict — required keys depend on the handler registered for `event_type`. Use `preview-event-block` to discover the expected shape without writing. - dimension_ids (list[str] | Unset): IDs of dimension members tagging this event (e.g. department, fund, project). - Propagate to the GL entries produced by the handler. + dimension_ids (list[str] | Unset): Management dimension members tagging this event (department, class, location, + project), one per axis. Every line its handler writes carries them on the axes the line does not tag itself. obligated_by_event_id (None | str | Unset): Forward-materialization link: the event that scheduled or obligated this one (e.g. depreciation entries point at the asset_acquired event). discharges_event_id (None | str | Unset): Settlement link: the obligation this event discharges (e.g. diff --git a/robosystems_client/models/dimension_response.py b/robosystems_client/models/dimension_response.py new file mode 100644 index 00000000..4b989405 --- /dev/null +++ b/robosystems_client/models/dimension_response.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="DimensionResponse") + + +@_attrs_define +class DimensionResponse: + """One member of a management dimension. + + Attributes: + id (str): The member's id, as lines carry it. + dimension_type (str): The axis: department, class, location, project, or another. + name (str): The member's label. + value (str): The member's stable key within its axis: the source's id for a synced member, a slug of the name + for one created here. + is_active (bool): False once retired: the member stays on the lines that carry it and can no longer be put on + new ones. + source (str | Unset): Where the member came from: `native` or `quickbooks`. Default: 'native'. + line_count (int | Unset): How many ledger lines carry the member. Default: 0. + created_at (datetime.datetime | None | Unset): When it was created. + """ + + id: str + dimension_type: str + name: str + value: str + is_active: bool + source: str | Unset = "native" + line_count: int | Unset = 0 + created_at: datetime.datetime | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + id = self.id + + dimension_type = self.dimension_type + + name = self.name + + value = self.value + + is_active = self.is_active + + source = self.source + + line_count = self.line_count + + created_at: None | str | Unset + if isinstance(self.created_at, Unset): + created_at = UNSET + elif isinstance(self.created_at, datetime.datetime): + created_at = self.created_at.isoformat() + else: + created_at = self.created_at + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "id": id, + "dimension_type": dimension_type, + "name": name, + "value": value, + "is_active": is_active, + } + ) + if source is not UNSET: + field_dict["source"] = source + if line_count is not UNSET: + field_dict["line_count"] = line_count + if created_at is not UNSET: + field_dict["created_at"] = created_at + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + id = d.pop("id") + + dimension_type = d.pop("dimension_type") + + name = d.pop("name") + + value = d.pop("value") + + is_active = d.pop("is_active") + + source = d.pop("source", UNSET) + + line_count = d.pop("line_count", UNSET) + + def _parse_created_at(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + created_at_type_0 = datetime.datetime.fromisoformat(data) + + return created_at_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + created_at = _parse_created_at(d.pop("created_at", UNSET)) + + dimension_response = cls( + id=id, + dimension_type=dimension_type, + name=name, + value=value, + is_active=is_active, + source=source, + line_count=line_count, + created_at=created_at, + ) + + dimension_response.additional_properties = d + return dimension_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/entry_template_request.py b/robosystems_client/models/entry_template_request.py index 12c66052..aefa2a56 100644 --- a/robosystems_client/models/entry_template_request.py +++ b/robosystems_client/models/entry_template_request.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Any, TypeVar +from typing import Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -25,6 +25,9 @@ class EntryTemplateRequest: EntryTemplateRequestEntryType.CLOSING. memo_template (str | Unset): Memo template ({structure_name} is replaced) Default: ''. auto_reverse (bool | Unset): Auto-generate a reversing entry on the first day of the next period Default: False. + dimension_ids (list[str] | Unset): The management dimension members (department, class, location, project) every + entry the schedule drafts carries on both lines, one per axis: a department's depreciation lands in that + department. """ debit_element_id: str @@ -34,6 +37,7 @@ class EntryTemplateRequest: ) memo_template: str | Unset = "" auto_reverse: bool | Unset = False + dimension_ids: list[str] | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -49,6 +53,10 @@ def to_dict(self) -> dict[str, Any]: auto_reverse = self.auto_reverse + dimension_ids: list[str] | Unset = UNSET + if not isinstance(self.dimension_ids, Unset): + dimension_ids = self.dimension_ids + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -63,6 +71,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["memo_template"] = memo_template if auto_reverse is not UNSET: field_dict["auto_reverse"] = auto_reverse + if dimension_ids is not UNSET: + field_dict["dimension_ids"] = dimension_ids return field_dict @@ -84,12 +94,15 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: auto_reverse = d.pop("auto_reverse", UNSET) + dimension_ids = cast(list[str], d.pop("dimension_ids", UNSET)) + entry_template_request = cls( debit_element_id=debit_element_id, credit_element_id=credit_element_id, entry_type=entry_type, memo_template=memo_template, auto_reverse=auto_reverse, + dimension_ids=dimension_ids, ) entry_template_request.additional_properties = d diff --git a/robosystems_client/models/journal_entry_line_dimensions.py b/robosystems_client/models/journal_entry_line_dimensions.py new file mode 100644 index 00000000..0ee8a1f8 --- /dev/null +++ b/robosystems_client/models/journal_entry_line_dimensions.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="JournalEntryLineDimensions") + + +@_attrs_define +class JournalEntryLineDimensions: + """A line's management dimension members, set in place. + + Attributes: + line_id (str): The line to re-tag, by id. + dimension_ids (list[str]): The members the line carries from now on, replacing what it carried; empty clears + them. At most one per axis. + """ + + line_id: str + dimension_ids: list[str] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + line_id = self.line_id + + dimension_ids = self.dimension_ids + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "line_id": line_id, + "dimension_ids": dimension_ids, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + line_id = d.pop("line_id") + + dimension_ids = cast(list[str], d.pop("dimension_ids")) + + journal_entry_line_dimensions = cls( + line_id=line_id, + dimension_ids=dimension_ids, + ) + + journal_entry_line_dimensions.additional_properties = d + return journal_entry_line_dimensions + + @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/journal_entry_line_item_input.py b/robosystems_client/models/journal_entry_line_item_input.py index 73c530ac..dfeb8d49 100644 --- a/robosystems_client/models/journal_entry_line_item_input.py +++ b/robosystems_client/models/journal_entry_line_item_input.py @@ -37,6 +37,9 @@ class JournalEntryLineItemInput: counts on its own, cash-effect signed (a credit is an inflow), so an elimination offsets the flow a member's line produced, and a tag does not spread to the other side. Refused when it is not a row of the entity's cash flow statement. + dimension_ids (list[str] | None | Unset): The management dimension members this line carries (department, class, + location, project), by id from `dimensions`; at most one per axis. An untagged line is read as the axis's + Unallocated. metadata (JournalEntryLineItemInputMetadataType0 | None | Unset): Optional per-line metadata stamped on `LineItem.metadata_`. Used to carry source-system fields the standard columns don't cover — e.g. an external flow-tag code that drives rollforward attribution (`transaction_description_code`), an external memo, or a cost- @@ -49,6 +52,7 @@ class JournalEntryLineItemInput: credit_amount: int | Unset = 0 description: None | str | Unset = UNSET flow_qname: None | str | Unset = UNSET + dimension_ids: list[str] | None | Unset = UNSET metadata: JournalEntryLineItemInputMetadataType0 | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -75,6 +79,15 @@ def to_dict(self) -> dict[str, Any]: else: flow_qname = self.flow_qname + dimension_ids: list[str] | None | Unset + if isinstance(self.dimension_ids, Unset): + dimension_ids = UNSET + elif isinstance(self.dimension_ids, list): + dimension_ids = self.dimension_ids + + else: + dimension_ids = self.dimension_ids + metadata: dict[str, Any] | None | Unset if isinstance(self.metadata, Unset): metadata = UNSET @@ -98,6 +111,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["description"] = description if flow_qname is not UNSET: field_dict["flow_qname"] = flow_qname + if dimension_ids is not UNSET: + field_dict["dimension_ids"] = dimension_ids if metadata is not UNSET: field_dict["metadata"] = metadata @@ -134,6 +149,23 @@ def _parse_flow_qname(data: object) -> None | str | Unset: flow_qname = _parse_flow_qname(d.pop("flow_qname", UNSET)) + def _parse_dimension_ids(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + dimension_ids_type_0 = cast(list[str], data) + + return dimension_ids_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + dimension_ids = _parse_dimension_ids(d.pop("dimension_ids", UNSET)) + def _parse_metadata( data: object, ) -> JournalEntryLineItemInputMetadataType0 | None | Unset: @@ -159,6 +191,7 @@ def _parse_metadata( credit_amount=credit_amount, description=description, flow_qname=flow_qname, + dimension_ids=dimension_ids, metadata=metadata, ) diff --git a/robosystems_client/models/journal_entry_line_item_response.py b/robosystems_client/models/journal_entry_line_item_response.py index 8b530189..a9a91a20 100644 --- a/robosystems_client/models/journal_entry_line_item_response.py +++ b/robosystems_client/models/journal_entry_line_item_response.py @@ -24,6 +24,7 @@ class JournalEntryLineItemResponse: description (None | str | Unset): flow_qname (None | str | Unset): The cash flow row this line carries, when tagged; null means the account's default flow. + dimension_ids (list[str] | Unset): The management dimension members the line carries, by id. """ id: str @@ -33,6 +34,7 @@ class JournalEntryLineItemResponse: line_order: int description: None | str | Unset = UNSET flow_qname: None | str | Unset = UNSET + dimension_ids: list[str] | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -58,6 +60,10 @@ def to_dict(self) -> dict[str, Any]: else: flow_qname = self.flow_qname + dimension_ids: list[str] | Unset = UNSET + if not isinstance(self.dimension_ids, Unset): + dimension_ids = self.dimension_ids + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -73,6 +79,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["description"] = description if flow_qname is not UNSET: field_dict["flow_qname"] = flow_qname + if dimension_ids is not UNSET: + field_dict["dimension_ids"] = dimension_ids return field_dict @@ -107,6 +115,8 @@ def _parse_flow_qname(data: object) -> None | str | Unset: flow_qname = _parse_flow_qname(d.pop("flow_qname", UNSET)) + dimension_ids = cast(list[str], d.pop("dimension_ids", UNSET)) + journal_entry_line_item_response = cls( id=id, element_id=element_id, @@ -115,6 +125,7 @@ def _parse_flow_qname(data: object) -> None | str | Unset: line_order=line_order, description=description, flow_qname=flow_qname, + dimension_ids=dimension_ids, ) journal_entry_line_item_response.additional_properties = d diff --git a/robosystems_client/models/live_financial_statement_request.py b/robosystems_client/models/live_financial_statement_request.py index e70ef3e0..8a4ba727 100644 --- a/robosystems_client/models/live_financial_statement_request.py +++ b/robosystems_client/models/live_financial_statement_request.py @@ -2,7 +2,7 @@ import datetime from collections.abc import Mapping -from typing import Any, TypeVar, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,6 +12,10 @@ ) from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.statement_dimension import StatementDimension + + T = TypeVar("T", bound="LiveFinancialStatementRequest") @@ -41,6 +45,12 @@ class LiveFinancialStatementRequest: wins when both are given. Default: False. limit (int | Unset): Max fact rows returned. Defaults to the ceiling so a statement is never cut mid-section — visible rows would stop footing to visible subtotals. Lower it only for a preview. Default: 1000. + dimension (None | StatementDimension | Unset): Slice an income statement by a management dimension (department, + class, location, project). `{type, member_id}` reads one member's lines, current and prior; `member_id: + unallocated` reads the lines no member of the axis tags. `{type}` alone reads the current window with one column + per member, then Unallocated, then Total, and says in `dimension.ties_out` whether every row's members plus + Unallocated equal its Total. Composes with `scope` and `entity_id`; a consolidation entity's lines are untagged, + so eliminations read as Unallocated. Refused (422) on any other statement. """ statement_type: str @@ -52,9 +62,12 @@ class LiveFinancialStatementRequest: scope: LiveFinancialStatementRequestScopeType0 | None | Unset = UNSET consolidated: bool | Unset = False limit: int | Unset = 1000 + dimension: None | StatementDimension | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + from ..models.statement_dimension import StatementDimension + statement_type = self.statement_type entity_id: None | str | Unset @@ -103,6 +116,14 @@ def to_dict(self) -> dict[str, Any]: limit = self.limit + dimension: dict[str, Any] | None | Unset + if isinstance(self.dimension, Unset): + dimension = UNSET + elif isinstance(self.dimension, StatementDimension): + dimension = self.dimension.to_dict() + else: + dimension = self.dimension + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -126,11 +147,15 @@ def to_dict(self) -> dict[str, Any]: field_dict["consolidated"] = consolidated if limit is not UNSET: field_dict["limit"] = limit + if dimension is not UNSET: + field_dict["dimension"] = dimension return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.statement_dimension import StatementDimension + d = dict(src_dict) statement_type = d.pop("statement_type") @@ -218,6 +243,23 @@ def _parse_scope( limit = d.pop("limit", UNSET) + def _parse_dimension(data: object) -> None | StatementDimension | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + dimension_type_0 = StatementDimension.from_dict(data) + + return dimension_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | StatementDimension | Unset, data) + + dimension = _parse_dimension(d.pop("dimension", UNSET)) + live_financial_statement_request = cls( statement_type=statement_type, entity_id=entity_id, @@ -228,6 +270,7 @@ def _parse_scope( scope=scope, consolidated=consolidated, limit=limit, + dimension=dimension, ) live_financial_statement_request.additional_properties = d diff --git a/robosystems_client/models/live_financial_statement_response.py b/robosystems_client/models/live_financial_statement_response.py index 5456338f..8709e37e 100644 --- a/robosystems_client/models/live_financial_statement_response.py +++ b/robosystems_client/models/live_financial_statement_response.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from ..models.live_statement_fact_row import LiveStatementFactRow from ..models.period_spec import PeriodSpec + from ..models.statement_dimension_response import StatementDimensionResponse from ..models.validation_check_response import ValidationCheckResponse @@ -44,6 +45,8 @@ class LiveFinancialStatementResponse: equation, net-income equation, totals footing, operating-plug size. Null only when no structure rendered. unmapped_count (int | Unset): Default: 0. truncated (bool | Unset): Default: False. + dimension (None | StatementDimensionResponse | Unset): How the statement was sliced by a management dimension, + when it was: the axis, the one member read, or the columns and whether they tie out to the Total. """ graph_id: str @@ -59,9 +62,11 @@ class LiveFinancialStatementResponse: validation: None | Unset | ValidationCheckResponse = UNSET unmapped_count: int | Unset = 0 truncated: bool | Unset = False + dimension: None | StatementDimensionResponse | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + from ..models.statement_dimension_response import StatementDimensionResponse from ..models.validation_check_response import ValidationCheckResponse graph_id = self.graph_id @@ -113,6 +118,14 @@ def to_dict(self) -> dict[str, Any]: truncated = self.truncated + dimension: dict[str, Any] | None | Unset + if isinstance(self.dimension, Unset): + dimension = UNSET + elif isinstance(self.dimension, StatementDimensionResponse): + dimension = self.dimension.to_dict() + else: + dimension = self.dimension + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -140,6 +153,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["unmapped_count"] = unmapped_count if truncated is not UNSET: field_dict["truncated"] = truncated + if dimension is not UNSET: + field_dict["dimension"] = dimension return field_dict @@ -147,6 +162,7 @@ def to_dict(self) -> dict[str, Any]: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.live_statement_fact_row import LiveStatementFactRow from ..models.period_spec import PeriodSpec + from ..models.statement_dimension_response import StatementDimensionResponse from ..models.validation_check_response import ValidationCheckResponse d = dict(src_dict) @@ -215,6 +231,23 @@ def _parse_validation(data: object) -> None | Unset | ValidationCheckResponse: truncated = d.pop("truncated", UNSET) + def _parse_dimension(data: object) -> None | StatementDimensionResponse | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + dimension_type_0 = StatementDimensionResponse.from_dict(data) + + return dimension_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | StatementDimensionResponse | Unset, data) + + dimension = _parse_dimension(d.pop("dimension", UNSET)) + live_financial_statement_response = cls( graph_id=graph_id, statement_type=statement_type, @@ -229,6 +262,7 @@ def _parse_validation(data: object) -> None | Unset | ValidationCheckResponse: validation=validation, unmapped_count=unmapped_count, truncated=truncated, + dimension=dimension, ) live_financial_statement_response.additional_properties = d diff --git a/robosystems_client/models/operation_envelope_dimension_response.py b/robosystems_client/models/operation_envelope_dimension_response.py new file mode 100644 index 00000000..a9a4cbd5 --- /dev/null +++ b/robosystems_client/models/operation_envelope_dimension_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_dimension_response_status import ( + OperationEnvelopeDimensionResponseStatus, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.dimension_response import DimensionResponse + + +T = TypeVar("T", bound="OperationEnvelopeDimensionResponse") + + +@_attrs_define +class OperationEnvelopeDimensionResponse: + """ + Attributes: + operation (str): Kebab-case operation name + operation_id (str): op_-prefixed ULID for audit and SSE correlation + status (OperationEnvelopeDimensionResponseStatus): Operation lifecycle state + at (str): ISO-8601 UTC timestamp + result (DimensionResponse | 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: OperationEnvelopeDimensionResponseStatus + at: str + result: DimensionResponse | 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.dimension_response import DimensionResponse + + 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, DimensionResponse): + 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.dimension_response import DimensionResponse + + d = dict(src_dict) + operation = d.pop("operation") + + operation_id = d.pop("operationId") + + status = OperationEnvelopeDimensionResponseStatus(d.pop("status")) + + at = d.pop("at") + + def _parse_result(data: object) -> DimensionResponse | 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 = DimensionResponse.from_dict(data) + + return result_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(DimensionResponse | 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_dimension_response = cls( + operation=operation, + operation_id=operation_id, + status=status, + at=at, + result=result, + created_by=created_by, + idempotent_replay=idempotent_replay, + ) + + operation_envelope_dimension_response.additional_properties = d + return operation_envelope_dimension_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_dimension_response_status.py b/robosystems_client/models/operation_envelope_dimension_response_status.py new file mode 100644 index 00000000..05740640 --- /dev/null +++ b/robosystems_client/models/operation_envelope_dimension_response_status.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class OperationEnvelopeDimensionResponseStatus(str, Enum): + COMPLETED = "completed" + FAILED = "failed" + PENDING = "pending" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/statement_dimension.py b/robosystems_client/models/statement_dimension.py new file mode 100644 index 00000000..1473d541 --- /dev/null +++ b/robosystems_client/models/statement_dimension.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="StatementDimension") + + +@_attrs_define +class StatementDimension: + """A management slice of a statement or trial balance. + + Attributes: + type_ (str): The axis to slice by: `department`, `class`, and so on. + member_id (None | str | Unset): One member's lines only, by id, or `unallocated` for the lines no member of the + axis tags. Omitted, a statement returns one column per member, then Unallocated, then Total, for the current + window. + """ + + type_: str + member_id: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + member_id: None | str | Unset + if isinstance(self.member_id, Unset): + member_id = UNSET + else: + member_id = self.member_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + } + ) + if member_id is not UNSET: + field_dict["member_id"] = member_id + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + type_ = d.pop("type") + + def _parse_member_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + member_id = _parse_member_id(d.pop("member_id", UNSET)) + + statement_dimension = cls( + type_=type_, + member_id=member_id, + ) + + statement_dimension.additional_properties = d + return statement_dimension + + @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/statement_dimension_column.py b/robosystems_client/models/statement_dimension_column.py new file mode 100644 index 00000000..08a57504 --- /dev/null +++ b/robosystems_client/models/statement_dimension_column.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="StatementDimensionColumn") + + +@_attrs_define +class StatementDimensionColumn: + """One column of a statement read by dimension. + + Attributes: + name (str): The column's label. + member_id (None | str | Unset): The member the column reads, `unallocated` for the untagged lines, null for the + Total. + """ + + name: str + member_id: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + member_id: None | str | Unset + if isinstance(self.member_id, Unset): + member_id = UNSET + else: + member_id = self.member_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "name": name, + } + ) + if member_id is not UNSET: + field_dict["member_id"] = member_id + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + name = d.pop("name") + + def _parse_member_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + member_id = _parse_member_id(d.pop("member_id", UNSET)) + + statement_dimension_column = cls( + name=name, + member_id=member_id, + ) + + statement_dimension_column.additional_properties = d + return statement_dimension_column + + @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/statement_dimension_response.py b/robosystems_client/models/statement_dimension_response.py new file mode 100644 index 00000000..c8eda655 --- /dev/null +++ b/robosystems_client/models/statement_dimension_response.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.statement_dimension_column import StatementDimensionColumn + + +T = TypeVar("T", bound="StatementDimensionResponse") + + +@_attrs_define +class StatementDimensionResponse: + """How a statement was sliced, and whether the slice ties out. + + Attributes: + type_ (str): The axis the statement was sliced by. + member_id (None | str | Unset): The one member read, when the statement was filtered to one; null when it was + read by column. + columns (list[StatementDimensionColumn] | Unset): Read by column: one per member, then Unallocated, then Total, + aligned with each row's `values` and with `periods`. Empty when filtered to one member. + ties_out (bool | None | Unset): Read by column: whether every row's members plus Unallocated equal its Total. + Null when filtered to one member. + differences (list[str] | Unset): The rows whose members plus Unallocated miss the Total, by qname. + """ + + type_: str + member_id: None | str | Unset = UNSET + columns: list[StatementDimensionColumn] | Unset = UNSET + ties_out: bool | None | Unset = UNSET + differences: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + member_id: None | str | Unset + if isinstance(self.member_id, Unset): + member_id = UNSET + else: + member_id = self.member_id + + columns: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.columns, Unset): + columns = [] + for columns_item_data in self.columns: + columns_item = columns_item_data.to_dict() + columns.append(columns_item) + + ties_out: bool | None | Unset + if isinstance(self.ties_out, Unset): + ties_out = UNSET + else: + ties_out = self.ties_out + + differences: list[str] | Unset = UNSET + if not isinstance(self.differences, Unset): + differences = self.differences + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + } + ) + if member_id is not UNSET: + field_dict["member_id"] = member_id + if columns is not UNSET: + field_dict["columns"] = columns + if ties_out is not UNSET: + field_dict["ties_out"] = ties_out + if differences is not UNSET: + field_dict["differences"] = differences + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.statement_dimension_column import StatementDimensionColumn + + d = dict(src_dict) + type_ = d.pop("type") + + def _parse_member_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + member_id = _parse_member_id(d.pop("member_id", UNSET)) + + _columns = d.pop("columns", UNSET) + columns: list[StatementDimensionColumn] | Unset = UNSET + if _columns is not UNSET: + columns = [] + for columns_item_data in _columns: + columns_item = StatementDimensionColumn.from_dict(columns_item_data) + + columns.append(columns_item) + + def _parse_ties_out(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + ties_out = _parse_ties_out(d.pop("ties_out", UNSET)) + + differences = cast(list[str], d.pop("differences", UNSET)) + + statement_dimension_response = cls( + type_=type_, + member_id=member_id, + columns=columns, + ties_out=ties_out, + differences=differences, + ) + + statement_dimension_response.additional_properties = d + return statement_dimension_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/update_dimension_request.py b/robosystems_client/models/update_dimension_request.py new file mode 100644 index 00000000..aec0c009 --- /dev/null +++ b/robosystems_client/models/update_dimension_request.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UpdateDimensionRequest") + + +@_attrs_define +class UpdateDimensionRequest: + """Rename or retire a member. Members are never deleted: lines reference + them. + + Attributes: + dimension_id (str): The member to change. + name (None | str | Unset): The new label. Refused on a synced member, whose name follows its source. + is_active (bool | None | Unset): False retires the member: lines that carry it keep it, and it can no longer be + put on new ones. True brings it back. + """ + + dimension_id: str + name: None | str | Unset = UNSET + is_active: bool | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + dimension_id = self.dimension_id + + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + is_active: bool | None | Unset + if isinstance(self.is_active, Unset): + is_active = UNSET + else: + is_active = self.is_active + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "dimension_id": dimension_id, + } + ) + if name is not UNSET: + field_dict["name"] = name + if is_active is not UNSET: + field_dict["is_active"] = is_active + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + dimension_id = d.pop("dimension_id") + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_is_active(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + is_active = _parse_is_active(d.pop("is_active", UNSET)) + + update_dimension_request = cls( + dimension_id=dimension_id, + name=name, + is_active=is_active, + ) + + update_dimension_request.additional_properties = d + return update_dimension_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/robosystems_client/models/update_journal_entry_request.py b/robosystems_client/models/update_journal_entry_request.py index 5f94b02e..7533b86c 100644 --- a/robosystems_client/models/update_journal_entry_request.py +++ b/robosystems_client/models/update_journal_entry_request.py @@ -13,6 +13,7 @@ from ..types import UNSET, Unset if TYPE_CHECKING: + from ..models.journal_entry_line_dimensions import JournalEntryLineDimensions from ..models.journal_entry_line_flow import JournalEntryLineFlow from ..models.journal_entry_line_item_input import JournalEntryLineItemInput @@ -24,9 +25,9 @@ class UpdateJournalEntryRequest: """Update a draft journal entry, or re-tag any entry's flows. - Posted entries accept only `line_flows`: a flow moves no balance, so it - changes in place, closed month or not. Anything else on a posted entry is - a reversal and a corrected entry. + Posted entries accept only `line_flows` and `line_dimensions`: neither + moves a balance, so each changes in place, closed month or not. Anything + else on a posted entry is a reversal and a corrected entry. Omitted fields are left unchanged. If `line_items` is provided, the existing line items are replaced entirely (atomic delete + insert) @@ -48,6 +49,10 @@ class UpdateJournalEntryRequest: its tag. Allowed on a posted entry, closed month included, and the only change one accepts; recorded on the entry as `flow_retags`, with what each changed line carried before. Not combinable with `line_items`, which replaces the lines (set `flow_qname` on them). + line_dimensions (list[JournalEntryLineDimensions] | None | Unset): Re-tag lines' management dimensions in place: + each named line carries exactly the members given (empty clears them). A dimension moves no balance, so this is + allowed on a posted entry, closed month included; recorded on the entry as `dimension_retags`, with what each + line carried before. Not combinable with `line_items` (set `dimension_ids` on the new lines). """ entry_id: str @@ -56,6 +61,7 @@ class UpdateJournalEntryRequest: type_: None | Unset | UpdateJournalEntryRequestTypeType0 = UNSET line_items: list[JournalEntryLineItemInput] | None | Unset = UNSET line_flows: list[JournalEntryLineFlow] | None | Unset = UNSET + line_dimensions: list[JournalEntryLineDimensions] | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -107,6 +113,18 @@ def to_dict(self) -> dict[str, Any]: else: line_flows = self.line_flows + line_dimensions: list[dict[str, Any]] | None | Unset + if isinstance(self.line_dimensions, Unset): + line_dimensions = UNSET + elif isinstance(self.line_dimensions, list): + line_dimensions = [] + for line_dimensions_type_0_item_data in self.line_dimensions: + line_dimensions_type_0_item = line_dimensions_type_0_item_data.to_dict() + line_dimensions.append(line_dimensions_type_0_item) + + else: + line_dimensions = self.line_dimensions + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -124,11 +142,14 @@ def to_dict(self) -> dict[str, Any]: field_dict["line_items"] = line_items if line_flows is not UNSET: field_dict["line_flows"] = line_flows + if line_dimensions is not UNSET: + field_dict["line_dimensions"] = line_dimensions return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.journal_entry_line_dimensions import JournalEntryLineDimensions from ..models.journal_entry_line_flow import JournalEntryLineFlow from ..models.journal_entry_line_item_input import JournalEntryLineItemInput @@ -228,6 +249,32 @@ def _parse_line_flows(data: object) -> list[JournalEntryLineFlow] | None | Unset line_flows = _parse_line_flows(d.pop("line_flows", UNSET)) + def _parse_line_dimensions( + data: object, + ) -> list[JournalEntryLineDimensions] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + line_dimensions_type_0 = [] + _line_dimensions_type_0 = data + for line_dimensions_type_0_item_data in _line_dimensions_type_0: + line_dimensions_type_0_item = JournalEntryLineDimensions.from_dict( + line_dimensions_type_0_item_data + ) + + line_dimensions_type_0.append(line_dimensions_type_0_item) + + return line_dimensions_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[JournalEntryLineDimensions] | None | Unset, data) + + line_dimensions = _parse_line_dimensions(d.pop("line_dimensions", UNSET)) + update_journal_entry_request = cls( entry_id=entry_id, posting_date=posting_date, @@ -235,6 +282,7 @@ def _parse_line_flows(data: object) -> list[JournalEntryLineFlow] | None | Unset type_=type_, line_items=line_items, line_flows=line_flows, + line_dimensions=line_dimensions, ) update_journal_entry_request.additional_properties = d diff --git a/tests/test_ledger_client.py b/tests/test_ledger_client.py index 7895d94a..8a822cd9 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -243,6 +243,7 @@ def test_get_trial_balance(self, mock_execute, mock_config, graph_id): "trialBalance": { "totalDebits": 1000, "totalCredits": 1000, + "sliced": False, "rows": [], } } @@ -1255,6 +1256,7 @@ def test_list_journal_entries_returns_standalone_entries( "description": None, "lineOrder": 1, "flowQname": None, + "dimensions": [], } ], } @@ -2346,6 +2348,89 @@ def test_update_event_block_metadata_patch(self, mock_op, mock_config, graph_id) assert body.metadata_patch.to_dict() == {"reason": "duplicate"} +# ── Management dimensions ───────────────────────────────────────────────── + + +@pytest.mark.unit +class TestDimensions: + @patch("robosystems_client.graphql.client.GraphQLClient.execute") + def test_trial_balance_slices_by_one_member( + self, mock_execute, mock_config, graph_id + ): + mock_execute.return_value = { + "trialBalance": { + "totalDebits": 0, + "totalCredits": 500, + "sliced": True, + "rows": [], + } + } + client = LedgerClient(mock_config) + tb = client.get_trial_balance( + graph_id, dimension_type="department", dimension_member_id="dim_sales" + ) + variables = mock_execute.call_args[0][2] + assert variables["dimensionType"] == "department" + assert variables["dimensionMemberId"] == "dim_sales" + assert tb is not None and tb.sliced is True + + @patch("robosystems_client.graphql.client.GraphQLClient.execute") + def test_get_dimensions_lists_members_and_axes( + self, mock_execute, mock_config, graph_id + ): + mock_execute.return_value = { + "dimensions": { + "types": ["department"], + "dimensions": [ + { + "id": "dim_sales", + "dimensionType": "department", + "name": "Sales", + "value": "sales", + "source": "native", + "isActive": True, + "lineCount": 3, + "createdAt": None, + } + ], + } + } + client = LedgerClient(mock_config) + found = client.get_dimensions(graph_id, dimension_type="department") + assert mock_execute.call_args[0][2] == { + "dimensionType": "department", + "includeInactive": False, + } + assert found is not None + assert found.types == ["department"] + assert found.dimensions[0].line_count == 3 + + @patch("robosystems_client.clients.ledger_client.op_create_dimension") + def test_create_dimension(self, mock_op, mock_config, graph_id): + mock_op.return_value = _mock_response( + _envelope("create-dimension", {"id": "dim_1", "name": "Sales"}) + ) + client = LedgerClient(mock_config) + result = client.create_dimension( + graph_id, {"dimension_type": "department", "name": "Sales"} + ) + assert result["id"] == "dim_1" + body = mock_op.call_args.kwargs["body"] + assert (body.dimension_type, body.name) == ("department", "Sales") + + @patch("robosystems_client.clients.ledger_client.op_update_dimension") + def test_update_dimension_retires(self, mock_op, mock_config, graph_id): + mock_op.return_value = _mock_response( + _envelope("update-dimension", {"id": "dim_1", "is_active": False}) + ) + client = LedgerClient(mock_config) + result = client.update_dimension( + graph_id, {"dimension_id": "dim_1", "is_active": False} + ) + assert result["is_active"] is False + assert mock_op.call_args.kwargs["body"].is_active is False + + # ── Agents ────────────────────────────────────────────────────────────────