From fd3016f6360bc97b1e77d55acfa7501ec285e008 Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Fri, 11 Sep 2026 23:27:12 -0500 Subject: [PATCH] =?UTF-8?q?feat(ledger):=20native=20chart=20surface=20?= =?UTF-8?q?=E2=80=94=20initialize=20from=20a=20template,=20sever=20a=20con?= =?UTF-8?q?nection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regenerated against robosystems main after #1378 and #1379: the initialize-chart-of-accounts operation with its request, response and template enum; disposition=disconnect|sever on deleteConnection; the chartTemplates GraphQL field with a ListChartTemplates query document and its typed model. Swept in from the same main: SearchRequest.snippet_chars (#1376) and the event_category doc text on EventBlockEnvelope. Facade: LedgerClient.list_chart_templates(graph_id) and LedgerClient.initialize_chart_of_accounts(graph_id, template, entity_type, name) beside initialize_ledger, with tests. Claude-Session: https://claude.ai/code/session_0188CbjDiNBtxdEYjSJ5Y7mX --- .../api/connections/delete_connection.py | 65 +++- .../initialize_chart_of_accounts.py | 322 ++++++++++++++++++ robosystems_client/clients/ledger_client.py | 56 +++ .../graphql/generated/__init__.py | 5 + .../graphql/generated/client.py | 13 + .../graphql/generated/list_chart_templates.py | 19 ++ .../graphql/generated/operations.py | 12 + .../ledger/ListChartTemplates.graphql | 5 + robosystems_client/graphql/schema.graphql | 11 + robosystems_client/models/__init__.py | 22 ++ .../models/delete_connection_disposition.py | 9 + .../models/event_block_envelope.py | 6 +- .../initialize_chart_of_accounts_request.py | 116 +++++++ ...lize_chart_of_accounts_request_template.py | 10 + .../initialize_chart_of_accounts_response.py | 132 +++++++ ...ize_chart_of_accounts_response_template.py | 10 + ...e_initialize_chart_of_accounts_response.py | 164 +++++++++ ...alize_chart_of_accounts_response_status.py | 10 + robosystems_client/models/search_request.py | 21 ++ tests/test_ledger_client.py | 65 ++++ 20 files changed, 1066 insertions(+), 7 deletions(-) create mode 100644 robosystems_client/api/extensions_robo_ledger/initialize_chart_of_accounts.py create mode 100644 robosystems_client/graphql/generated/list_chart_templates.py create mode 100644 robosystems_client/graphql/operations/ledger/ListChartTemplates.graphql create mode 100644 robosystems_client/models/delete_connection_disposition.py create mode 100644 robosystems_client/models/initialize_chart_of_accounts_request.py create mode 100644 robosystems_client/models/initialize_chart_of_accounts_request_template.py create mode 100644 robosystems_client/models/initialize_chart_of_accounts_response.py create mode 100644 robosystems_client/models/initialize_chart_of_accounts_response_template.py create mode 100644 robosystems_client/models/operation_envelope_initialize_chart_of_accounts_response.py create mode 100644 robosystems_client/models/operation_envelope_initialize_chart_of_accounts_response_status.py diff --git a/robosystems_client/api/connections/delete_connection.py b/robosystems_client/api/connections/delete_connection.py index 8c72a01..be35f26 100644 --- a/robosystems_client/api/connections/delete_connection.py +++ b/robosystems_client/api/connections/delete_connection.py @@ -6,23 +6,38 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.delete_connection_disposition import DeleteConnectionDisposition from ...models.error_response import ErrorResponse from ...models.http_validation_error import HTTPValidationError from ...models.success_response import SuccessResponse -from ...types import Response +from ...types import UNSET, Response, Unset def _get_kwargs( graph_id: str, connection_id: str, + *, + disposition: DeleteConnectionDisposition + | Unset = DeleteConnectionDisposition.DISCONNECT, ) -> dict[str, Any]: + params: dict[str, Any] = {} + + json_disposition: str | Unset = UNSET + if not isinstance(disposition, Unset): + json_disposition = disposition.value + + params["disposition"] = json_disposition + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + _kwargs: dict[str, Any] = { "method": "delete", "url": "/v1/graphs/{graph_id}/connections/{connection_id}".format( graph_id=quote(str(graph_id), safe=""), connection_id=quote(str(connection_id), safe=""), ), + "params": params, } return _kwargs @@ -93,15 +108,24 @@ def sync_detailed( connection_id: str, *, client: AuthenticatedClient, + disposition: DeleteConnectionDisposition + | Unset = DeleteConnectionDisposition.DISCONNECT, ) -> Response[ErrorResponse | HTTPValidationError | SuccessResponse]: """Delete Connection Removes the connection and revokes credentials. Imported data is preserved in the graph. Requires - admin role. + admin role. `disposition=sever` (QuickBooks only) is the cutover to native books: the chart + QuickBooks created becomes the tenant's own and QuickBooks can never resume over it; the default + `disconnect` keeps the connection reconnectable. Args: graph_id (str): connection_id (str): Connection identifier + disposition (DeleteConnectionDisposition | Unset): `disconnect` (default): soft-delete; a + later re-OAuth to the same realm revives the connection. `sever`: the native-accounting + cutover — QuickBooks only; the chart it created is stamped native-owned, write_policy + drops to native, and the connection is never revived. Default: + DeleteConnectionDisposition.DISCONNECT. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -114,6 +138,7 @@ def sync_detailed( kwargs = _get_kwargs( graph_id=graph_id, connection_id=connection_id, + disposition=disposition, ) response = client.get_httpx_client().request( @@ -128,15 +153,24 @@ def sync( connection_id: str, *, client: AuthenticatedClient, + disposition: DeleteConnectionDisposition + | Unset = DeleteConnectionDisposition.DISCONNECT, ) -> ErrorResponse | HTTPValidationError | SuccessResponse | None: """Delete Connection Removes the connection and revokes credentials. Imported data is preserved in the graph. Requires - admin role. + admin role. `disposition=sever` (QuickBooks only) is the cutover to native books: the chart + QuickBooks created becomes the tenant's own and QuickBooks can never resume over it; the default + `disconnect` keeps the connection reconnectable. Args: graph_id (str): connection_id (str): Connection identifier + disposition (DeleteConnectionDisposition | Unset): `disconnect` (default): soft-delete; a + later re-OAuth to the same realm revives the connection. `sever`: the native-accounting + cutover — QuickBooks only; the chart it created is stamped native-owned, write_policy + drops to native, and the connection is never revived. Default: + DeleteConnectionDisposition.DISCONNECT. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -150,6 +184,7 @@ def sync( graph_id=graph_id, connection_id=connection_id, client=client, + disposition=disposition, ).parsed @@ -158,15 +193,24 @@ async def asyncio_detailed( connection_id: str, *, client: AuthenticatedClient, + disposition: DeleteConnectionDisposition + | Unset = DeleteConnectionDisposition.DISCONNECT, ) -> Response[ErrorResponse | HTTPValidationError | SuccessResponse]: """Delete Connection Removes the connection and revokes credentials. Imported data is preserved in the graph. Requires - admin role. + admin role. `disposition=sever` (QuickBooks only) is the cutover to native books: the chart + QuickBooks created becomes the tenant's own and QuickBooks can never resume over it; the default + `disconnect` keeps the connection reconnectable. Args: graph_id (str): connection_id (str): Connection identifier + disposition (DeleteConnectionDisposition | Unset): `disconnect` (default): soft-delete; a + later re-OAuth to the same realm revives the connection. `sever`: the native-accounting + cutover — QuickBooks only; the chart it created is stamped native-owned, write_policy + drops to native, and the connection is never revived. Default: + DeleteConnectionDisposition.DISCONNECT. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -179,6 +223,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( graph_id=graph_id, connection_id=connection_id, + disposition=disposition, ) response = await client.get_async_httpx_client().request(**kwargs) @@ -191,15 +236,24 @@ async def asyncio( connection_id: str, *, client: AuthenticatedClient, + disposition: DeleteConnectionDisposition + | Unset = DeleteConnectionDisposition.DISCONNECT, ) -> ErrorResponse | HTTPValidationError | SuccessResponse | None: """Delete Connection Removes the connection and revokes credentials. Imported data is preserved in the graph. Requires - admin role. + admin role. `disposition=sever` (QuickBooks only) is the cutover to native books: the chart + QuickBooks created becomes the tenant's own and QuickBooks can never resume over it; the default + `disconnect` keeps the connection reconnectable. Args: graph_id (str): connection_id (str): Connection identifier + disposition (DeleteConnectionDisposition | Unset): `disconnect` (default): soft-delete; a + later re-OAuth to the same realm revives the connection. `sever`: the native-accounting + cutover — QuickBooks only; the chart it created is stamped native-owned, write_policy + drops to native, and the connection is never revived. Default: + DeleteConnectionDisposition.DISCONNECT. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -214,5 +268,6 @@ async def asyncio( graph_id=graph_id, connection_id=connection_id, client=client, + disposition=disposition, ) ).parsed diff --git a/robosystems_client/api/extensions_robo_ledger/initialize_chart_of_accounts.py b/robosystems_client/api/extensions_robo_ledger/initialize_chart_of_accounts.py new file mode 100644 index 0000000..6669a07 --- /dev/null +++ b/robosystems_client/api/extensions_robo_ledger/initialize_chart_of_accounts.py @@ -0,0 +1,322 @@ +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.initialize_chart_of_accounts_request import ( + InitializeChartOfAccountsRequest, +) +from ...models.operation_envelope_initialize_chart_of_accounts_response import ( + OperationEnvelopeInitializeChartOfAccountsResponse, +) +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + graph_id: str, + *, + body: InitializeChartOfAccountsRequest, + 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/initialize-chart-of-accounts".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 | OperationEnvelopeInitializeChartOfAccountsResponse | None: + if response.status_code == 200: + response_200 = OperationEnvelopeInitializeChartOfAccountsResponse.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 | OperationEnvelopeInitializeChartOfAccountsResponse]: + 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: InitializeChartOfAccountsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeInitializeChartOfAccountsResponse]: + """Initialize Chart of Accounts + + Create the graph's chart of accounts from a shipped template — the fresh-company path to native + books. Use when the graph has NO chart (a QuickBooks-synced tenant never needs this: its chart + arrives with the sync and stays after a sever) and before connecting a bank feed, which needs a + chart to resolve against. Templates: `saas` (subscription software), `services` (professional + services), `product` (inventory and COGS) — the `chartTemplates` GraphQL field lists them with names + and account counts. Creates the chart, its `coa_mapping` structure and the template's CoA → rs-gaap + mapping associations in one transaction, with the equity rows mapped by the entity's legal form + (`entity_type`, defaulting to the graph's primary entity). One-time: 409 once a chart exists — a + chart is never replaced. Customize afterwards with update-taxonomy-block; accounts that carry + activity are never deleted. + + **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 (InitializeChartOfAccountsRequest): Create the graph's chart of accounts from a + shipped template. + + Refused (409) when the graph already has an active ``chart_of_accounts`` + taxonomy — a QuickBooks-synced tenant never needs this, and a chart is + never replaced. The template's equity rows are mapped by the entity's + legal form (``entity_type``: corporation / llc / partnership); omit it + to use the graph's primary entity, falling back to corporation. + + 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 | OperationEnvelopeInitializeChartOfAccountsResponse] + """ + + 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: InitializeChartOfAccountsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeInitializeChartOfAccountsResponse | None: + """Initialize Chart of Accounts + + Create the graph's chart of accounts from a shipped template — the fresh-company path to native + books. Use when the graph has NO chart (a QuickBooks-synced tenant never needs this: its chart + arrives with the sync and stays after a sever) and before connecting a bank feed, which needs a + chart to resolve against. Templates: `saas` (subscription software), `services` (professional + services), `product` (inventory and COGS) — the `chartTemplates` GraphQL field lists them with names + and account counts. Creates the chart, its `coa_mapping` structure and the template's CoA → rs-gaap + mapping associations in one transaction, with the equity rows mapped by the entity's legal form + (`entity_type`, defaulting to the graph's primary entity). One-time: 409 once a chart exists — a + chart is never replaced. Customize afterwards with update-taxonomy-block; accounts that carry + activity are never deleted. + + **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 (InitializeChartOfAccountsRequest): Create the graph's chart of accounts from a + shipped template. + + Refused (409) when the graph already has an active ``chart_of_accounts`` + taxonomy — a QuickBooks-synced tenant never needs this, and a chart is + never replaced. The template's equity rows are mapped by the entity's + legal form (``entity_type``: corporation / llc / partnership); omit it + to use the graph's primary entity, falling back to corporation. + + 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 | OperationEnvelopeInitializeChartOfAccountsResponse + """ + + 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: InitializeChartOfAccountsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeInitializeChartOfAccountsResponse]: + """Initialize Chart of Accounts + + Create the graph's chart of accounts from a shipped template — the fresh-company path to native + books. Use when the graph has NO chart (a QuickBooks-synced tenant never needs this: its chart + arrives with the sync and stays after a sever) and before connecting a bank feed, which needs a + chart to resolve against. Templates: `saas` (subscription software), `services` (professional + services), `product` (inventory and COGS) — the `chartTemplates` GraphQL field lists them with names + and account counts. Creates the chart, its `coa_mapping` structure and the template's CoA → rs-gaap + mapping associations in one transaction, with the equity rows mapped by the entity's legal form + (`entity_type`, defaulting to the graph's primary entity). One-time: 409 once a chart exists — a + chart is never replaced. Customize afterwards with update-taxonomy-block; accounts that carry + activity are never deleted. + + **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 (InitializeChartOfAccountsRequest): Create the graph's chart of accounts from a + shipped template. + + Refused (409) when the graph already has an active ``chart_of_accounts`` + taxonomy — a QuickBooks-synced tenant never needs this, and a chart is + never replaced. The template's equity rows are mapped by the entity's + legal form (``entity_type``: corporation / llc / partnership); omit it + to use the graph's primary entity, falling back to corporation. + + 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 | OperationEnvelopeInitializeChartOfAccountsResponse] + """ + + 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: InitializeChartOfAccountsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeInitializeChartOfAccountsResponse | None: + """Initialize Chart of Accounts + + Create the graph's chart of accounts from a shipped template — the fresh-company path to native + books. Use when the graph has NO chart (a QuickBooks-synced tenant never needs this: its chart + arrives with the sync and stays after a sever) and before connecting a bank feed, which needs a + chart to resolve against. Templates: `saas` (subscription software), `services` (professional + services), `product` (inventory and COGS) — the `chartTemplates` GraphQL field lists them with names + and account counts. Creates the chart, its `coa_mapping` structure and the template's CoA → rs-gaap + mapping associations in one transaction, with the equity rows mapped by the entity's legal form + (`entity_type`, defaulting to the graph's primary entity). One-time: 409 once a chart exists — a + chart is never replaced. Customize afterwards with update-taxonomy-block; accounts that carry + activity are never deleted. + + **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 (InitializeChartOfAccountsRequest): Create the graph's chart of accounts from a + shipped template. + + Refused (409) when the graph already has an active ``chart_of_accounts`` + taxonomy — a QuickBooks-synced tenant never needs this, and a chart is + never replaced. The template's equity rows are mapped by the entity's + legal form (``entity_type``: corporation / llc / partnership); omit it + to use the graph's primary entity, falling back to corporation. + + 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 | OperationEnvelopeInitializeChartOfAccountsResponse + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + client=client, + body=body, + idempotency_key=idempotency_key, + ) + ).parsed diff --git a/robosystems_client/clients/ledger_client.py b/robosystems_client/clients/ledger_client.py index fd354a4..8bc9469 100644 --- a/robosystems_client/clients/ledger_client.py +++ b/robosystems_client/clients/ledger_client.py @@ -79,6 +79,9 @@ from ..api.extensions_robo_ledger.delete_mapping_association import ( sync_detailed as op_delete_mapping_association, ) +from ..api.extensions_robo_ledger.initialize_chart_of_accounts import ( + sync_detailed as op_initialize_chart_of_accounts, +) from ..api.extensions_robo_ledger.initialize_ledger import ( sync_detailed as op_initialize_ledger, ) @@ -222,6 +225,12 @@ from ..graphql.generated.get_ledger_fiscal_calendar import ( GetLedgerFiscalCalendarFiscalCalendar as FiscalCalendar, ) +from ..graphql.generated.list_chart_templates import ( + ListChartTemplates, +) +from ..graphql.generated.list_chart_templates import ( + ListChartTemplatesChartTemplates as ChartTemplate, +) from ..graphql.generated.get_ledger_mapped_trial_balance import ( GetLedgerMappedTrialBalance, ) @@ -388,6 +397,7 @@ GET_LEDGER_ENTITY_GQL, GET_LEDGER_EVENT_BLOCK_GQL, GET_LEDGER_FISCAL_CALENDAR_GQL, + LIST_CHART_TEMPLATES_GQL, GET_LEDGER_MAPPED_TRIAL_BALANCE_GQL, GET_LEDGER_MAPPING_COVERAGE_GQL, GET_LEDGER_MAPPING_GQL, @@ -471,6 +481,12 @@ from ..models.delete_mapping_association_operation import ( DeleteMappingAssociationOperation, ) +from ..models.initialize_chart_of_accounts_request import ( + InitializeChartOfAccountsRequest, +) +from ..models.initialize_chart_of_accounts_request_template import ( + InitializeChartOfAccountsRequestTemplate, +) from ..models.initialize_ledger_request import InitializeLedgerRequest from ..models.create_publish_list_request import CreatePublishListRequest from ..models.create_report_request import CreateReportRequest @@ -509,6 +525,9 @@ from ..models.event_handler_response import EventHandlerResponse from ..models.fiscal_calendar_response import FiscalCalendarResponse from ..models.information_block_envelope import InformationBlockEnvelope +from ..models.initialize_chart_of_accounts_response import ( + InitializeChartOfAccountsResponse, +) from ..models.initialize_ledger_response import InitializeLedgerResponse from ..models.journal_entry_response import JournalEntryResponse from ..models.ledger_agent_response import LedgerAgentResponse @@ -2046,6 +2065,43 @@ def initialize_ledger( envelope = self._call_op("Initialize ledger", response) return self._typed_result("Initialize ledger", envelope, InitializeLedgerResponse) + # ── Chart of accounts ─────────────────────────────────────────────────── + + def list_chart_templates(self, graph_id: str) -> list[ChartTemplate]: + """Shipped chart-of-accounts templates for `initialize_chart_of_accounts`.""" + data = self._query(graph_id, LIST_CHART_TEMPLATES_GQL) + return ListChartTemplates.model_validate(data).chart_templates + + def initialize_chart_of_accounts( + self, + graph_id: str, + template: InitializeChartOfAccountsRequestTemplate | str, + *, + entity_type: str | None = None, + name: str | None = None, + ) -> InitializeChartOfAccountsResponse: + """One-time chart of accounts from a shipped template — the fresh-company + path to native books. + + ``template`` is one of `list_chart_templates` (``saas`` / ``services`` / + ``product``). Refused (409) once the graph has any chart of accounts — a + QuickBooks-synced tenant never needs this. ``entity_type`` (corporation / + llc / partnership) picks the equity mapping; it defaults to the graph's + primary entity, then corporation. + """ + body = InitializeChartOfAccountsRequest( + template=InitializeChartOfAccountsRequestTemplate(template), + entity_type=entity_type if entity_type is not None else UNSET, + name=name if name is not None else UNSET, + ) + response = op_initialize_chart_of_accounts( + graph_id=graph_id, body=body, client=self._get_client() + ) + envelope = self._call_op("Initialize chart of accounts", response) + return self._typed_result( + "Initialize chart of accounts", envelope, InitializeChartOfAccountsResponse + ) + def set_close_target( self, graph_id: str, diff --git a/robosystems_client/graphql/generated/__init__.py b/robosystems_client/graphql/generated/__init__.py index 0930fea..629f154 100644 --- a/robosystems_client/graphql/generated/__init__.py +++ b/robosystems_client/graphql/generated/__init__.py @@ -187,6 +187,7 @@ GetLibraryElementEquivalentsLibraryElementEquivalentsEquivalents, ) from .get_library_taxonomy import GetLibraryTaxonomy, GetLibraryTaxonomyLibraryTaxonomy +from .list_chart_templates import ListChartTemplates, ListChartTemplatesChartTemplates from .list_information_blocks import ( ListInformationBlocks, ListInformationBlocksInformationBlocks, @@ -347,6 +348,7 @@ GET_LIBRARY_ELEMENT_EQUIVALENTS_GQL, GET_LIBRARY_ELEMENT_GQL, GET_LIBRARY_TAXONOMY_GQL, + LIST_CHART_TEMPLATES_GQL, LIST_INFORMATION_BLOCKS_GQL, LIST_INVESTOR_PORTFOLIOS_GQL, LIST_INVESTOR_POSITIONS_GQL, @@ -556,6 +558,7 @@ "GraphQLClientGraphQLMultiError", "GraphQLClientHttpError", "GraphQLClientInvalidResponseError", + "LIST_CHART_TEMPLATES_GQL", "LIST_INFORMATION_BLOCKS_GQL", "LIST_INVESTOR_PORTFOLIOS_GQL", "LIST_INVESTOR_POSITIONS_GQL", @@ -578,6 +581,8 @@ "LIST_LIBRARY_STRUCTURES_GQL", "LIST_LIBRARY_TAXONOMIES_GQL", "LIST_LIBRARY_TAXONOMY_ARCS_GQL", + "ListChartTemplates", + "ListChartTemplatesChartTemplates", "ListInformationBlocks", "ListInformationBlocksInformationBlocks", "ListInformationBlocksInformationBlocksArtifact", diff --git a/robosystems_client/graphql/generated/client.py b/robosystems_client/graphql/generated/client.py index 7970dc2..a068d58 100644 --- a/robosystems_client/graphql/generated/client.py +++ b/robosystems_client/graphql/generated/client.py @@ -34,6 +34,7 @@ from .get_library_element_classifications import GetLibraryElementClassifications from .get_library_element_equivalents import GetLibraryElementEquivalents from .get_library_taxonomy import GetLibraryTaxonomy +from .list_chart_templates import ListChartTemplates from .list_information_blocks import ListInformationBlocks from .list_investor_portfolios import ListInvestorPortfolios from .list_investor_positions import ListInvestorPositions @@ -89,6 +90,7 @@ GET_LIBRARY_ELEMENT_EQUIVALENTS_GQL, GET_LIBRARY_ELEMENT_GQL, GET_LIBRARY_TAXONOMY_GQL, + LIST_CHART_TEMPLATES_GQL, LIST_INFORMATION_BLOCKS_GQL, LIST_INVESTOR_PORTFOLIOS_GQL, LIST_INVESTOR_POSITIONS_GQL, @@ -547,6 +549,17 @@ def get_ledger_trial_balance( data = self.get_data(response) return GetLedgerTrialBalance.model_validate(data) + def list_chart_templates(self, **kwargs: Any) -> ListChartTemplates: + variables: dict[str, object] = {} + response = self.execute( + query=LIST_CHART_TEMPLATES_GQL, + operation_name="ListChartTemplates", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return ListChartTemplates.model_validate(data) + def list_information_blocks( self, block_type: Union[Optional[str], UnsetType] = UNSET, diff --git a/robosystems_client/graphql/generated/list_chart_templates.py b/robosystems_client/graphql/generated/list_chart_templates.py new file mode 100644 index 0000000..bccf8d5 --- /dev/null +++ b/robosystems_client/graphql/generated/list_chart_templates.py @@ -0,0 +1,19 @@ +from pydantic import Field + +from .base_model import BaseModel + + +class ListChartTemplates(BaseModel): + chart_templates: list["ListChartTemplatesChartTemplates"] = Field( + alias="chartTemplates" + ) + + +class ListChartTemplatesChartTemplates(BaseModel): + key: str + display_name: str = Field(alias="displayName") + description: str + account_count: int = Field(alias="accountCount") + + +ListChartTemplates.model_rebuild() diff --git a/robosystems_client/graphql/generated/operations.py b/robosystems_client/graphql/generated/operations.py index 378d0cf..1341cad 100644 --- a/robosystems_client/graphql/generated/operations.py +++ b/robosystems_client/graphql/generated/operations.py @@ -30,6 +30,7 @@ "GET_LIBRARY_ELEMENT_EQUIVALENTS_GQL", "GET_LIBRARY_ELEMENT_GQL", "GET_LIBRARY_TAXONOMY_GQL", + "LIST_CHART_TEMPLATES_GQL", "LIST_INFORMATION_BLOCKS_GQL", "LIST_INVESTOR_PORTFOLIOS_GQL", "LIST_INVESTOR_POSITIONS_GQL", @@ -1159,6 +1160,17 @@ } """ +LIST_CHART_TEMPLATES_GQL = """ +query ListChartTemplates { + chartTemplates { + key + displayName + description + accountCount + } +} +""" + LIST_INFORMATION_BLOCKS_GQL = """ query ListInformationBlocks($blockType: String, $category: String, $limit: Int, $offset: Int) { informationBlocks( diff --git a/robosystems_client/graphql/operations/ledger/ListChartTemplates.graphql b/robosystems_client/graphql/operations/ledger/ListChartTemplates.graphql new file mode 100644 index 0000000..15e22e6 --- /dev/null +++ b/robosystems_client/graphql/operations/ledger/ListChartTemplates.graphql @@ -0,0 +1,5 @@ +query ListChartTemplates { + chartTemplates { + key displayName description accountCount + } +} diff --git a/robosystems_client/graphql/schema.graphql b/robosystems_client/graphql/schema.graphql index 553669d..5cbc4e3 100644 --- a/robosystems_client/graphql/schema.graphql +++ b/robosystems_client/graphql/schema.graphql @@ -35,6 +35,7 @@ type Query { mappingCoverage(mappingId: String!): MappingCoverage mappedTrialBalance(mappingId: String!, startDate: Date = null, endDate: Date = null): MappedTrialBalance periodCloseStatus(periodStart: Date!, periodEnd: Date!): PeriodCloseStatus + chartTemplates: [ChartTemplate!]! fiscalCalendar: FiscalCalendar periodDrafts(period: String!): PeriodDrafts closingBookStructures: ClosingBookStructures @@ -1222,6 +1223,16 @@ type CloseReceipt { statementRuleSummary: JSON } +""" +A shipped chart-of-accounts template for `initialize-chart-of-accounts` — the fresh-company path to native books. +""" +type ChartTemplate { + key: String! + displayName: String! + description: String! + accountCount: Int! +} + """Current fiscal calendar state for a graph.""" type FiscalCalendar { graphId: String! diff --git a/robosystems_client/models/__init__.py b/robosystems_client/models/__init__.py index 88f9c24..e9b74d3 100644 --- a/robosystems_client/models/__init__.py +++ b/robosystems_client/models/__init__.py @@ -160,6 +160,7 @@ from .database_health_response import DatabaseHealthResponse from .database_info_response import DatabaseInfoResponse from .database_storage_entry import DatabaseStorageEntry +from .delete_connection_disposition import DeleteConnectionDisposition from .delete_document_op import DeleteDocumentOp from .delete_file_op import DeleteFileOp from .delete_forecast_arm import DeleteForecastArm @@ -296,6 +297,14 @@ from .information_model_response import InformationModelResponse from .ingest_file_op import IngestFileOp from .initial_entity_data import InitialEntityData +from .initialize_chart_of_accounts_request import InitializeChartOfAccountsRequest +from .initialize_chart_of_accounts_request_template import ( + InitializeChartOfAccountsRequestTemplate, +) +from .initialize_chart_of_accounts_response import InitializeChartOfAccountsResponse +from .initialize_chart_of_accounts_response_template import ( + InitializeChartOfAccountsResponseTemplate, +) from .initialize_ledger_request import InitializeLedgerRequest from .initialize_ledger_response import InitializeLedgerResponse from .instance_usage import InstanceUsage @@ -503,6 +512,12 @@ from .operation_envelope_information_block_envelope_status import ( OperationEnvelopeInformationBlockEnvelopeStatus, ) +from .operation_envelope_initialize_chart_of_accounts_response import ( + OperationEnvelopeInitializeChartOfAccountsResponse, +) +from .operation_envelope_initialize_chart_of_accounts_response_status import ( + OperationEnvelopeInitializeChartOfAccountsResponseStatus, +) from .operation_envelope_initialize_ledger_response import ( OperationEnvelopeInitializeLedgerResponse, ) @@ -1083,6 +1098,7 @@ "DatabaseHealthResponse", "DatabaseInfoResponse", "DatabaseStorageEntry", + "DeleteConnectionDisposition", "DeleteDocumentOp", "DeleteFileOp", "DeleteForecastArm", @@ -1199,6 +1215,10 @@ "InformationModelResponse", "IngestFileOp", "InitialEntityData", + "InitializeChartOfAccountsRequest", + "InitializeChartOfAccountsRequestTemplate", + "InitializeChartOfAccountsResponse", + "InitializeChartOfAccountsResponseTemplate", "InitializeLedgerRequest", "InitializeLedgerResponse", "InstanceUsage", @@ -1308,6 +1328,8 @@ "OperationEnvelopeGraphMetadataResultStatus", "OperationEnvelopeInformationBlockEnvelope", "OperationEnvelopeInformationBlockEnvelopeStatus", + "OperationEnvelopeInitializeChartOfAccountsResponse", + "OperationEnvelopeInitializeChartOfAccountsResponseStatus", "OperationEnvelopeInitializeLedgerResponse", "OperationEnvelopeInitializeLedgerResponseStatus", "OperationEnvelopeJournalEntryResponse", diff --git a/robosystems_client/models/delete_connection_disposition.py b/robosystems_client/models/delete_connection_disposition.py new file mode 100644 index 0000000..95e0e9d --- /dev/null +++ b/robosystems_client/models/delete_connection_disposition.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class DeleteConnectionDisposition(str, Enum): + DISCONNECT = "disconnect" + SEVER = "sever" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/event_block_envelope.py b/robosystems_client/models/event_block_envelope.py index 2d98967..91986c6 100644 --- a/robosystems_client/models/event_block_envelope.py +++ b/robosystems_client/models/event_block_envelope.py @@ -31,8 +31,10 @@ class EventBlockEnvelope: Attributes: id (str): Event ID (`evt_*` ULID). event_type (str): Open-vocabulary event type (e.g. `invoice_issued`, `bank_transaction`, `control_executed`). - event_category (str): REA category — economic (`sales`, `purchase`, `financing`, `payroll`, `treasury`, - `adjustment`, `recognition`, `other`) or support (`control`, `approval`, `reconciliation`, `inquiry`). + event_category (str): REA category, scoped by `event_class` — economic (`sales`, `purchase`, `financing`, + `payroll`, `treasury`, `adjustment`, `recognition`, `other`), support (`control`, `approval`, `reconciliation`, + `inquiry`), or operational (`pipeline`, `engagement`, `schedule`, `other`) for occurrences that drive no GL — a + lead, a lifecycle change, an outreach, a schedule setup. status (str): Lifecycle state. One of: `captured` (raw, pre-classification), `classified` (handler ran, GL pending), `committed` (GL entries posted), `pending` (committed but awaiting fulfillment of an obligation), `fulfilled` (obligation discharged — retractable while its ledger rows are still drafts), `voided` (canceled — diff --git a/robosystems_client/models/initialize_chart_of_accounts_request.py b/robosystems_client/models/initialize_chart_of_accounts_request.py new file mode 100644 index 0000000..02c215b --- /dev/null +++ b/robosystems_client/models/initialize_chart_of_accounts_request.py @@ -0,0 +1,116 @@ +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 ..models.initialize_chart_of_accounts_request_template import ( + InitializeChartOfAccountsRequestTemplate, +) +from ..types import UNSET, Unset + +T = TypeVar("T", bound="InitializeChartOfAccountsRequest") + + +@_attrs_define +class InitializeChartOfAccountsRequest: + """Create the graph's chart of accounts from a shipped template. + + Refused (409) when the graph already has an active ``chart_of_accounts`` + taxonomy — a QuickBooks-synced tenant never needs this, and a chart is + never replaced. The template's equity rows are mapped by the entity's + legal form (``entity_type``: corporation / llc / partnership); omit it + to use the graph's primary entity, falling back to corporation. + + Attributes: + template (InitializeChartOfAccountsRequestTemplate): Template key: `saas` (subscription software — deferred + revenue, cost of revenue, R&D / S&M / G&A), `services` (professional services — no inventory, no COGS), + `product` (inventory and cost of goods sold, direct + wholesale + subscription revenue). + entity_type (None | str | Unset): Legal form for the equity mapping: `corporation`, `llc` or `partnership`. + Defaults to the graph's primary entity, then to corporation. + name (None | str | Unset): Chart display name. Defaults to 'Chart of Accounts'. + """ + + template: InitializeChartOfAccountsRequestTemplate + entity_type: None | str | Unset = UNSET + name: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + template = self.template.value + + entity_type: None | str | Unset + if isinstance(self.entity_type, Unset): + entity_type = UNSET + else: + entity_type = self.entity_type + + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "template": template, + } + ) + if entity_type is not UNSET: + field_dict["entity_type"] = entity_type + if name is not UNSET: + field_dict["name"] = name + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + template = InitializeChartOfAccountsRequestTemplate(d.pop("template")) + + def _parse_entity_type(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + entity_type = _parse_entity_type(d.pop("entity_type", UNSET)) + + 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)) + + initialize_chart_of_accounts_request = cls( + template=template, + entity_type=entity_type, + name=name, + ) + + initialize_chart_of_accounts_request.additional_properties = d + return initialize_chart_of_accounts_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/initialize_chart_of_accounts_request_template.py b/robosystems_client/models/initialize_chart_of_accounts_request_template.py new file mode 100644 index 0000000..b85f0c4 --- /dev/null +++ b/robosystems_client/models/initialize_chart_of_accounts_request_template.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class InitializeChartOfAccountsRequestTemplate(str, Enum): + PRODUCT = "product" + SAAS = "saas" + SERVICES = "services" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/initialize_chart_of_accounts_response.py b/robosystems_client/models/initialize_chart_of_accounts_response.py new file mode 100644 index 0000000..f07700f --- /dev/null +++ b/robosystems_client/models/initialize_chart_of_accounts_response.py @@ -0,0 +1,132 @@ +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 ..models.initialize_chart_of_accounts_response_template import ( + InitializeChartOfAccountsResponseTemplate, +) +from ..types import UNSET, Unset + +T = TypeVar("T", bound="InitializeChartOfAccountsResponse") + + +@_attrs_define +class InitializeChartOfAccountsResponse: + """ + Attributes: + taxonomy_id (str): The new chart's taxonomy id. + name (str): + template (InitializeChartOfAccountsResponseTemplate): + entity_type (str): Legal form the equity rows were mapped for. + elements_created (int): + mappings_created (int): + frameworks (list[str] | Unset): Frameworks the chart was mapped into — each template mapping set whose framework + this graph's library carries (rs-gaap today; every framework in the graph's pin once it is plural). + unresolved (list[str] | Unset): What could not be mapped, never fatal — the accounts exist and can be mapped by + hand: a target qname the framework's library copy did not resolve, `: not in this graph's library` + for a template mapping set whose framework this graph does not carry, or a template row naming an account it + does not declare. + """ + + taxonomy_id: str + name: str + template: InitializeChartOfAccountsResponseTemplate + entity_type: str + elements_created: int + mappings_created: int + frameworks: list[str] | Unset = UNSET + unresolved: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + taxonomy_id = self.taxonomy_id + + name = self.name + + template = self.template.value + + entity_type = self.entity_type + + elements_created = self.elements_created + + mappings_created = self.mappings_created + + frameworks: list[str] | Unset = UNSET + if not isinstance(self.frameworks, Unset): + frameworks = self.frameworks + + unresolved: list[str] | Unset = UNSET + if not isinstance(self.unresolved, Unset): + unresolved = self.unresolved + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "taxonomy_id": taxonomy_id, + "name": name, + "template": template, + "entity_type": entity_type, + "elements_created": elements_created, + "mappings_created": mappings_created, + } + ) + if frameworks is not UNSET: + field_dict["frameworks"] = frameworks + if unresolved is not UNSET: + field_dict["unresolved"] = unresolved + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + taxonomy_id = d.pop("taxonomy_id") + + name = d.pop("name") + + template = InitializeChartOfAccountsResponseTemplate(d.pop("template")) + + entity_type = d.pop("entity_type") + + elements_created = d.pop("elements_created") + + mappings_created = d.pop("mappings_created") + + frameworks = cast(list[str], d.pop("frameworks", UNSET)) + + unresolved = cast(list[str], d.pop("unresolved", UNSET)) + + initialize_chart_of_accounts_response = cls( + taxonomy_id=taxonomy_id, + name=name, + template=template, + entity_type=entity_type, + elements_created=elements_created, + mappings_created=mappings_created, + frameworks=frameworks, + unresolved=unresolved, + ) + + initialize_chart_of_accounts_response.additional_properties = d + return initialize_chart_of_accounts_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/initialize_chart_of_accounts_response_template.py b/robosystems_client/models/initialize_chart_of_accounts_response_template.py new file mode 100644 index 0000000..d01577c --- /dev/null +++ b/robosystems_client/models/initialize_chart_of_accounts_response_template.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class InitializeChartOfAccountsResponseTemplate(str, Enum): + PRODUCT = "product" + SAAS = "saas" + SERVICES = "services" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/operation_envelope_initialize_chart_of_accounts_response.py b/robosystems_client/models/operation_envelope_initialize_chart_of_accounts_response.py new file mode 100644 index 0000000..f9b4738 --- /dev/null +++ b/robosystems_client/models/operation_envelope_initialize_chart_of_accounts_response.py @@ -0,0 +1,164 @@ +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_initialize_chart_of_accounts_response_status import ( + OperationEnvelopeInitializeChartOfAccountsResponseStatus, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.initialize_chart_of_accounts_response import ( + InitializeChartOfAccountsResponse, + ) + + +T = TypeVar("T", bound="OperationEnvelopeInitializeChartOfAccountsResponse") + + +@_attrs_define +class OperationEnvelopeInitializeChartOfAccountsResponse: + """ + Attributes: + operation (str): Kebab-case operation name + operation_id (str): op_-prefixed ULID for audit and SSE correlation + status (OperationEnvelopeInitializeChartOfAccountsResponseStatus): Operation lifecycle state + at (str): ISO-8601 UTC timestamp + result (InitializeChartOfAccountsResponse | None | Unset): Command-specific result payload + created_by (None | str | Unset): User ID that initiated the operation (null for legacy callers) + 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: OperationEnvelopeInitializeChartOfAccountsResponseStatus + at: str + result: InitializeChartOfAccountsResponse | 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.initialize_chart_of_accounts_response import ( + InitializeChartOfAccountsResponse, + ) + + 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, InitializeChartOfAccountsResponse): + 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.initialize_chart_of_accounts_response import ( + InitializeChartOfAccountsResponse, + ) + + d = dict(src_dict) + operation = d.pop("operation") + + operation_id = d.pop("operationId") + + status = OperationEnvelopeInitializeChartOfAccountsResponseStatus(d.pop("status")) + + at = d.pop("at") + + def _parse_result(data: object) -> InitializeChartOfAccountsResponse | 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 = InitializeChartOfAccountsResponse.from_dict(data) + + return result_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(InitializeChartOfAccountsResponse | 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_initialize_chart_of_accounts_response = cls( + operation=operation, + operation_id=operation_id, + status=status, + at=at, + result=result, + created_by=created_by, + idempotent_replay=idempotent_replay, + ) + + operation_envelope_initialize_chart_of_accounts_response.additional_properties = d + return operation_envelope_initialize_chart_of_accounts_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_initialize_chart_of_accounts_response_status.py b/robosystems_client/models/operation_envelope_initialize_chart_of_accounts_response_status.py new file mode 100644 index 0000000..669c9c1 --- /dev/null +++ b/robosystems_client/models/operation_envelope_initialize_chart_of_accounts_response_status.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class OperationEnvelopeInitializeChartOfAccountsResponseStatus(str, Enum): + COMPLETED = "completed" + FAILED = "failed" + PENDING = "pending" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/search_request.py b/robosystems_client/models/search_request.py index 87a939d..efdca3f 100644 --- a/robosystems_client/models/search_request.py +++ b/robosystems_client/models/search_request.py @@ -31,6 +31,8 @@ class SearchRequest: False. size (int | Unset): Max results to return Default: 10. offset (int | Unset): Pagination offset Default: 0. + snippet_chars (int | None | Unset): Approximate snippet budget per hit in characters; the default is three + highlight fragments of about 200 """ query: str @@ -45,6 +47,7 @@ class SearchRequest: semantic: bool | Unset = False size: int | Unset = 10 offset: int | Unset = 0 + snippet_chars: int | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -104,6 +107,12 @@ def to_dict(self) -> dict[str, Any]: offset = self.offset + snippet_chars: int | None | Unset + if isinstance(self.snippet_chars, Unset): + snippet_chars = UNSET + else: + snippet_chars = self.snippet_chars + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -133,6 +142,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["size"] = size if offset is not UNSET: field_dict["offset"] = offset + if snippet_chars is not UNSET: + field_dict["snippet_chars"] = snippet_chars return field_dict @@ -219,6 +230,15 @@ def _parse_date_to(data: object) -> None | str | Unset: offset = d.pop("offset", UNSET) + def _parse_snippet_chars(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + snippet_chars = _parse_snippet_chars(d.pop("snippet_chars", UNSET)) + search_request = cls( query=query, entity=entity, @@ -232,6 +252,7 @@ def _parse_date_to(data: object) -> None | str | Unset: semantic=semantic, size=size, offset=offset, + snippet_chars=snippet_chars, ) search_request.additional_properties = d diff --git a/tests/test_ledger_client.py b/tests/test_ledger_client.py index 75a135e..0c95f30 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -2520,3 +2520,68 @@ def test_list_blocked_source_graphs(self, mock_query, mock_config, graph_id): assert page is not None assert page.blocked_source_graphs[0].source_graph_id == "kg_sender" assert page.pagination.total == 1 + + +# ── Chart of accounts ───────────────────────────────────────────────── + + +@pytest.mark.unit +class TestChartOfAccountsOps: + @patch("robosystems_client.graphql.client.GraphQLClient.execute") + def test_list_chart_templates(self, mock_execute, mock_config, graph_id): + mock_execute.return_value = { + "chartTemplates": [ + { + "key": "saas", + "displayName": "SaaS / subscription software", + "description": "Recurring revenue …", + "accountCount": 20, + }, + { + "key": "product", + "displayName": "Product business (inventory and COGS)", + "description": "Goods sold …", + "accountCount": 27, + }, + ] + } + client = LedgerClient(mock_config) + templates = client.list_chart_templates(graph_id) + assert [t.key for t in templates] == ["saas", "product"] + assert templates[0].display_name == "SaaS / subscription software" + assert templates[1].account_count == 27 + + @patch("robosystems_client.clients.ledger_client.op_initialize_chart_of_accounts") + def test_initialize_chart_of_accounts(self, mock_op, mock_config, graph_id): + envelope = _envelope( + "initialize-chart-of-accounts", + { + "taxonomy_id": "tax_new", + "name": "Chart of Accounts", + "template": "saas", + "entity_type": "llc", + "elements_created": 20, + "mappings_created": 20, + "frameworks": ["rs-gaap"], + "unresolved": [], + }, + ) + mock_op.return_value = _mock_response(envelope) + client = LedgerClient(mock_config) + + result = client.initialize_chart_of_accounts(graph_id, "saas", entity_type="llc") + + body = mock_op.call_args.kwargs["body"] + assert body.template.value == "saas" + assert body.entity_type == "llc" + # dict mocks come back as a plain dict (see LedgerClient._typed_result) + assert result["taxonomy_id"] == "tax_new" + assert result["frameworks"] == ["rs-gaap"] + assert result["mappings_created"] == 20 + + def test_initialize_chart_of_accounts_rejects_unknown_template( + self, mock_config, graph_id + ): + client = LedgerClient(mock_config) + with pytest.raises(ValueError): + client.initialize_chart_of_accounts(graph_id, "retail")