From 4036134e03b536789529bb7abf213b20c41e3912 Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Thu, 1 Oct 2026 23:28:33 -0500 Subject: [PATCH 1/2] feat: add reconciliations to the ledger facade Regenerated against the current API, then on `LedgerClient`: - `list_reconciliations` reads every reconciliation's standing for a period. - `preview_reconciliations`, `refresh_reconciliations`, `record_statement_balance`, `set_reconciliation_policy` and `sign_off_reconciliation` call the matching operations. - `close_period` takes `allow_unreconciled_accounts`, and `create_schedule` takes `booked_on`. - `get_fiscal_calendar` carries the unreconciled-account count and sample. Claude-Session: https://claude.ai/code/session_01N4VGnvrpE2cwbBZykz4yGd --- .../preview_reconciliations.py | 300 ++++++++++ .../record_statement_balance.py | 306 ++++++++++ .../refresh_reconciliations.py | 304 ++++++++++ .../set_reconciliation_policy.py | 284 +++++++++ .../sign_off_reconciliation.py | 286 +++++++++ robosystems_client/clients/ledger_client.py | 200 ++++++- .../graphql/generated/__init__.py | 16 + .../graphql/generated/client.py | 15 + .../generated/get_ledger_fiscal_calendar.py | 2 + .../generated/list_ledger_reconciliations.py | 91 +++ .../graphql/generated/operations.py | 69 +++ .../ledger/GetLedgerFiscalCalendar.graphql | 1 + .../ledger/ListLedgerReconciliations.graphql | 20 + robosystems_client/graphql/schema.graphql | 234 +++++++- robosystems_client/models/__init__.py | 64 +++ .../models/artifact_response.py | 22 +- .../models/backfill_plan_history_operation.py | 11 + .../models/close_period_operation.py | 12 + .../models/create_legacy_arm_block_type.py | 1 + .../models/delete_legacy_arm_block_type.py | 1 + .../models/delete_schedule_arm.py | 17 +- .../models/delete_schedule_request.py | 17 +- .../models/fiscal_calendar_response.py | 25 + ...n_envelope_reconciliation_list_response.py | 158 +++++ ...ope_reconciliation_list_response_status.py | 10 + ...envelope_reconciliation_policy_response.py | 158 +++++ ...e_reconciliation_policy_response_status.py | 10 + ...nvelope_reconciliation_preview_response.py | 158 +++++ ..._reconciliation_preview_response_status.py | 10 + ...eration_envelope_reconciliation_summary.py | 158 +++++ ..._envelope_reconciliation_summary_status.py | 10 + .../models/preview_reconciliations_request.py | 99 ++++ .../preview_reconciliations_request_method.py | 10 + .../models/reconciliation_component.py | 154 +++++ .../models/reconciliation_list_response.py | 107 ++++ .../models/reconciliation_mechanics.py | 149 +++++ .../models/reconciliation_mechanics_method.py | 10 + .../models/reconciliation_mechanics_scope.py | 9 + .../models/reconciliation_policy_response.py | 94 +++ .../models/reconciliation_preview_response.py | 218 +++++++ .../reconciliation_preview_response_method.py | 10 + .../models/reconciliation_row.py | 244 ++++++++ .../models/reconciliation_summary.py | 544 ++++++++++++++++++ .../record_statement_balance_request.py | 124 ++++ .../models/refresh_reconciliations_request.py | 62 ++ .../models/schedule_metadata_request.py | 35 ++ .../set_reconciliation_policy_request.py | 147 +++++ .../models/sign_off_reconciliation_request.py | 92 +++ .../models/update_legacy_arm_block_type.py | 1 + .../models/update_schedule_arm.py | 3 +- .../models/update_schedule_request.py | 3 +- tests/test_ledger_client.py | 314 ++++++++++ 52 files changed, 5371 insertions(+), 28 deletions(-) create mode 100644 robosystems_client/api/robo_ledger_ledger_events/preview_reconciliations.py create mode 100644 robosystems_client/api/robo_ledger_ledger_events/record_statement_balance.py create mode 100644 robosystems_client/api/robo_ledger_ledger_events/refresh_reconciliations.py create mode 100644 robosystems_client/api/robo_ledger_ledger_events/set_reconciliation_policy.py create mode 100644 robosystems_client/api/robo_ledger_ledger_events/sign_off_reconciliation.py create mode 100644 robosystems_client/graphql/generated/list_ledger_reconciliations.py create mode 100644 robosystems_client/graphql/operations/ledger/ListLedgerReconciliations.graphql create mode 100644 robosystems_client/models/operation_envelope_reconciliation_list_response.py create mode 100644 robosystems_client/models/operation_envelope_reconciliation_list_response_status.py create mode 100644 robosystems_client/models/operation_envelope_reconciliation_policy_response.py create mode 100644 robosystems_client/models/operation_envelope_reconciliation_policy_response_status.py create mode 100644 robosystems_client/models/operation_envelope_reconciliation_preview_response.py create mode 100644 robosystems_client/models/operation_envelope_reconciliation_preview_response_status.py create mode 100644 robosystems_client/models/operation_envelope_reconciliation_summary.py create mode 100644 robosystems_client/models/operation_envelope_reconciliation_summary_status.py create mode 100644 robosystems_client/models/preview_reconciliations_request.py create mode 100644 robosystems_client/models/preview_reconciliations_request_method.py create mode 100644 robosystems_client/models/reconciliation_component.py create mode 100644 robosystems_client/models/reconciliation_list_response.py create mode 100644 robosystems_client/models/reconciliation_mechanics.py create mode 100644 robosystems_client/models/reconciliation_mechanics_method.py create mode 100644 robosystems_client/models/reconciliation_mechanics_scope.py create mode 100644 robosystems_client/models/reconciliation_policy_response.py create mode 100644 robosystems_client/models/reconciliation_preview_response.py create mode 100644 robosystems_client/models/reconciliation_preview_response_method.py create mode 100644 robosystems_client/models/reconciliation_row.py create mode 100644 robosystems_client/models/reconciliation_summary.py create mode 100644 robosystems_client/models/record_statement_balance_request.py create mode 100644 robosystems_client/models/refresh_reconciliations_request.py create mode 100644 robosystems_client/models/set_reconciliation_policy_request.py create mode 100644 robosystems_client/models/sign_off_reconciliation_request.py diff --git a/robosystems_client/api/robo_ledger_ledger_events/preview_reconciliations.py b/robosystems_client/api/robo_ledger_ledger_events/preview_reconciliations.py new file mode 100644 index 0000000..c2d6c48 --- /dev/null +++ b/robosystems_client/api/robo_ledger_ledger_events/preview_reconciliations.py @@ -0,0 +1,300 @@ +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_reconciliation_preview_response import ( + OperationEnvelopeReconciliationPreviewResponse, +) +from ...models.preview_reconciliations_request import PreviewReconciliationsRequest +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + graph_id: str, + *, + body: PreviewReconciliationsRequest, + 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/preview-reconciliations".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 | OperationEnvelopeReconciliationPreviewResponse | None: + if response.status_code == 200: + response_200 = OperationEnvelopeReconciliationPreviewResponse.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 | OperationEnvelopeReconciliationPreviewResponse]: + 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: PreviewReconciliationsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeReconciliationPreviewResponse]: + """Preview Reconciliations + + Compare the ledger at a period end with something outside it, without recording anything. `method` + picks the check. `source_ledger` (the default) reads QuickBooks' own trial balance for the period + end and sets it beside the ledger's, account by account: balance-sheet accounts cumulatively, income + and expense accounts from the start of the fiscal year. A difference there means the ledger's copy + of the books has drifted from the source (a transaction deleted or back-dated there after it was + synced, or activity not yet synced), so run it before trusting any other figure on a synced ledger; + it needs a connected QuickBooks ledger. `schedule_register` compares each asset account a schedule + carries a balance on (a prepaid, accumulated depreciation, a fixed-asset cost account) with what its + schedules say it holds, and lists the schedules behind each figure. A difference there is a balance + with no schedule behind it, or a scheduled amount the ledger does not hold. Each row carries both + balances, the difference, and whether the account ties. + + **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 (PreviewReconciliationsRequest): Compare the ledger's balances at a period end with + an independent source. + + 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 | OperationEnvelopeReconciliationPreviewResponse] + """ + + 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: PreviewReconciliationsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeReconciliationPreviewResponse | None: + """Preview Reconciliations + + Compare the ledger at a period end with something outside it, without recording anything. `method` + picks the check. `source_ledger` (the default) reads QuickBooks' own trial balance for the period + end and sets it beside the ledger's, account by account: balance-sheet accounts cumulatively, income + and expense accounts from the start of the fiscal year. A difference there means the ledger's copy + of the books has drifted from the source (a transaction deleted or back-dated there after it was + synced, or activity not yet synced), so run it before trusting any other figure on a synced ledger; + it needs a connected QuickBooks ledger. `schedule_register` compares each asset account a schedule + carries a balance on (a prepaid, accumulated depreciation, a fixed-asset cost account) with what its + schedules say it holds, and lists the schedules behind each figure. A difference there is a balance + with no schedule behind it, or a scheduled amount the ledger does not hold. Each row carries both + balances, the difference, and whether the account ties. + + **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 (PreviewReconciliationsRequest): Compare the ledger's balances at a period end with + an independent source. + + 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 | OperationEnvelopeReconciliationPreviewResponse + """ + + 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: PreviewReconciliationsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeReconciliationPreviewResponse]: + """Preview Reconciliations + + Compare the ledger at a period end with something outside it, without recording anything. `method` + picks the check. `source_ledger` (the default) reads QuickBooks' own trial balance for the period + end and sets it beside the ledger's, account by account: balance-sheet accounts cumulatively, income + and expense accounts from the start of the fiscal year. A difference there means the ledger's copy + of the books has drifted from the source (a transaction deleted or back-dated there after it was + synced, or activity not yet synced), so run it before trusting any other figure on a synced ledger; + it needs a connected QuickBooks ledger. `schedule_register` compares each asset account a schedule + carries a balance on (a prepaid, accumulated depreciation, a fixed-asset cost account) with what its + schedules say it holds, and lists the schedules behind each figure. A difference there is a balance + with no schedule behind it, or a scheduled amount the ledger does not hold. Each row carries both + balances, the difference, and whether the account ties. + + **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 (PreviewReconciliationsRequest): Compare the ledger's balances at a period end with + an independent source. + + 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 | OperationEnvelopeReconciliationPreviewResponse] + """ + + 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: PreviewReconciliationsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeReconciliationPreviewResponse | None: + """Preview Reconciliations + + Compare the ledger at a period end with something outside it, without recording anything. `method` + picks the check. `source_ledger` (the default) reads QuickBooks' own trial balance for the period + end and sets it beside the ledger's, account by account: balance-sheet accounts cumulatively, income + and expense accounts from the start of the fiscal year. A difference there means the ledger's copy + of the books has drifted from the source (a transaction deleted or back-dated there after it was + synced, or activity not yet synced), so run it before trusting any other figure on a synced ledger; + it needs a connected QuickBooks ledger. `schedule_register` compares each asset account a schedule + carries a balance on (a prepaid, accumulated depreciation, a fixed-asset cost account) with what its + schedules say it holds, and lists the schedules behind each figure. A difference there is a balance + with no schedule behind it, or a scheduled amount the ledger does not hold. Each row carries both + balances, the difference, and whether the account ties. + + **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 (PreviewReconciliationsRequest): Compare the ledger's balances at a period end with + an independent source. + + 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 | OperationEnvelopeReconciliationPreviewResponse + """ + + 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/record_statement_balance.py b/robosystems_client/api/robo_ledger_ledger_events/record_statement_balance.py new file mode 100644 index 0000000..8ff39a0 --- /dev/null +++ b/robosystems_client/api/robo_ledger_ledger_events/record_statement_balance.py @@ -0,0 +1,306 @@ +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_reconciliation_summary import ( + OperationEnvelopeReconciliationSummary, +) +from ...models.record_statement_balance_request import RecordStatementBalanceRequest +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + graph_id: str, + *, + body: RecordStatementBalanceRequest, + idempotency_key: None | str | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + if not isinstance(idempotency_key, Unset): + headers["Idempotency-Key"] = idempotency_key + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/extensions/roboledger/{graph_id}/operations/record-statement-balance".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 | OperationEnvelopeReconciliationSummary | None: + if response.status_code == 200: + response_200 = OperationEnvelopeReconciliationSummary.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 | OperationEnvelopeReconciliationSummary]: + 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: RecordStatementBalanceRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeReconciliationSummary]: + """Record Statement Balance + + Record the ending balance of a statement (a bank, card or loan statement) for one balance-sheet + account, and reconcile the account to it. Give the balance as the statement shows it, as a positive + number in the account's normal direction, with the statement's ending date. The ledger's balance at + that date is set beside it, counting the drafts the close will post, and the result is recorded on + the account's statement reconciliation for the period the statement ends in. Attach the statement as + evidence by passing the `document_id` of a document added with create-document. Writes no books. The + first statement recorded for an account creates its reconciliation, which does not hold the close: + turn `required_for_close` on with set-reconciliation-policy to make every period's close wait for a + statement on that account. Recording the same account and date again replaces the earlier balance; + when more than one statement ends in a period, the one with the latest date stands. Recording a + balance that differs from one already signed off lapses that sign-off. A difference is not explained + here: it is activity one side has and the other does not yet, or an error on either. Returns the + reconciliation's standing for the period. + + **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 (RecordStatementBalanceRequest): Record the ending balance of a statement for one + account. + + 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 | OperationEnvelopeReconciliationSummary] + """ + + 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: RecordStatementBalanceRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeReconciliationSummary | None: + """Record Statement Balance + + Record the ending balance of a statement (a bank, card or loan statement) for one balance-sheet + account, and reconcile the account to it. Give the balance as the statement shows it, as a positive + number in the account's normal direction, with the statement's ending date. The ledger's balance at + that date is set beside it, counting the drafts the close will post, and the result is recorded on + the account's statement reconciliation for the period the statement ends in. Attach the statement as + evidence by passing the `document_id` of a document added with create-document. Writes no books. The + first statement recorded for an account creates its reconciliation, which does not hold the close: + turn `required_for_close` on with set-reconciliation-policy to make every period's close wait for a + statement on that account. Recording the same account and date again replaces the earlier balance; + when more than one statement ends in a period, the one with the latest date stands. Recording a + balance that differs from one already signed off lapses that sign-off. A difference is not explained + here: it is activity one side has and the other does not yet, or an error on either. Returns the + reconciliation's standing for the period. + + **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 (RecordStatementBalanceRequest): Record the ending balance of a statement for one + account. + + 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 | OperationEnvelopeReconciliationSummary + """ + + 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: RecordStatementBalanceRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeReconciliationSummary]: + """Record Statement Balance + + Record the ending balance of a statement (a bank, card or loan statement) for one balance-sheet + account, and reconcile the account to it. Give the balance as the statement shows it, as a positive + number in the account's normal direction, with the statement's ending date. The ledger's balance at + that date is set beside it, counting the drafts the close will post, and the result is recorded on + the account's statement reconciliation for the period the statement ends in. Attach the statement as + evidence by passing the `document_id` of a document added with create-document. Writes no books. The + first statement recorded for an account creates its reconciliation, which does not hold the close: + turn `required_for_close` on with set-reconciliation-policy to make every period's close wait for a + statement on that account. Recording the same account and date again replaces the earlier balance; + when more than one statement ends in a period, the one with the latest date stands. Recording a + balance that differs from one already signed off lapses that sign-off. A difference is not explained + here: it is activity one side has and the other does not yet, or an error on either. Returns the + reconciliation's standing for the period. + + **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 (RecordStatementBalanceRequest): Record the ending balance of a statement for one + account. + + 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 | OperationEnvelopeReconciliationSummary] + """ + + 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: RecordStatementBalanceRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeReconciliationSummary | None: + """Record Statement Balance + + Record the ending balance of a statement (a bank, card or loan statement) for one balance-sheet + account, and reconcile the account to it. Give the balance as the statement shows it, as a positive + number in the account's normal direction, with the statement's ending date. The ledger's balance at + that date is set beside it, counting the drafts the close will post, and the result is recorded on + the account's statement reconciliation for the period the statement ends in. Attach the statement as + evidence by passing the `document_id` of a document added with create-document. Writes no books. The + first statement recorded for an account creates its reconciliation, which does not hold the close: + turn `required_for_close` on with set-reconciliation-policy to make every period's close wait for a + statement on that account. Recording the same account and date again replaces the earlier balance; + when more than one statement ends in a period, the one with the latest date stands. Recording a + balance that differs from one already signed off lapses that sign-off. A difference is not explained + here: it is activity one side has and the other does not yet, or an error on either. Returns the + reconciliation's standing for the period. + + **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 (RecordStatementBalanceRequest): Record the ending balance of a statement for one + account. + + 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 | OperationEnvelopeReconciliationSummary + """ + + 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/refresh_reconciliations.py b/robosystems_client/api/robo_ledger_ledger_events/refresh_reconciliations.py new file mode 100644 index 0000000..7b96435 --- /dev/null +++ b/robosystems_client/api/robo_ledger_ledger_events/refresh_reconciliations.py @@ -0,0 +1,304 @@ +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_reconciliation_list_response import ( + OperationEnvelopeReconciliationListResponse, +) +from ...models.refresh_reconciliations_request import RefreshReconciliationsRequest +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + graph_id: str, + *, + body: RefreshReconciliationsRequest, + 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/refresh-reconciliations".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 | OperationEnvelopeReconciliationListResponse | None: + if response.status_code == 200: + response_200 = OperationEnvelopeReconciliationListResponse.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 | OperationEnvelopeReconciliationListResponse]: + 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: RefreshReconciliationsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeReconciliationListResponse]: + """Refresh Reconciliations + + Run every reconciliation that applies at a period end and record each result on its block. A ledger + synced from QuickBooks is compared with QuickBooks' own trial balance, on one block for the whole + ledger. Each asset account a schedule carries a balance on is compared with what its schedules say + it holds, on a block of its own. Each account with a statement balance recorded in the period + (record-statement-balance) is compared with that balance. A block reconciles for the period when its + difference is within its materiality. Running it again replaces the period's comparison, so the + answer is always as of the last run. A block is created the first time its check applies, and from + then on the period's close waits on it until set-reconciliation-policy releases it. The one + exception is an account block whose first comparison does not tie: it is created without holding the + close, so a difference found on first contact is reported and becomes a close requirement only when + you turn it on. Returns every reconciliation's standing for the period. Use preview-reconciliations + to see a comparison without recording it. + + **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 (RefreshReconciliationsRequest): Compare each reconciliation at a period end and + record the result. + + 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 | OperationEnvelopeReconciliationListResponse] + """ + + 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: RefreshReconciliationsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeReconciliationListResponse | None: + """Refresh Reconciliations + + Run every reconciliation that applies at a period end and record each result on its block. A ledger + synced from QuickBooks is compared with QuickBooks' own trial balance, on one block for the whole + ledger. Each asset account a schedule carries a balance on is compared with what its schedules say + it holds, on a block of its own. Each account with a statement balance recorded in the period + (record-statement-balance) is compared with that balance. A block reconciles for the period when its + difference is within its materiality. Running it again replaces the period's comparison, so the + answer is always as of the last run. A block is created the first time its check applies, and from + then on the period's close waits on it until set-reconciliation-policy releases it. The one + exception is an account block whose first comparison does not tie: it is created without holding the + close, so a difference found on first contact is reported and becomes a close requirement only when + you turn it on. Returns every reconciliation's standing for the period. Use preview-reconciliations + to see a comparison without recording it. + + **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 (RefreshReconciliationsRequest): Compare each reconciliation at a period end and + record the result. + + 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 | OperationEnvelopeReconciliationListResponse + """ + + 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: RefreshReconciliationsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeReconciliationListResponse]: + """Refresh Reconciliations + + Run every reconciliation that applies at a period end and record each result on its block. A ledger + synced from QuickBooks is compared with QuickBooks' own trial balance, on one block for the whole + ledger. Each asset account a schedule carries a balance on is compared with what its schedules say + it holds, on a block of its own. Each account with a statement balance recorded in the period + (record-statement-balance) is compared with that balance. A block reconciles for the period when its + difference is within its materiality. Running it again replaces the period's comparison, so the + answer is always as of the last run. A block is created the first time its check applies, and from + then on the period's close waits on it until set-reconciliation-policy releases it. The one + exception is an account block whose first comparison does not tie: it is created without holding the + close, so a difference found on first contact is reported and becomes a close requirement only when + you turn it on. Returns every reconciliation's standing for the period. Use preview-reconciliations + to see a comparison without recording it. + + **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 (RefreshReconciliationsRequest): Compare each reconciliation at a period end and + record the result. + + 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 | OperationEnvelopeReconciliationListResponse] + """ + + 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: RefreshReconciliationsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeReconciliationListResponse | None: + """Refresh Reconciliations + + Run every reconciliation that applies at a period end and record each result on its block. A ledger + synced from QuickBooks is compared with QuickBooks' own trial balance, on one block for the whole + ledger. Each asset account a schedule carries a balance on is compared with what its schedules say + it holds, on a block of its own. Each account with a statement balance recorded in the period + (record-statement-balance) is compared with that balance. A block reconciles for the period when its + difference is within its materiality. Running it again replaces the period's comparison, so the + answer is always as of the last run. A block is created the first time its check applies, and from + then on the period's close waits on it until set-reconciliation-policy releases it. The one + exception is an account block whose first comparison does not tie: it is created without holding the + close, so a difference found on first contact is reported and becomes a close requirement only when + you turn it on. Returns every reconciliation's standing for the period. Use preview-reconciliations + to see a comparison without recording it. + + **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 (RefreshReconciliationsRequest): Compare each reconciliation at a period end and + record the result. + + 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 | OperationEnvelopeReconciliationListResponse + """ + + 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/set_reconciliation_policy.py b/robosystems_client/api/robo_ledger_ledger_events/set_reconciliation_policy.py new file mode 100644 index 0000000..3a5a3ba --- /dev/null +++ b/robosystems_client/api/robo_ledger_ledger_events/set_reconciliation_policy.py @@ -0,0 +1,284 @@ +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_reconciliation_policy_response import ( + OperationEnvelopeReconciliationPolicyResponse, +) +from ...models.set_reconciliation_policy_request import SetReconciliationPolicyRequest +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + graph_id: str, + *, + body: SetReconciliationPolicyRequest, + 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/set-reconciliation-policy".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 | OperationEnvelopeReconciliationPolicyResponse | None: + if response.status_code == 200: + response_200 = OperationEnvelopeReconciliationPolicyResponse.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 | OperationEnvelopeReconciliationPolicyResponse]: + 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: SetReconciliationPolicyRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeReconciliationPolicyResponse]: + """Set Reconciliation Policy + + Change how much the close cares about one reconciliation: whether the period's close waits on it + (`required_for_close`); its `materiality`, the difference up to which it still counts as reconciled; + whether the close also waits for a sign-off (`review_required`); and whether the reviewer must be + someone other than the person who ran the comparison (`separate_reviewer`, which can only be turned + on while the graph has at least two members who can write; if it later has one, turn it off or add a + member). Omitted fields keep their value. The next refresh-reconciliations uses the new materiality; + comparisons already recorded are not re-judged. + + **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 (SetReconciliationPolicyRequest): Change how much the close cares about one + reconciliation. + + 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 | OperationEnvelopeReconciliationPolicyResponse] + """ + + 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: SetReconciliationPolicyRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeReconciliationPolicyResponse | None: + """Set Reconciliation Policy + + Change how much the close cares about one reconciliation: whether the period's close waits on it + (`required_for_close`); its `materiality`, the difference up to which it still counts as reconciled; + whether the close also waits for a sign-off (`review_required`); and whether the reviewer must be + someone other than the person who ran the comparison (`separate_reviewer`, which can only be turned + on while the graph has at least two members who can write; if it later has one, turn it off or add a + member). Omitted fields keep their value. The next refresh-reconciliations uses the new materiality; + comparisons already recorded are not re-judged. + + **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 (SetReconciliationPolicyRequest): Change how much the close cares about one + reconciliation. + + 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 | OperationEnvelopeReconciliationPolicyResponse + """ + + 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: SetReconciliationPolicyRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeReconciliationPolicyResponse]: + """Set Reconciliation Policy + + Change how much the close cares about one reconciliation: whether the period's close waits on it + (`required_for_close`); its `materiality`, the difference up to which it still counts as reconciled; + whether the close also waits for a sign-off (`review_required`); and whether the reviewer must be + someone other than the person who ran the comparison (`separate_reviewer`, which can only be turned + on while the graph has at least two members who can write; if it later has one, turn it off or add a + member). Omitted fields keep their value. The next refresh-reconciliations uses the new materiality; + comparisons already recorded are not re-judged. + + **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 (SetReconciliationPolicyRequest): Change how much the close cares about one + reconciliation. + + 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 | OperationEnvelopeReconciliationPolicyResponse] + """ + + 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: SetReconciliationPolicyRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeReconciliationPolicyResponse | None: + """Set Reconciliation Policy + + Change how much the close cares about one reconciliation: whether the period's close waits on it + (`required_for_close`); its `materiality`, the difference up to which it still counts as reconciled; + whether the close also waits for a sign-off (`review_required`); and whether the reviewer must be + someone other than the person who ran the comparison (`separate_reviewer`, which can only be turned + on while the graph has at least two members who can write; if it later has one, turn it off or add a + member). Omitted fields keep their value. The next refresh-reconciliations uses the new materiality; + comparisons already recorded are not re-judged. + + **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 (SetReconciliationPolicyRequest): Change how much the close cares about one + reconciliation. + + 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 | OperationEnvelopeReconciliationPolicyResponse + """ + + 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/sign_off_reconciliation.py b/robosystems_client/api/robo_ledger_ledger_events/sign_off_reconciliation.py new file mode 100644 index 0000000..f95b9e2 --- /dev/null +++ b/robosystems_client/api/robo_ledger_ledger_events/sign_off_reconciliation.py @@ -0,0 +1,286 @@ +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_reconciliation_summary import ( + OperationEnvelopeReconciliationSummary, +) +from ...models.sign_off_reconciliation_request import SignOffReconciliationRequest +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + graph_id: str, + *, + body: SignOffReconciliationRequest, + 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/sign-off-reconciliation".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 | OperationEnvelopeReconciliationSummary | None: + if response.status_code == 200: + response_200 = OperationEnvelopeReconciliationSummary.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 | OperationEnvelopeReconciliationSummary]: + 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: SignOffReconciliationRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeReconciliationSummary]: + """Sign Off Reconciliation + + Record your review of a reconciliation for a period. Only a period that reconciles can be signed + off, and only by a member of the graph: access that comes from an organization role is not enough. + The sign-off records who ran the comparison and who reviewed it, and says so when they are the same + person; a reconciliation with `separate_reviewer` set refuses that case instead. The sign-off stands + for the balances as they were compared: if any of them changes afterwards, the period goes back to + reconciled or unreconciled and needs signing off again, unless the balances return to the ones + already signed. Signing off a period already reviewed changes nothing. Returns the reconciliation's + standing for the period. + + **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 (SignOffReconciliationRequest): Sign off a reconciliation for a period as its + reviewer. + + 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 | OperationEnvelopeReconciliationSummary] + """ + + 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: SignOffReconciliationRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeReconciliationSummary | None: + """Sign Off Reconciliation + + Record your review of a reconciliation for a period. Only a period that reconciles can be signed + off, and only by a member of the graph: access that comes from an organization role is not enough. + The sign-off records who ran the comparison and who reviewed it, and says so when they are the same + person; a reconciliation with `separate_reviewer` set refuses that case instead. The sign-off stands + for the balances as they were compared: if any of them changes afterwards, the period goes back to + reconciled or unreconciled and needs signing off again, unless the balances return to the ones + already signed. Signing off a period already reviewed changes nothing. Returns the reconciliation's + standing for the period. + + **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 (SignOffReconciliationRequest): Sign off a reconciliation for a period as its + reviewer. + + 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 | OperationEnvelopeReconciliationSummary + """ + + 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: SignOffReconciliationRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeReconciliationSummary]: + """Sign Off Reconciliation + + Record your review of a reconciliation for a period. Only a period that reconciles can be signed + off, and only by a member of the graph: access that comes from an organization role is not enough. + The sign-off records who ran the comparison and who reviewed it, and says so when they are the same + person; a reconciliation with `separate_reviewer` set refuses that case instead. The sign-off stands + for the balances as they were compared: if any of them changes afterwards, the period goes back to + reconciled or unreconciled and needs signing off again, unless the balances return to the ones + already signed. Signing off a period already reviewed changes nothing. Returns the reconciliation's + standing for the period. + + **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 (SignOffReconciliationRequest): Sign off a reconciliation for a period as its + reviewer. + + 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 | OperationEnvelopeReconciliationSummary] + """ + + 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: SignOffReconciliationRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeReconciliationSummary | None: + """Sign Off Reconciliation + + Record your review of a reconciliation for a period. Only a period that reconciles can be signed + off, and only by a member of the graph: access that comes from an organization role is not enough. + The sign-off records who ran the comparison and who reviewed it, and says so when they are the same + person; a reconciliation with `separate_reviewer` set refuses that case instead. The sign-off stands + for the balances as they were compared: if any of them changes afterwards, the period goes back to + reconciled or unreconciled and needs signing off again, unless the balances return to the ones + already signed. Signing off a period already reviewed changes nothing. Returns the reconciliation's + standing for the period. + + **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 (SignOffReconciliationRequest): Sign off a reconciliation for a period as its + reviewer. + + 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 | OperationEnvelopeReconciliationSummary + """ + + 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 e4afea9..641864a 100644 --- a/robosystems_client/clients/ledger_client.py +++ b/robosystems_client/clients/ledger_client.py @@ -42,6 +42,21 @@ from ..api.robo_ledger_fiscal_close.close_period import ( sync_detailed as op_close_period, ) +from ..api.robo_ledger_ledger_events.preview_reconciliations import ( + sync_detailed as op_preview_reconciliations, +) +from ..api.robo_ledger_ledger_events.record_statement_balance import ( + sync_detailed as op_record_statement_balance, +) +from ..api.robo_ledger_ledger_events.refresh_reconciliations import ( + sync_detailed as op_refresh_reconciliations, +) +from ..api.robo_ledger_ledger_events.set_reconciliation_policy import ( + sync_detailed as op_set_reconciliation_policy, +) +from ..api.robo_ledger_ledger_events.sign_off_reconciliation import ( + sync_detailed as op_sign_off_reconciliation, +) from ..api.robo_ledger_information_blocks.compute_metrics import ( sync_detailed as op_compute_metrics, ) @@ -227,6 +242,12 @@ from ..graphql.generated.get_ledger_fiscal_calendar import ( GetLedgerFiscalCalendarFiscalCalendar as FiscalCalendar, ) +from ..graphql.generated.list_ledger_reconciliations import ( + ListLedgerReconciliations, +) +from ..graphql.generated.list_ledger_reconciliations import ( + ListLedgerReconciliationsReconciliations as ReconciliationList, +) from ..graphql.generated.list_chart_templates import ( ListChartTemplates, ) @@ -400,6 +421,7 @@ GET_LEDGER_EVENT_BLOCK_GQL, GET_LEDGER_FISCAL_CALENDAR_GQL, LIST_CHART_TEMPLATES_GQL, + LIST_LEDGER_RECONCILIATIONS_GQL, GET_LEDGER_MAPPED_TRIAL_BALANCE_GQL, GET_LEDGER_MAPPING_COVERAGE_GQL, GET_LEDGER_MAPPING_GQL, @@ -472,6 +494,18 @@ from ..models.update_schedule_arm import UpdateScheduleArm from ..models.update_schedule_request import UpdateScheduleRequest from ..models.close_period_operation import ClosePeriodOperation +from ..models.preview_reconciliations_request import PreviewReconciliationsRequest +from ..models.preview_reconciliations_request_method import ( + PreviewReconciliationsRequestMethod, +) +from ..models.reconciliation_list_response import ReconciliationListResponse +from ..models.reconciliation_policy_response import ReconciliationPolicyResponse +from ..models.reconciliation_preview_response import ReconciliationPreviewResponse +from ..models.reconciliation_summary import ReconciliationSummary +from ..models.record_statement_balance_request import RecordStatementBalanceRequest +from ..models.refresh_reconciliations_request import RefreshReconciliationsRequest +from ..models.set_reconciliation_policy_request import SetReconciliationPolicyRequest +from ..models.sign_off_reconciliation_request import SignOffReconciliationRequest from ..models.compute_metrics_request import ComputeMetricsRequest from ..models.compute_metrics_response import ComputeMetricsResponse from ..models.create_view_request import CreateViewRequest @@ -1438,8 +1472,14 @@ def create_schedule( useful_life_months: int | None = None, asset_element_id: str | None = None, auto_reverse: bool = False, + booked_on: str | None = None, ) -> InformationBlockEnvelope: - """Create a new schedule with pre-generated monthly facts.""" + """Create a new schedule with pre-generated monthly facts. + + ``booked_on`` (``YYYY-MM-DD``) is the day the schedule's cost went on + the books, when that is before its first period. The schedule + reconciliation carries the cost from that day. + """ payload_dict: dict[str, Any] = { "name": name, "element_ids": element_ids, @@ -1467,6 +1507,8 @@ def create_schedule( schedule_metadata["useful_life_months"] = useful_life_months if asset_element_id: schedule_metadata["asset_element_id"] = asset_element_id + if booked_on is not None: + schedule_metadata["booked_on"] = booked_on if schedule_metadata: payload_dict["schedule_metadata"] = schedule_metadata @@ -2130,12 +2172,24 @@ def close_period( period: str, note: str | None = None, allow_stale_sync: bool | None = None, + allow_unreconciled_accounts: bool | None = None, ) -> ClosePeriodResponse: - """Close a fiscal period — the final commit action.""" + """Close a fiscal period — the final commit action. + + ``allow_unreconciled_accounts`` closes despite a reconciliation the + close waits on that is not reconciled for the period. Prefer + `refresh_reconciliations` and clearing what it reports; the override is + recorded in the close audit note. + """ body = ClosePeriodOperation( period=period, note=note if note is not None else UNSET, allow_stale_sync=(allow_stale_sync if allow_stale_sync is not None else UNSET), + allow_unreconciled_accounts=( + allow_unreconciled_accounts + if allow_unreconciled_accounts is not None + else UNSET + ), ) response = op_close_period(graph_id=graph_id, body=body, client=self._get_client()) envelope = self._call_op("Close period", response) @@ -2158,6 +2212,148 @@ def reopen_period( envelope = self._call_op("Reopen period", response) return self._typed_result("Reopen period", envelope, FiscalCalendarResponse) + # ── Reconciliations ───────────────────────────────────────────────── + + def list_reconciliations( + self, graph_id: str, period: str + ) -> ReconciliationList | None: + """Every reconciliation block's standing at a period end (``YYYY-MM``). + + A block not yet compared for the period reads ``not_started``. + Comparisons are recorded by `refresh_reconciliations` and + `record_statement_balance`. + """ + data = self._query(graph_id, LIST_LEDGER_RECONCILIATIONS_GQL, {"period": period}) + return ListLedgerReconciliations.model_validate(data).reconciliations + + def preview_reconciliations( + self, + graph_id: str, + period: str, + *, + method: PreviewReconciliationsRequestMethod | str | None = None, + include_tied: bool | None = None, + ) -> ReconciliationPreviewResponse: + """Compare the ledger with something outside it. Records nothing. + + ``method`` picks the check: ``source_ledger`` (the default, which needs a + connected QuickBooks ledger), ``schedule_register`` or ``statement``. + ``include_tied`` also returns the accounts that tie. + """ + body = PreviewReconciliationsRequest( + period=period, + method=( + PreviewReconciliationsRequestMethod(method) if method is not None else UNSET + ), + include_tied=include_tied if include_tied is not None else UNSET, + ) + response = op_preview_reconciliations( + graph_id=graph_id, body=body, client=self._get_client() + ) + envelope = self._call_op("Preview reconciliations", response) + return self._typed_result( + "Preview reconciliations", envelope, ReconciliationPreviewResponse + ) + + def refresh_reconciliations( + self, graph_id: str, period: str + ) -> ReconciliationListResponse: + """Run every reconciliation that applies at a period end and record each + result on its block. ``notes`` names any check that could not run.""" + body = RefreshReconciliationsRequest(period=period) + response = op_refresh_reconciliations( + graph_id=graph_id, body=body, client=self._get_client() + ) + envelope = self._call_op("Refresh reconciliations", response) + return self._typed_result( + "Refresh reconciliations", envelope, ReconciliationListResponse + ) + + def record_statement_balance( + self, + graph_id: str, + *, + element_id: str, + as_of: str | datetime.date, + balance: float, + document_id: str | None = None, + note: str | None = None, + ) -> ReconciliationSummary: + """Record a statement's ending balance for a balance-sheet account and + reconcile the account to it for the period the statement ends in. + + ``balance`` is as the statement shows it: a positive number in the + account's normal direction (money in a bank account, or the amount owed + on a loan or a card). ``as_of`` is the statement's ending date. + ``document_id`` names the statement itself, as a document already on + the graph. Writes no books. + """ + body = RecordStatementBalanceRequest( + element_id=element_id, + as_of=datetime.date.fromisoformat(as_of) if isinstance(as_of, str) else as_of, + balance=balance, + document_id=document_id if document_id is not None else UNSET, + note=note if note is not None else UNSET, + ) + response = op_record_statement_balance( + graph_id=graph_id, body=body, client=self._get_client() + ) + envelope = self._call_op("Record statement balance", response) + return self._typed_result( + "Record statement balance", envelope, ReconciliationSummary + ) + + def set_reconciliation_policy( + self, + graph_id: str, + structure_id: str, + *, + required_for_close: bool | None = None, + materiality: float | None = None, + review_required: bool | None = None, + separate_reviewer: bool | None = None, + ) -> ReconciliationPolicyResponse: + """Change how much the close cares about one reconciliation. An omitted + field keeps its value.""" + body = SetReconciliationPolicyRequest( + structure_id=structure_id, + required_for_close=( + required_for_close if required_for_close is not None else UNSET + ), + materiality=materiality if materiality is not None else UNSET, + review_required=review_required if review_required is not None else UNSET, + separate_reviewer=(separate_reviewer if separate_reviewer is not None else UNSET), + ) + response = op_set_reconciliation_policy( + graph_id=graph_id, body=body, client=self._get_client() + ) + envelope = self._call_op("Set reconciliation policy", response) + return self._typed_result( + "Set reconciliation policy", envelope, ReconciliationPolicyResponse + ) + + def sign_off_reconciliation( + self, + graph_id: str, + structure_id: str, + period: str, + note: str | None = None, + ) -> ReconciliationSummary: + """Sign off a reconciled period as its reviewer. Only a member of the + graph can; a later change to any balance lapses the sign-off.""" + body = SignOffReconciliationRequest( + structure_id=structure_id, + period=period, + note=note if note is not None else UNSET, + ) + response = op_sign_off_reconciliation( + graph_id=graph_id, body=body, client=self._get_client() + ) + envelope = self._call_op("Sign off reconciliation", response) + return self._typed_result( + "Sign off reconciliation", envelope, ReconciliationSummary + ) + # ── Reports ───────────────────────────────────────────────────────── def create_report( diff --git a/robosystems_client/graphql/generated/__init__.py b/robosystems_client/graphql/generated/__init__.py index abadf93..7679df5 100644 --- a/robosystems_client/graphql/generated/__init__.py +++ b/robosystems_client/graphql/generated/__init__.py @@ -269,6 +269,14 @@ ListLedgerPublishListsPublishListsPagination, ListLedgerPublishListsPublishListsPublishLists, ) +from .list_ledger_reconciliations import ( + ListLedgerReconciliations, + ListLedgerReconciliationsReconciliations, + ListLedgerReconciliationsReconciliationsReconciliations, + ListLedgerReconciliationsReconciliationsReconciliationsComponents, + ListLedgerReconciliationsReconciliationsReconciliationsDifferences, + ListLedgerReconciliationsReconciliationsReconciliationsDifferencesComponents, +) from .list_ledger_reports import ( ListLedgerReports, ListLedgerReportsReports, @@ -362,6 +370,7 @@ LIST_LEDGER_JOURNAL_ENTRIES_GQL, LIST_LEDGER_MAPPINGS_GQL, LIST_LEDGER_PUBLISH_LISTS_GQL, + LIST_LEDGER_RECONCILIATIONS_GQL, LIST_LEDGER_REPORTS_GQL, LIST_LEDGER_STRUCTURES_GQL, LIST_LEDGER_TAXONOMIES_GQL, @@ -572,6 +581,7 @@ "LIST_LEDGER_JOURNAL_ENTRIES_GQL", "LIST_LEDGER_MAPPINGS_GQL", "LIST_LEDGER_PUBLISH_LISTS_GQL", + "LIST_LEDGER_RECONCILIATIONS_GQL", "LIST_LEDGER_REPORTS_GQL", "LIST_LEDGER_STRUCTURES_GQL", "LIST_LEDGER_TAXONOMIES_GQL", @@ -644,6 +654,12 @@ "ListLedgerPublishListsPublishLists", "ListLedgerPublishListsPublishListsPagination", "ListLedgerPublishListsPublishListsPublishLists", + "ListLedgerReconciliations", + "ListLedgerReconciliationsReconciliations", + "ListLedgerReconciliationsReconciliationsReconciliations", + "ListLedgerReconciliationsReconciliationsReconciliationsComponents", + "ListLedgerReconciliationsReconciliationsReconciliationsDifferences", + "ListLedgerReconciliationsReconciliationsReconciliationsDifferencesComponents", "ListLedgerReports", "ListLedgerReportsReports", "ListLedgerReportsReportsReports", diff --git a/robosystems_client/graphql/generated/client.py b/robosystems_client/graphql/generated/client.py index f74c489..6af7dd9 100644 --- a/robosystems_client/graphql/generated/client.py +++ b/robosystems_client/graphql/generated/client.py @@ -48,6 +48,7 @@ from .list_ledger_journal_entries import ListLedgerJournalEntries from .list_ledger_mappings import ListLedgerMappings from .list_ledger_publish_lists import ListLedgerPublishLists +from .list_ledger_reconciliations import ListLedgerReconciliations from .list_ledger_reports import ListLedgerReports from .list_ledger_structures import ListLedgerStructures from .list_ledger_taxonomies import ListLedgerTaxonomies @@ -104,6 +105,7 @@ LIST_LEDGER_JOURNAL_ENTRIES_GQL, LIST_LEDGER_MAPPINGS_GQL, LIST_LEDGER_PUBLISH_LISTS_GQL, + LIST_LEDGER_RECONCILIATIONS_GQL, LIST_LEDGER_REPORTS_GQL, LIST_LEDGER_STRUCTURES_GQL, LIST_LEDGER_TAXONOMIES_GQL, @@ -768,6 +770,19 @@ def list_ledger_publish_lists( data = self.get_data(response) return ListLedgerPublishLists.model_validate(data) + def list_ledger_reconciliations( + self, period: str, **kwargs: Any + ) -> ListLedgerReconciliations: + variables: dict[str, object] = {"period": period} + response = self.execute( + query=LIST_LEDGER_RECONCILIATIONS_GQL, + operation_name="ListLedgerReconciliations", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return ListLedgerReconciliations.model_validate(data) + def list_ledger_reports( self, lifecycle: Union[Optional[ReportLifecycle], UnsetType] = UNSET, **kwargs: Any ) -> ListLedgerReports: diff --git a/robosystems_client/graphql/generated/get_ledger_fiscal_calendar.py b/robosystems_client/graphql/generated/get_ledger_fiscal_calendar.py index 9d58726..2a32b4e 100644 --- a/robosystems_client/graphql/generated/get_ledger_fiscal_calendar.py +++ b/robosystems_client/graphql/generated/get_ledger_fiscal_calendar.py @@ -31,6 +31,8 @@ class GetLedgerFiscalCalendarFiscalCalendar(BaseModel): ] = Field(alias="strandedObligationSample") reconciling_item_count: int = Field(alias="reconcilingItemCount") reconciling_item_sample: list[str] = Field(alias="reconcilingItemSample") + unreconciled_account_count: int = Field(alias="unreconciledAccountCount") + unreconciled_account_sample: list[str] = Field(alias="unreconciledAccountSample") sync_stale_days: Optional[int] = Field(alias="syncStaleDays") last_close_at: Optional[str] = Field(alias="lastCloseAt") initialized_at: Optional[str] = Field(alias="initializedAt") diff --git a/robosystems_client/graphql/generated/list_ledger_reconciliations.py b/robosystems_client/graphql/generated/list_ledger_reconciliations.py new file mode 100644 index 0000000..bcd1e82 --- /dev/null +++ b/robosystems_client/graphql/generated/list_ledger_reconciliations.py @@ -0,0 +1,91 @@ +from typing import Optional + +from pydantic import Field + +from .base_model import BaseModel + + +class ListLedgerReconciliations(BaseModel): + reconciliations: Optional["ListLedgerReconciliationsReconciliations"] + + +class ListLedgerReconciliationsReconciliations(BaseModel): + period: str + as_of: str = Field(alias="asOf") + notes: list[str] + reconciliations: list["ListLedgerReconciliationsReconciliationsReconciliations"] + + +class ListLedgerReconciliationsReconciliationsReconciliations(BaseModel): + structure_id: str = Field(alias="structureId") + name: str + scope: str + method: str + element_id: Optional[str] = Field(alias="elementId") + required_for_close: bool = Field(alias="requiredForClose") + materiality: float + period: str + as_of: str = Field(alias="asOf") + status: str + unreconciled_difference: Optional[float] = Field(alias="unreconciledDifference") + accounts_compared: Optional[int] = Field(alias="accountsCompared") + accounts_different: Optional[int] = Field(alias="accountsDifferent") + ledger_balance: Optional[float] = Field(alias="ledgerBalance") + independent_balance: Optional[float] = Field(alias="independentBalance") + balance_as_of: Optional[str] = Field(alias="balanceAsOf") + components: list["ListLedgerReconciliationsReconciliationsReconciliationsComponents"] + source: Optional[str] + compared_at: Optional[str] = Field(alias="comparedAt") + fact_set_id: Optional[str] = Field(alias="factSetId") + compared_by: Optional[str] = Field(alias="comparedBy") + compared_via: Optional[str] = Field(alias="comparedVia") + review_required: bool = Field(alias="reviewRequired") + separate_reviewer: bool = Field(alias="separateReviewer") + reviewed_by: Optional[str] = Field(alias="reviewedBy") + reviewed_at: Optional[str] = Field(alias="reviewedAt") + self_reviewed: Optional[bool] = Field(alias="selfReviewed") + differences: list[ + "ListLedgerReconciliationsReconciliationsReconciliationsDifferences" + ] + + +class ListLedgerReconciliationsReconciliationsReconciliationsComponents(BaseModel): + name: str + amount: float + structure_id: Optional[str] = Field(alias="structureId") + event_id: Optional[str] = Field(alias="eventId") + document_id: Optional[str] = Field(alias="documentId") + note: Optional[str] + + +class ListLedgerReconciliationsReconciliationsReconciliationsDifferences(BaseModel): + element_id: Optional[str] = Field(alias="elementId") + account_code: Optional[str] = Field(alias="accountCode") + account_name: str = Field(alias="accountName") + source_account_id: Optional[str] = Field(alias="sourceAccountId") + statement: Optional[str] + ledger_balance: float = Field(alias="ledgerBalance") + independent_balance: float = Field(alias="independentBalance") + difference: float + status: str + as_of: Optional[str] = Field(alias="asOf") + components: list[ + "ListLedgerReconciliationsReconciliationsReconciliationsDifferencesComponents" + ] + + +class ListLedgerReconciliationsReconciliationsReconciliationsDifferencesComponents( + BaseModel +): + name: str + amount: float + structure_id: Optional[str] = Field(alias="structureId") + event_id: Optional[str] = Field(alias="eventId") + document_id: Optional[str] = Field(alias="documentId") + note: Optional[str] + + +ListLedgerReconciliations.model_rebuild() +ListLedgerReconciliationsReconciliations.model_rebuild() +ListLedgerReconciliationsReconciliationsReconciliations.model_rebuild() +ListLedgerReconciliationsReconciliationsReconciliationsDifferences.model_rebuild() diff --git a/robosystems_client/graphql/generated/operations.py b/robosystems_client/graphql/generated/operations.py index 87c90aa..041f113 100644 --- a/robosystems_client/graphql/generated/operations.py +++ b/robosystems_client/graphql/generated/operations.py @@ -44,6 +44,7 @@ "LIST_LEDGER_JOURNAL_ENTRIES_GQL", "LIST_LEDGER_MAPPINGS_GQL", "LIST_LEDGER_PUBLISH_LISTS_GQL", + "LIST_LEDGER_RECONCILIATIONS_GQL", "LIST_LEDGER_REPORTS_GQL", "LIST_LEDGER_STRUCTURES_GQL", "LIST_LEDGER_TAXONOMIES_GQL", @@ -653,6 +654,8 @@ } reconcilingItemCount reconcilingItemSample + unreconciledAccountCount + unreconciledAccountSample syncStaleDays lastCloseAt initializedAt @@ -1592,6 +1595,72 @@ } """ +LIST_LEDGER_RECONCILIATIONS_GQL = """ +query ListLedgerReconciliations($period: String!) { + reconciliations(period: $period) { + period + asOf + notes + reconciliations { + structureId + name + scope + method + elementId + requiredForClose + materiality + period + asOf + status + unreconciledDifference + accountsCompared + accountsDifferent + ledgerBalance + independentBalance + balanceAsOf + components { + name + amount + structureId + eventId + documentId + note + } + source + comparedAt + factSetId + comparedBy + comparedVia + reviewRequired + separateReviewer + reviewedBy + reviewedAt + selfReviewed + differences { + elementId + accountCode + accountName + sourceAccountId + statement + ledgerBalance + independentBalance + difference + status + asOf + components { + name + amount + structureId + eventId + documentId + note + } + } + } + } +} +""" + LIST_LEDGER_REPORTS_GQL = """ query ListLedgerReports($lifecycle: ReportLifecycle) { reports(lifecycle: $lifecycle) { diff --git a/robosystems_client/graphql/operations/ledger/GetLedgerFiscalCalendar.graphql b/robosystems_client/graphql/operations/ledger/GetLedgerFiscalCalendar.graphql index d8c8b59..4c8a71b 100644 --- a/robosystems_client/graphql/operations/ledger/GetLedgerFiscalCalendar.graphql +++ b/robosystems_client/graphql/operations/ledger/GetLedgerFiscalCalendar.graphql @@ -8,6 +8,7 @@ query GetLedgerFiscalCalendar { strandedObligationCount strandedObligationSample { eventId scheduleId scheduleName period } reconcilingItemCount reconcilingItemSample + unreconciledAccountCount unreconciledAccountSample syncStaleDays lastCloseAt initializedAt lastSyncAt periods { name startDate endDate status closedAt } diff --git a/robosystems_client/graphql/operations/ledger/ListLedgerReconciliations.graphql b/robosystems_client/graphql/operations/ledger/ListLedgerReconciliations.graphql new file mode 100644 index 0000000..1a599f6 --- /dev/null +++ b/robosystems_client/graphql/operations/ledger/ListLedgerReconciliations.graphql @@ -0,0 +1,20 @@ +query ListLedgerReconciliations($period: String!) { + reconciliations(period: $period) { + period asOf notes + reconciliations { + structureId name scope method elementId + requiredForClose materiality + period asOf status unreconciledDifference + accountsCompared accountsDifferent + ledgerBalance independentBalance balanceAsOf + components { name amount structureId eventId documentId note } + source comparedAt factSetId comparedBy comparedVia + reviewRequired separateReviewer reviewedBy reviewedAt selfReviewed + differences { + elementId accountCode accountName sourceAccountId statement + ledgerBalance independentBalance difference status asOf + components { name amount structureId eventId documentId note } + } + } + } +} diff --git a/robosystems_client/graphql/schema.graphql b/robosystems_client/graphql/schema.graphql index de68b18..337e763 100644 --- a/robosystems_client/graphql/schema.graphql +++ b/robosystems_client/graphql/schema.graphql @@ -247,6 +247,17 @@ type Query { endDate: Date = null ): AccountRollups + """ + Every reconciliation's standing at a period end. + + A reconciliation not yet compared for the period is `not_started`. + Comparisons are recorded by the `refresh-reconciliations` operation. + """ + reconciliations( + """The period, as YYYY-MM.""" + period: String! + ): ReconciliationList + """Trial balance for posted entries in a date range.""" trialBalance( """Start of the posting window (inclusive).""" @@ -1601,6 +1612,212 @@ type AccountRollupRow { netBalance: Float! } +"""Every reconciliation's standing for one period.""" +type ReconciliationList { + """The period, as YYYY-MM.""" + period: String! + + """The period's last day.""" + asOf: Date! + + """One entry per reconciliation block, oldest block first.""" + reconciliations: [ReconciliationSummary!]! + + """ + What a refresh could not compare, such as a check skipped because its source is no longer connected. Empty on a plain read. + """ + notes: [String!]! +} + +"""One reconciliation's standing for a period.""" +type ReconciliationSummary { + """The reconciliation block.""" + structureId: String! + + """The block's name.""" + name: String! + + """ + `ledger`: the whole ledger against one source. `account`: one account against an independent balance. + """ + scope: String! + + """ + Where the independent side comes from. `source_ledger` is the synced accounting system's own trial balance. `schedule_register` is what the account's schedules say it carries. `statement` is the ending balance of a statement recorded for the account. + """ + method: String! + + """The account reconciled; null for a ledger-scope block.""" + elementId: String + + """Whether the period's close waits on this reconciliation.""" + requiredForClose: Boolean! + + """A difference up to this amount still counts as reconciled.""" + materiality: Float! + + """The period, as YYYY-MM.""" + period: String! + + """The period's last day.""" + asOf: Date! + + """ + `not_started`: not compared for this period. `unreconciled`: the sides differ by more than the materiality. `explained`: they differ and items account for all of it. `reconciled`: nothing is left unexplained. `reviewed`: reconciled and signed off. + """ + status: String! + + """ + What is left unexplained at the last comparison; null when the period has not been compared. For a ledger-scope block, the sum of every account's absolute difference. For an account-scope block, the ledger balance minus the independent one. + """ + unreconciledDifference: Float + + """Ledger-scope only: accounts with a balance on either side.""" + accountsCompared: Int + + """Ledger-scope only: accounts that do not tie.""" + accountsDifferent: Int + + """ + Account-scope only: the account's balance at the last comparison, debit-positive, as the period's close will leave it. + """ + ledgerBalance: Float + + """ + Account-scope only: what the independent source said at the last comparison, debit-positive. + """ + independentBalance: Float + + """ + Account-scope only: the date the two balances are stated at. The period's last day, unless a statement ended earlier in the period. + """ + balanceAsOf: Date + + """ + Account-scope only: what makes up the independent balance. One entry per schedule for `schedule_register`; the recorded statement for `statement`. + """ + components: [ReconciliationComponent!]! + + """The system the independent side was read from.""" + source: String + + """When the two sides were last compared.""" + comparedAt: DateTime + + """The FactSet holding the period's comparison.""" + factSetId: String + + """The user whose action ran the last comparison.""" + comparedBy: String + + """ + `operation` when someone ran refresh-reconciliations; `sync` when a source sync refreshed it. + """ + comparedVia: String + + """Whether the close also waits for a sign-off.""" + reviewRequired: Boolean! + + """ + Whether the reviewer must be someone other than the person who ran the comparison. + """ + separateReviewer: Boolean! + + """ + The user who signed off the comparison as it stands. Null when nobody has, or when the balances changed after the sign-off. + """ + reviewedBy: String + + """When the standing sign-off was made.""" + reviewedAt: DateTime + + """ + True when the reviewer is the person who ran the comparison they signed off. Null when there is no standing sign-off. + """ + selfReviewed: Boolean + + """ + Ledger-scope only: the accounts that did not tie at the last comparison, largest difference first. + """ + differences: [ReconciliationRow!]! +} + +""" +One part of an account's independent balance: what a single schedule +says the account carries, or a recorded statement balance. +""" +type ReconciliationComponent { + """The schedule's name, or the statement and its date.""" + name: String! + + """What this part says the account holds, debit-positive.""" + amount: Float! + + """The schedule, for a `schedule_register` part.""" + structureId: String + + """The recorded balance, for a `statement` part.""" + eventId: String + + """The statement document given as evidence, when one was.""" + documentId: String + + """ + Why a schedule carries nothing (disposed of, or ended early), or the note recorded with a statement balance. + """ + note: String +} + +""" +One account: the ledger's balance, the independent balance, the difference. + +Balances are debit-positive, so a credit balance is negative on both sides. +""" +type ReconciliationRow { + """The chart account; null when the ledger has none for it.""" + elementId: String + + """The chart account's code.""" + accountCode: String + + """The account's name in the ledger, else in the source.""" + accountName: String! + + """The account's id in the source system, when it has one.""" + sourceAccountId: String + + """ + `balance_sheet` or `income_statement`. Balance-sheet accounts are compared cumulatively to the period end; income-statement accounts from the start of the fiscal year. Null when the ledger has no account. + """ + statement: String + + """ + What the ledger holds. For `source_ledger`, landed entries only. For `schedule_register`, the balance as the period's close will leave it: landed entries, drafts awaiting the close, and schedule entries not yet drafted. + """ + ledgerBalance: Float! + + """What the independent source says.""" + independentBalance: Float! + + """Ledger minus independent.""" + difference: Float! + + """ + `tied`: both sides agree to the cent. `different`: both know the account and disagree. `not_in_ledger`: the source reports an account the ledger has none for. `not_in_source`: the ledger holds a balance on an account the source does not have. + """ + status: String! + + """ + The date both balances are stated at, when it is not the period's last day: a statement that ends mid-period is compared with the ledger at the statement's own date. + """ + asOf: Date + + """ + Account-scope methods only: what makes up the independent balance. One entry per schedule for `schedule_register`; the recorded statement for `statement`. + """ + components: [ReconciliationComponent!]! +} + """ Trial balance for posted entries in a date range — every CoA account that had activity, plus aggregate totals. @@ -2155,6 +2372,16 @@ type FiscalCalendar { Source identifiers (or event ids) of up to 5 unposted source events, so the blocker names what is holding the close. """ unpostedSourceEventSample: [String!]! + + """ + Reconciliations the close waits on that are not reconciled for this period, including any never compared for it. Run refresh-reconciliations and clear what it reports, release one with set-reconciliation-policy, or close over them knowingly with allow_unreconciled_accounts. + """ + unreconciledAccountCount: Int! + + """ + Name and status of up to 5 of those reconciliations, so the blocker names what is holding the close. + """ + unreconciledAccountSample: [String!]! lastCloseAt: DateTime initializedAt: DateTime @@ -2313,9 +2540,10 @@ One row in the closing book — a navigable artifact for the period (statement, schedule, rollup, etc.). `itemType` discriminates: 'statement', 'schedule', -'account_rollups', 'period_close', 'trial_balance'. Statement items -carry `reportId` to fetch the rendered facts; schedule items -carry `status` ('complete' | 'draft' | 'pending'). +'account_rollups', 'period_close', 'reconciliations', 'trial_balance'. +Statement items carry `reportId` to fetch the rendered facts; schedule +items carry `status` ('complete' | 'draft' | 'pending'). The single +'reconciliations' item opens the period's reconciliation worklist. """ type ClosingBookItem { id: String! diff --git a/robosystems_client/models/__init__.py b/robosystems_client/models/__init__.py index 8c5f64e..fd90d21 100644 --- a/robosystems_client/models/__init__.py +++ b/robosystems_client/models/__init__.py @@ -622,6 +622,30 @@ from .operation_envelope_publish_list_response_status import ( OperationEnvelopePublishListResponseStatus, ) +from .operation_envelope_reconciliation_list_response import ( + OperationEnvelopeReconciliationListResponse, +) +from .operation_envelope_reconciliation_list_response_status import ( + OperationEnvelopeReconciliationListResponseStatus, +) +from .operation_envelope_reconciliation_policy_response import ( + OperationEnvelopeReconciliationPolicyResponse, +) +from .operation_envelope_reconciliation_policy_response_status import ( + OperationEnvelopeReconciliationPolicyResponseStatus, +) +from .operation_envelope_reconciliation_preview_response import ( + OperationEnvelopeReconciliationPreviewResponse, +) +from .operation_envelope_reconciliation_preview_response_status import ( + OperationEnvelopeReconciliationPreviewResponseStatus, +) +from .operation_envelope_reconciliation_summary import ( + OperationEnvelopeReconciliationSummary, +) +from .operation_envelope_reconciliation_summary_status import ( + OperationEnvelopeReconciliationSummaryStatus, +) from .operation_envelope_reconciling_item_plan import ( OperationEnvelopeReconcilingItemPlan, ) @@ -757,6 +781,8 @@ from .preview_event_block_response_handler_metadata import ( PreviewEventBlockResponseHandlerMetadata, ) +from .preview_reconciliations_request import PreviewReconciliationsRequest +from .preview_reconciliations_request_method import PreviewReconciliationsRequestMethod from .preview_reconciling_item_request import PreviewReconcilingItemRequest from .promote_obligations_request import PromoteObligationsRequest from .promote_obligations_response import PromoteObligationsResponse @@ -769,6 +795,16 @@ from .quick_books_connection_config import QuickBooksConnectionConfig from .rate_limits import RateLimits from .rebuild_schedule_request import RebuildScheduleRequest +from .reconciliation_component import ReconciliationComponent +from .reconciliation_list_response import ReconciliationListResponse +from .reconciliation_mechanics import ReconciliationMechanics +from .reconciliation_mechanics_method import ReconciliationMechanicsMethod +from .reconciliation_mechanics_scope import ReconciliationMechanicsScope +from .reconciliation_policy_response import ReconciliationPolicyResponse +from .reconciliation_preview_response import ReconciliationPreviewResponse +from .reconciliation_preview_response_method import ReconciliationPreviewResponseMethod +from .reconciliation_row import ReconciliationRow +from .reconciliation_summary import ReconciliationSummary from .reconciling_item_catch_up import ReconcilingItemCatchUp from .reconciling_item_delta_line import ReconcilingItemDeltaLine from .reconciling_item_entry_summary import ReconcilingItemEntrySummary @@ -777,9 +813,11 @@ ReconcilingItemPlanDefaultDisposition, ) from .reconciling_item_regenerated import ReconcilingItemRegenerated +from .record_statement_balance_request import RecordStatementBalanceRequest from .recovery_codes_request import RecoveryCodesRequest from .recovery_codes_request_assertion_type_0 import RecoveryCodesRequestAssertionType0 from .recovery_codes_response import RecoveryCodesResponse +from .refresh_reconciliations_request import RefreshReconciliationsRequest from .regenerate_report_operation import RegenerateReportOperation from .register_request import RegisterRequest from .remember_op import RememberOp @@ -851,11 +889,13 @@ from .service_offering_summary import ServiceOfferingSummary from .service_offerings_response import ServiceOfferingsResponse from .set_close_target_operation import SetCloseTargetOperation +from .set_reconciliation_policy_request import SetReconciliationPolicyRequest from .set_write_policy_request import SetWritePolicyRequest from .set_write_policy_request_write_policy import SetWritePolicyRequestWritePolicy from .share_report_operation import ShareReportOperation from .share_report_response import ShareReportResponse from .share_result_item import ShareResultItem +from .sign_off_reconciliation_request import SignOffReconciliationRequest from .skipped_forecast_lite import SkippedForecastLite from .skipped_metric_lite import SkippedMetricLite from .sql_statement_request import SqlStatementRequest @@ -1427,6 +1467,14 @@ "OperationEnvelopePromoteObligationsResponseStatus", "OperationEnvelopePublishListResponse", "OperationEnvelopePublishListResponseStatus", + "OperationEnvelopeReconciliationListResponse", + "OperationEnvelopeReconciliationListResponseStatus", + "OperationEnvelopeReconciliationPolicyResponse", + "OperationEnvelopeReconciliationPolicyResponseStatus", + "OperationEnvelopeReconciliationPreviewResponse", + "OperationEnvelopeReconciliationPreviewResponseStatus", + "OperationEnvelopeReconciliationSummary", + "OperationEnvelopeReconciliationSummaryStatus", "OperationEnvelopeReconcilingItemPlan", "OperationEnvelopeReconcilingItemPlanStatus", "OperationEnvelopeReportResponse", @@ -1516,6 +1564,8 @@ "PositionBlock", "PreviewEventBlockResponse", "PreviewEventBlockResponseHandlerMetadata", + "PreviewReconciliationsRequest", + "PreviewReconciliationsRequestMethod", "PreviewReconcilingItemRequest", "PromoteObligationsRequest", "PromoteObligationsResponse", @@ -1526,15 +1576,27 @@ "QuickBooksConnectionConfig", "RateLimits", "RebuildScheduleRequest", + "ReconciliationComponent", + "ReconciliationListResponse", + "ReconciliationMechanics", + "ReconciliationMechanicsMethod", + "ReconciliationMechanicsScope", + "ReconciliationPolicyResponse", + "ReconciliationPreviewResponse", + "ReconciliationPreviewResponseMethod", + "ReconciliationRow", + "ReconciliationSummary", "ReconcilingItemCatchUp", "ReconcilingItemDeltaLine", "ReconcilingItemEntrySummary", "ReconcilingItemPlan", "ReconcilingItemPlanDefaultDisposition", "ReconcilingItemRegenerated", + "RecordStatementBalanceRequest", "RecoveryCodesRequest", "RecoveryCodesRequestAssertionType0", "RecoveryCodesResponse", + "RefreshReconciliationsRequest", "RegenerateReportOperation", "RegisterRequest", "RememberOp", @@ -1590,11 +1652,13 @@ "ServiceOfferingsResponse", "ServiceOfferingSummary", "SetCloseTargetOperation", + "SetReconciliationPolicyRequest", "SetWritePolicyRequest", "SetWritePolicyRequestWritePolicy", "ShareReportOperation", "ShareReportResponse", "ShareResultItem", + "SignOffReconciliationRequest", "SkippedForecastLite", "SkippedMetricLite", "SqlStatementRequest", diff --git a/robosystems_client/models/artifact_response.py b/robosystems_client/models/artifact_response.py index 13f9b16..8936c59 100644 --- a/robosystems_client/models/artifact_response.py +++ b/robosystems_client/models/artifact_response.py @@ -12,6 +12,7 @@ from ..models.artifact_response_template_type_0 import ArtifactResponseTemplateType0 from ..models.forecast_mechanics import ForecastMechanics from ..models.metric_mechanics import MetricMechanics + from ..models.reconciliation_mechanics import ReconciliationMechanics from ..models.rollforward_mechanics import RollforwardMechanics from ..models.schedule_mechanics import ScheduleMechanics from ..models.statement_mechanics import StatementMechanics @@ -25,7 +26,8 @@ class ArtifactResponse: """The block's producible-artifact envelope — topic, template, mechanics. Attributes: - mechanics (ForecastMechanics | MetricMechanics | RollforwardMechanics | ScheduleMechanics | StatementMechanics): + mechanics (ForecastMechanics | MetricMechanics | ReconciliationMechanics | RollforwardMechanics | + ScheduleMechanics | StatementMechanics): topic (None | str | Unset): Structure.description — the block's human-readable topic. renderer_note (None | str | Unset): e.g. 'in thousands', 'except per share'. template (ArtifactResponseTemplateType0 | None | Unset): Reusable layout (ordering, subtotals, styling) when @@ -36,6 +38,7 @@ class ArtifactResponse: mechanics: ( ForecastMechanics | MetricMechanics + | ReconciliationMechanics | RollforwardMechanics | ScheduleMechanics | StatementMechanics @@ -47,6 +50,7 @@ class ArtifactResponse: def to_dict(self) -> dict[str, Any]: from ..models.artifact_response_template_type_0 import ArtifactResponseTemplateType0 + from ..models.forecast_mechanics import ForecastMechanics from ..models.metric_mechanics import MetricMechanics from ..models.rollforward_mechanics import RollforwardMechanics from ..models.schedule_mechanics import ScheduleMechanics @@ -61,6 +65,8 @@ def to_dict(self) -> dict[str, Any]: mechanics = self.mechanics.to_dict() elif isinstance(self.mechanics, RollforwardMechanics): mechanics = self.mechanics.to_dict() + elif isinstance(self.mechanics, ForecastMechanics): + mechanics = self.mechanics.to_dict() else: mechanics = self.mechanics.to_dict() @@ -105,6 +111,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.artifact_response_template_type_0 import ArtifactResponseTemplateType0 from ..models.forecast_mechanics import ForecastMechanics from ..models.metric_mechanics import MetricMechanics + from ..models.reconciliation_mechanics import ReconciliationMechanics from ..models.rollforward_mechanics import RollforwardMechanics from ..models.schedule_mechanics import ScheduleMechanics from ..models.statement_mechanics import StatementMechanics @@ -116,6 +123,7 @@ def _parse_mechanics( ) -> ( ForecastMechanics | MetricMechanics + | ReconciliationMechanics | RollforwardMechanics | ScheduleMechanics | StatementMechanics @@ -152,11 +160,19 @@ def _parse_mechanics( return mechanics_type_3 except (TypeError, ValueError, AttributeError, KeyError): pass + try: + if not isinstance(data, dict): + raise TypeError() + mechanics_type_4 = ForecastMechanics.from_dict(data) + + return mechanics_type_4 + except (TypeError, ValueError, AttributeError, KeyError): + pass if not isinstance(data, dict): raise TypeError() - mechanics_type_4 = ForecastMechanics.from_dict(data) + mechanics_type_5 = ReconciliationMechanics.from_dict(data) - return mechanics_type_4 + return mechanics_type_5 mechanics = _parse_mechanics(d.pop("mechanics")) diff --git a/robosystems_client/models/backfill_plan_history_operation.py b/robosystems_client/models/backfill_plan_history_operation.py index 898c5fe..aa0d83c 100644 --- a/robosystems_client/models/backfill_plan_history_operation.py +++ b/robosystems_client/models/backfill_plan_history_operation.py @@ -31,6 +31,9 @@ class BackfillPlanHistoryOperation: allow_unposted_source_events (bool | Unset): Override the unposted-source-event gate on each reclose. Only needed when a source event inside the backfill window was never committed and you have decided not to commit or void it first. Default: False. + allow_unreconciled_accounts (bool | Unset): Override the reconciliation gate on each reclose. Needed when a + month inside the backfill window has a reconciliation the close waits on that was never compared for it, which + is every month closed before the reconciliation existed. Default: False. restamp (bool | Unset): Also re-derive months that ALREADY have canonical statement sets (default: skip them). Use after an engine improvement changes what a stamp produces — each month reruns the full reopen → reclose cycle and replaces its sets. A restamp run is not self-resuming (every month in range stays a candidate); @@ -44,6 +47,7 @@ class BackfillPlanHistoryOperation: allow_stranded_obligations: bool | Unset = False allow_reconciling_items: bool | Unset = False allow_unposted_source_events: bool | Unset = False + allow_unreconciled_accounts: bool | Unset = False restamp: bool | Unset = False note: None | str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -65,6 +69,8 @@ def to_dict(self) -> dict[str, Any]: allow_unposted_source_events = self.allow_unposted_source_events + allow_unreconciled_accounts = self.allow_unreconciled_accounts + restamp = self.restamp note: None | str | Unset @@ -88,6 +94,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["allow_reconciling_items"] = allow_reconciling_items if allow_unposted_source_events is not UNSET: field_dict["allow_unposted_source_events"] = allow_unposted_source_events + if allow_unreconciled_accounts is not UNSET: + field_dict["allow_unreconciled_accounts"] = allow_unreconciled_accounts if restamp is not UNSET: field_dict["restamp"] = restamp if note is not UNSET: @@ -118,6 +126,8 @@ def _parse_start_period(data: object) -> None | str | Unset: allow_unposted_source_events = d.pop("allow_unposted_source_events", UNSET) + allow_unreconciled_accounts = d.pop("allow_unreconciled_accounts", UNSET) + restamp = d.pop("restamp", UNSET) def _parse_note(data: object) -> None | str | Unset: @@ -136,6 +146,7 @@ def _parse_note(data: object) -> None | str | Unset: allow_stranded_obligations=allow_stranded_obligations, allow_reconciling_items=allow_reconciling_items, allow_unposted_source_events=allow_unposted_source_events, + allow_unreconciled_accounts=allow_unreconciled_accounts, restamp=restamp, note=note, ) diff --git a/robosystems_client/models/close_period_operation.py b/robosystems_client/models/close_period_operation.py index df32eed..3b1f348 100644 --- a/robosystems_client/models/close_period_operation.py +++ b/robosystems_client/models/close_period_operation.py @@ -35,6 +35,10 @@ class ClosePeriodOperation: committed. Once the period closes they cannot post into it, so the stamped statements leave them out. Prefer classifying and committing, or voiding, each first. The override is recorded in the close audit note. Default: False. + allow_unreconciled_accounts (bool | Unset): Override the reconciliation gate — close even though a + reconciliation the close waits on is not reconciled for the period, or was never compared for it. The books are + then closed without that check. Prefer refresh-reconciliations and clearing what it reports first. The override + is recorded in the close audit note. Default: False. """ period: str @@ -43,6 +47,7 @@ class ClosePeriodOperation: allow_stranded_obligations: bool | Unset = False allow_reconciling_items: bool | Unset = False allow_unposted_source_events: bool | Unset = False + allow_unreconciled_accounts: bool | Unset = False additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -62,6 +67,8 @@ def to_dict(self) -> dict[str, Any]: allow_unposted_source_events = self.allow_unposted_source_events + allow_unreconciled_accounts = self.allow_unreconciled_accounts + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -79,6 +86,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["allow_reconciling_items"] = allow_reconciling_items if allow_unposted_source_events is not UNSET: field_dict["allow_unposted_source_events"] = allow_unposted_source_events + if allow_unreconciled_accounts is not UNSET: + field_dict["allow_unreconciled_accounts"] = allow_unreconciled_accounts return field_dict @@ -104,6 +113,8 @@ def _parse_note(data: object) -> None | str | Unset: allow_unposted_source_events = d.pop("allow_unposted_source_events", UNSET) + allow_unreconciled_accounts = d.pop("allow_unreconciled_accounts", UNSET) + close_period_operation = cls( period=period, note=note, @@ -111,6 +122,7 @@ def _parse_note(data: object) -> None | str | Unset: allow_stranded_obligations=allow_stranded_obligations, allow_reconciling_items=allow_reconciling_items, allow_unposted_source_events=allow_unposted_source_events, + allow_unreconciled_accounts=allow_unreconciled_accounts, ) close_period_operation.additional_properties = d diff --git a/robosystems_client/models/create_legacy_arm_block_type.py b/robosystems_client/models/create_legacy_arm_block_type.py index 7228d9b..7b330db 100644 --- a/robosystems_client/models/create_legacy_arm_block_type.py +++ b/robosystems_client/models/create_legacy_arm_block_type.py @@ -8,6 +8,7 @@ class CreateLegacyArmBlockType(str, Enum): EQUITY_STATEMENT = "equity_statement" INCOME_STATEMENT = "income_statement" METRIC = "metric" + RECONCILIATION = "reconciliation" REGULATORY_DISCLOSURE = "regulatory_disclosure" def __str__(self) -> str: diff --git a/robosystems_client/models/delete_legacy_arm_block_type.py b/robosystems_client/models/delete_legacy_arm_block_type.py index c382178..d7332ee 100644 --- a/robosystems_client/models/delete_legacy_arm_block_type.py +++ b/robosystems_client/models/delete_legacy_arm_block_type.py @@ -8,6 +8,7 @@ class DeleteLegacyArmBlockType(str, Enum): EQUITY_STATEMENT = "equity_statement" INCOME_STATEMENT = "income_statement" METRIC = "metric" + RECONCILIATION = "reconciliation" REGULATORY_DISCLOSURE = "regulatory_disclosure" def __str__(self) -> str: diff --git a/robosystems_client/models/delete_schedule_arm.py b/robosystems_client/models/delete_schedule_arm.py index 9ec2cf3..55f2cfc 100644 --- a/robosystems_client/models/delete_schedule_arm.py +++ b/robosystems_client/models/delete_schedule_arm.py @@ -21,15 +21,14 @@ class DeleteScheduleArm: Attributes: block_type (Literal['schedule']): Discriminator value selecting this arm. - payload (DeleteScheduleRequest): Delete a schedule — cascades through facts and associations. - - Hard deletes the Structure, all Facts tied to it, and all - Associations tied to it. This is a permanent, irreversible - operation. For ending a schedule early without removing history, use - `terminate-schedule` (no entry) or - `create-event-block(event_type='asset_disposed')` (the handler voids - the remaining obligation chain + posts the disposal entry atomically; - recognized facts stay as history). + payload (DeleteScheduleRequest): Delete a schedule and everything under it. Permanent and irreversible. + + Removes the Structure, its Facts and Associations, its draft entries and + its unposted obligations. Refused once any of the schedule's entries has + posted, because the schedule is their support. To end a schedule early + and keep its history, use `terminate-schedule` (no entry) or + `create-event-block(event_type='asset_disposed')`, whose handler voids + the remaining obligations and posts the disposal entry in one step. """ block_type: Literal["schedule"] diff --git a/robosystems_client/models/delete_schedule_request.py b/robosystems_client/models/delete_schedule_request.py index 1f10619..f99b34b 100644 --- a/robosystems_client/models/delete_schedule_request.py +++ b/robosystems_client/models/delete_schedule_request.py @@ -11,15 +11,14 @@ @_attrs_define class DeleteScheduleRequest: - """Delete a schedule — cascades through facts and associations. - - Hard deletes the Structure, all Facts tied to it, and all - Associations tied to it. This is a permanent, irreversible - operation. For ending a schedule early without removing history, use - `terminate-schedule` (no entry) or - `create-event-block(event_type='asset_disposed')` (the handler voids - the remaining obligation chain + posts the disposal entry atomically; - recognized facts stay as history). + """Delete a schedule and everything under it. Permanent and irreversible. + + Removes the Structure, its Facts and Associations, its draft entries and + its unposted obligations. Refused once any of the schedule's entries has + posted, because the schedule is their support. To end a schedule early + and keep its history, use `terminate-schedule` (no entry) or + `create-event-block(event_type='asset_disposed')`, whose handler voids + the remaining obligations and posts the disposal entry in one step. Attributes: structure_id (str): diff --git a/robosystems_client/models/fiscal_calendar_response.py b/robosystems_client/models/fiscal_calendar_response.py index c94a7a9..c51e098 100644 --- a/robosystems_client/models/fiscal_calendar_response.py +++ b/robosystems_client/models/fiscal_calendar_response.py @@ -62,6 +62,11 @@ class FiscalCalendarResponse: void each, or close over them knowingly with allow_unposted_source_events. Default: 0. unposted_source_event_sample (list[str] | Unset): Source identifiers (or event ids) of up to 5 unposted source events, so the blocker names what is holding the close. + unreconciled_account_count (int | Unset): Reconciliations the close waits on that are not reconciled for this + period, including any never compared for it. Run refresh-reconciliations and clear what it reports, release one + with set-reconciliation-policy, or close over them knowingly with allow_unreconciled_accounts. Default: 0. + unreconciled_account_sample (list[str] | Unset): Name and status of up to 5 of those reconciliations, so the + blocker names what is holding the close. last_close_at (datetime.datetime | None | Unset): initialized_at (datetime.datetime | None | Unset): last_sync_at (datetime.datetime | None | Unset): Most recent QB sync timestamp (if connected) @@ -86,6 +91,8 @@ class FiscalCalendarResponse: reconciling_item_sample: list[str] | Unset = UNSET unposted_source_event_count: int | Unset = 0 unposted_source_event_sample: list[str] | Unset = UNSET + unreconciled_account_count: int | Unset = 0 + unreconciled_account_sample: list[str] | Unset = UNSET last_close_at: datetime.datetime | None | Unset = UNSET initialized_at: datetime.datetime | None | Unset = UNSET last_sync_at: datetime.datetime | None | Unset = UNSET @@ -163,6 +170,12 @@ def to_dict(self) -> dict[str, Any]: if not isinstance(self.unposted_source_event_sample, Unset): unposted_source_event_sample = self.unposted_source_event_sample + unreconciled_account_count = self.unreconciled_account_count + + unreconciled_account_sample: list[str] | Unset = UNSET + if not isinstance(self.unreconciled_account_sample, Unset): + unreconciled_account_sample = self.unreconciled_account_sample + last_close_at: None | str | Unset if isinstance(self.last_close_at, Unset): last_close_at = UNSET @@ -234,6 +247,10 @@ def to_dict(self) -> dict[str, Any]: field_dict["unposted_source_event_count"] = unposted_source_event_count if unposted_source_event_sample is not UNSET: field_dict["unposted_source_event_sample"] = unposted_source_event_sample + if unreconciled_account_count is not UNSET: + field_dict["unreconciled_account_count"] = unreconciled_account_count + if unreconciled_account_sample is not UNSET: + field_dict["unreconciled_account_sample"] = unreconciled_account_sample if last_close_at is not UNSET: field_dict["last_close_at"] = last_close_at if initialized_at is not UNSET: @@ -339,6 +356,12 @@ def _parse_sync_stale_days(data: object) -> int | None | Unset: list[str], d.pop("unposted_source_event_sample", UNSET) ) + unreconciled_account_count = d.pop("unreconciled_account_count", UNSET) + + unreconciled_account_sample = cast( + list[str], d.pop("unreconciled_account_sample", UNSET) + ) + def _parse_last_close_at(data: object) -> datetime.datetime | None | Unset: if data is None: return data @@ -418,6 +441,8 @@ def _parse_last_sync_at(data: object) -> datetime.datetime | None | Unset: reconciling_item_sample=reconciling_item_sample, unposted_source_event_count=unposted_source_event_count, unposted_source_event_sample=unposted_source_event_sample, + unreconciled_account_count=unreconciled_account_count, + unreconciled_account_sample=unreconciled_account_sample, last_close_at=last_close_at, initialized_at=initialized_at, last_sync_at=last_sync_at, diff --git a/robosystems_client/models/operation_envelope_reconciliation_list_response.py b/robosystems_client/models/operation_envelope_reconciliation_list_response.py new file mode 100644 index 0000000..c7a73e8 --- /dev/null +++ b/robosystems_client/models/operation_envelope_reconciliation_list_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_reconciliation_list_response_status import ( + OperationEnvelopeReconciliationListResponseStatus, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.reconciliation_list_response import ReconciliationListResponse + + +T = TypeVar("T", bound="OperationEnvelopeReconciliationListResponse") + + +@_attrs_define +class OperationEnvelopeReconciliationListResponse: + """ + Attributes: + operation (str): Kebab-case operation name + operation_id (str): op_-prefixed ULID for audit and SSE correlation + status (OperationEnvelopeReconciliationListResponseStatus): Operation lifecycle state + at (str): ISO-8601 UTC timestamp + result (None | ReconciliationListResponse | 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: OperationEnvelopeReconciliationListResponseStatus + at: str + result: None | ReconciliationListResponse | 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.reconciliation_list_response import ReconciliationListResponse + + 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, ReconciliationListResponse): + 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.reconciliation_list_response import ReconciliationListResponse + + d = dict(src_dict) + operation = d.pop("operation") + + operation_id = d.pop("operationId") + + status = OperationEnvelopeReconciliationListResponseStatus(d.pop("status")) + + at = d.pop("at") + + def _parse_result(data: object) -> None | ReconciliationListResponse | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + result_type_0 = ReconciliationListResponse.from_dict(data) + + return result_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | ReconciliationListResponse | 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_reconciliation_list_response = cls( + operation=operation, + operation_id=operation_id, + status=status, + at=at, + result=result, + created_by=created_by, + idempotent_replay=idempotent_replay, + ) + + operation_envelope_reconciliation_list_response.additional_properties = d + return operation_envelope_reconciliation_list_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_reconciliation_list_response_status.py b/robosystems_client/models/operation_envelope_reconciliation_list_response_status.py new file mode 100644 index 0000000..d5ca2c5 --- /dev/null +++ b/robosystems_client/models/operation_envelope_reconciliation_list_response_status.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class OperationEnvelopeReconciliationListResponseStatus(str, Enum): + COMPLETED = "completed" + FAILED = "failed" + PENDING = "pending" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/operation_envelope_reconciliation_policy_response.py b/robosystems_client/models/operation_envelope_reconciliation_policy_response.py new file mode 100644 index 0000000..ecab703 --- /dev/null +++ b/robosystems_client/models/operation_envelope_reconciliation_policy_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_reconciliation_policy_response_status import ( + OperationEnvelopeReconciliationPolicyResponseStatus, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.reconciliation_policy_response import ReconciliationPolicyResponse + + +T = TypeVar("T", bound="OperationEnvelopeReconciliationPolicyResponse") + + +@_attrs_define +class OperationEnvelopeReconciliationPolicyResponse: + """ + Attributes: + operation (str): Kebab-case operation name + operation_id (str): op_-prefixed ULID for audit and SSE correlation + status (OperationEnvelopeReconciliationPolicyResponseStatus): Operation lifecycle state + at (str): ISO-8601 UTC timestamp + result (None | ReconciliationPolicyResponse | 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: OperationEnvelopeReconciliationPolicyResponseStatus + at: str + result: None | ReconciliationPolicyResponse | 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.reconciliation_policy_response import ReconciliationPolicyResponse + + 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, ReconciliationPolicyResponse): + 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.reconciliation_policy_response import ReconciliationPolicyResponse + + d = dict(src_dict) + operation = d.pop("operation") + + operation_id = d.pop("operationId") + + status = OperationEnvelopeReconciliationPolicyResponseStatus(d.pop("status")) + + at = d.pop("at") + + def _parse_result(data: object) -> None | ReconciliationPolicyResponse | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + result_type_0 = ReconciliationPolicyResponse.from_dict(data) + + return result_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | ReconciliationPolicyResponse | 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_reconciliation_policy_response = cls( + operation=operation, + operation_id=operation_id, + status=status, + at=at, + result=result, + created_by=created_by, + idempotent_replay=idempotent_replay, + ) + + operation_envelope_reconciliation_policy_response.additional_properties = d + return operation_envelope_reconciliation_policy_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_reconciliation_policy_response_status.py b/robosystems_client/models/operation_envelope_reconciliation_policy_response_status.py new file mode 100644 index 0000000..125cadf --- /dev/null +++ b/robosystems_client/models/operation_envelope_reconciliation_policy_response_status.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class OperationEnvelopeReconciliationPolicyResponseStatus(str, Enum): + COMPLETED = "completed" + FAILED = "failed" + PENDING = "pending" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/operation_envelope_reconciliation_preview_response.py b/robosystems_client/models/operation_envelope_reconciliation_preview_response.py new file mode 100644 index 0000000..82c74da --- /dev/null +++ b/robosystems_client/models/operation_envelope_reconciliation_preview_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_reconciliation_preview_response_status import ( + OperationEnvelopeReconciliationPreviewResponseStatus, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.reconciliation_preview_response import ReconciliationPreviewResponse + + +T = TypeVar("T", bound="OperationEnvelopeReconciliationPreviewResponse") + + +@_attrs_define +class OperationEnvelopeReconciliationPreviewResponse: + """ + Attributes: + operation (str): Kebab-case operation name + operation_id (str): op_-prefixed ULID for audit and SSE correlation + status (OperationEnvelopeReconciliationPreviewResponseStatus): Operation lifecycle state + at (str): ISO-8601 UTC timestamp + result (None | ReconciliationPreviewResponse | 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: OperationEnvelopeReconciliationPreviewResponseStatus + at: str + result: None | ReconciliationPreviewResponse | 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.reconciliation_preview_response import ReconciliationPreviewResponse + + 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, ReconciliationPreviewResponse): + 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.reconciliation_preview_response import ReconciliationPreviewResponse + + d = dict(src_dict) + operation = d.pop("operation") + + operation_id = d.pop("operationId") + + status = OperationEnvelopeReconciliationPreviewResponseStatus(d.pop("status")) + + at = d.pop("at") + + def _parse_result(data: object) -> None | ReconciliationPreviewResponse | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + result_type_0 = ReconciliationPreviewResponse.from_dict(data) + + return result_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | ReconciliationPreviewResponse | 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_reconciliation_preview_response = cls( + operation=operation, + operation_id=operation_id, + status=status, + at=at, + result=result, + created_by=created_by, + idempotent_replay=idempotent_replay, + ) + + operation_envelope_reconciliation_preview_response.additional_properties = d + return operation_envelope_reconciliation_preview_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_reconciliation_preview_response_status.py b/robosystems_client/models/operation_envelope_reconciliation_preview_response_status.py new file mode 100644 index 0000000..1ede81b --- /dev/null +++ b/robosystems_client/models/operation_envelope_reconciliation_preview_response_status.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class OperationEnvelopeReconciliationPreviewResponseStatus(str, Enum): + COMPLETED = "completed" + FAILED = "failed" + PENDING = "pending" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/operation_envelope_reconciliation_summary.py b/robosystems_client/models/operation_envelope_reconciliation_summary.py new file mode 100644 index 0000000..eb74369 --- /dev/null +++ b/robosystems_client/models/operation_envelope_reconciliation_summary.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_reconciliation_summary_status import ( + OperationEnvelopeReconciliationSummaryStatus, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.reconciliation_summary import ReconciliationSummary + + +T = TypeVar("T", bound="OperationEnvelopeReconciliationSummary") + + +@_attrs_define +class OperationEnvelopeReconciliationSummary: + """ + Attributes: + operation (str): Kebab-case operation name + operation_id (str): op_-prefixed ULID for audit and SSE correlation + status (OperationEnvelopeReconciliationSummaryStatus): Operation lifecycle state + at (str): ISO-8601 UTC timestamp + result (None | ReconciliationSummary | 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: OperationEnvelopeReconciliationSummaryStatus + at: str + result: None | ReconciliationSummary | 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.reconciliation_summary import ReconciliationSummary + + 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, ReconciliationSummary): + 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.reconciliation_summary import ReconciliationSummary + + d = dict(src_dict) + operation = d.pop("operation") + + operation_id = d.pop("operationId") + + status = OperationEnvelopeReconciliationSummaryStatus(d.pop("status")) + + at = d.pop("at") + + def _parse_result(data: object) -> None | ReconciliationSummary | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + result_type_0 = ReconciliationSummary.from_dict(data) + + return result_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | ReconciliationSummary | 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_reconciliation_summary = cls( + operation=operation, + operation_id=operation_id, + status=status, + at=at, + result=result, + created_by=created_by, + idempotent_replay=idempotent_replay, + ) + + operation_envelope_reconciliation_summary.additional_properties = d + return operation_envelope_reconciliation_summary + + @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_reconciliation_summary_status.py b/robosystems_client/models/operation_envelope_reconciliation_summary_status.py new file mode 100644 index 0000000..7c2880a --- /dev/null +++ b/robosystems_client/models/operation_envelope_reconciliation_summary_status.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class OperationEnvelopeReconciliationSummaryStatus(str, Enum): + COMPLETED = "completed" + FAILED = "failed" + PENDING = "pending" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/preview_reconciliations_request.py b/robosystems_client/models/preview_reconciliations_request.py new file mode 100644 index 0000000..d28202a --- /dev/null +++ b/robosystems_client/models/preview_reconciliations_request.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.preview_reconciliations_request_method import ( + PreviewReconciliationsRequestMethod, +) +from ..types import UNSET, Unset + +T = TypeVar("T", bound="PreviewReconciliationsRequest") + + +@_attrs_define +class PreviewReconciliationsRequest: + """Compare the ledger's balances at a period end with an independent source. + + Attributes: + period (str): Period to compare at its last day, as YYYY-MM. + method (PreviewReconciliationsRequestMethod | Unset): Which check to preview. `source_ledger` compares every + account with the synced accounting system's own trial balance. `schedule_register` compares each asset account a + schedule carries a balance on with what its schedules say it holds. `statement` compares each account that has a + statement balance recorded in the period with that balance. Default: + PreviewReconciliationsRequestMethod.SOURCE_LEDGER. + include_tied (bool | Unset): Also return the accounts that tie. Off by default: the differences are the work, + and the counts cover the rest. Default: False. + """ + + period: str + method: PreviewReconciliationsRequestMethod | Unset = ( + PreviewReconciliationsRequestMethod.SOURCE_LEDGER + ) + include_tied: bool | Unset = False + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + period = self.period + + method: str | Unset = UNSET + if not isinstance(self.method, Unset): + method = self.method.value + + include_tied = self.include_tied + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "period": period, + } + ) + if method is not UNSET: + field_dict["method"] = method + if include_tied is not UNSET: + field_dict["include_tied"] = include_tied + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + period = d.pop("period") + + _method = d.pop("method", UNSET) + method: PreviewReconciliationsRequestMethod | Unset + if isinstance(_method, Unset): + method = UNSET + else: + method = PreviewReconciliationsRequestMethod(_method) + + include_tied = d.pop("include_tied", UNSET) + + preview_reconciliations_request = cls( + period=period, + method=method, + include_tied=include_tied, + ) + + preview_reconciliations_request.additional_properties = d + return preview_reconciliations_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/preview_reconciliations_request_method.py b/robosystems_client/models/preview_reconciliations_request_method.py new file mode 100644 index 0000000..28eea5e --- /dev/null +++ b/robosystems_client/models/preview_reconciliations_request_method.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class PreviewReconciliationsRequestMethod(str, Enum): + SCHEDULE_REGISTER = "schedule_register" + SOURCE_LEDGER = "source_ledger" + STATEMENT = "statement" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/reconciliation_component.py b/robosystems_client/models/reconciliation_component.py new file mode 100644 index 0000000..cb2ffc6 --- /dev/null +++ b/robosystems_client/models/reconciliation_component.py @@ -0,0 +1,154 @@ +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="ReconciliationComponent") + + +@_attrs_define +class ReconciliationComponent: + """One part of an account's independent balance: what a single schedule + says the account carries, or a recorded statement balance. + + Attributes: + name (str): The schedule's name, or the statement and its date. + amount (float): What this part says the account holds, debit-positive. + structure_id (None | str | Unset): The schedule, for a `schedule_register` part. + event_id (None | str | Unset): The recorded balance, for a `statement` part. + document_id (None | str | Unset): The statement document given as evidence, when one was. + note (None | str | Unset): Why a schedule carries nothing (disposed of, or ended early), or the note recorded + with a statement balance. + """ + + name: str + amount: float + structure_id: None | str | Unset = UNSET + event_id: None | str | Unset = UNSET + document_id: None | str | Unset = UNSET + note: 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 + + amount = self.amount + + structure_id: None | str | Unset + if isinstance(self.structure_id, Unset): + structure_id = UNSET + else: + structure_id = self.structure_id + + event_id: None | str | Unset + if isinstance(self.event_id, Unset): + event_id = UNSET + else: + event_id = self.event_id + + document_id: None | str | Unset + if isinstance(self.document_id, Unset): + document_id = UNSET + else: + document_id = self.document_id + + note: None | str | Unset + if isinstance(self.note, Unset): + note = UNSET + else: + note = self.note + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "name": name, + "amount": amount, + } + ) + if structure_id is not UNSET: + field_dict["structure_id"] = structure_id + if event_id is not UNSET: + field_dict["event_id"] = event_id + if document_id is not UNSET: + field_dict["document_id"] = document_id + if note is not UNSET: + field_dict["note"] = note + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + name = d.pop("name") + + amount = d.pop("amount") + + def _parse_structure_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + structure_id = _parse_structure_id(d.pop("structure_id", UNSET)) + + def _parse_event_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + event_id = _parse_event_id(d.pop("event_id", UNSET)) + + def _parse_document_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + document_id = _parse_document_id(d.pop("document_id", UNSET)) + + def _parse_note(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + note = _parse_note(d.pop("note", UNSET)) + + reconciliation_component = cls( + name=name, + amount=amount, + structure_id=structure_id, + event_id=event_id, + document_id=document_id, + note=note, + ) + + reconciliation_component.additional_properties = d + return reconciliation_component + + @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/reconciliation_list_response.py b/robosystems_client/models/reconciliation_list_response.py new file mode 100644 index 0000000..b07c869 --- /dev/null +++ b/robosystems_client/models/reconciliation_list_response.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +import datetime +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.reconciliation_summary import ReconciliationSummary + + +T = TypeVar("T", bound="ReconciliationListResponse") + + +@_attrs_define +class ReconciliationListResponse: + """Every reconciliation's standing for one period. + + Attributes: + period (str): The period, as YYYY-MM. + as_of (datetime.date): The period's last day. + reconciliations (list[ReconciliationSummary]): One entry per reconciliation block, oldest block first. + notes (list[str] | Unset): What a refresh could not compare, such as a check skipped because its source is no + longer connected. Empty on a plain read. + """ + + period: str + as_of: datetime.date + reconciliations: list[ReconciliationSummary] + notes: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + period = self.period + + as_of = self.as_of.isoformat() + + reconciliations = [] + for reconciliations_item_data in self.reconciliations: + reconciliations_item = reconciliations_item_data.to_dict() + reconciliations.append(reconciliations_item) + + notes: list[str] | Unset = UNSET + if not isinstance(self.notes, Unset): + notes = self.notes + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "period": period, + "as_of": as_of, + "reconciliations": reconciliations, + } + ) + if notes is not UNSET: + field_dict["notes"] = notes + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.reconciliation_summary import ReconciliationSummary + + d = dict(src_dict) + period = d.pop("period") + + as_of = datetime.date.fromisoformat(d.pop("as_of")) + + reconciliations = [] + _reconciliations = d.pop("reconciliations") + for reconciliations_item_data in _reconciliations: + reconciliations_item = ReconciliationSummary.from_dict(reconciliations_item_data) + + reconciliations.append(reconciliations_item) + + notes = cast(list[str], d.pop("notes", UNSET)) + + reconciliation_list_response = cls( + period=period, + as_of=as_of, + reconciliations=reconciliations, + notes=notes, + ) + + reconciliation_list_response.additional_properties = d + return reconciliation_list_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/reconciliation_mechanics.py b/robosystems_client/models/reconciliation_mechanics.py new file mode 100644 index 0000000..15ad84f --- /dev/null +++ b/robosystems_client/models/reconciliation_mechanics.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Literal, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.reconciliation_mechanics_method import ReconciliationMechanicsMethod +from ..models.reconciliation_mechanics_scope import ReconciliationMechanicsScope +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ReconciliationMechanics") + + +@_attrs_define +class ReconciliationMechanics: + """Mechanics for `block_type='reconciliation'`: what is compared, against + what, and how much the close cares. + + A `ledger`-scope reconciliation checks the whole ledger against one + source (the synced accounting system's own trial balance). An + `account`-scope one ties a single account to an independent balance. + + Attributes: + scope (ReconciliationMechanicsScope): Whether the block covers the whole ledger or one account. + method (ReconciliationMechanicsMethod): Where the independent side comes from. + kind (Literal['reconciliation'] | Unset): Default: 'reconciliation'. + element_id (None | str | Unset): The account reconciled; null for a ledger-scope block. + required_for_close (bool | Unset): Whether the period's close waits on this reconciliation. Default: True. + materiality (float | Unset): A difference up to this amount still counts as reconciled. Zero means the two sides + must agree to the cent. Default: 0.0. + review_required (bool | Unset): Whether the close waits for a sign-off as well, not just for the two sides to + reconcile. Default: False. + separate_reviewer (bool | Unset): Whether the person who signs off must be someone other than the person who ran + the comparison. Default: False. + """ + + scope: ReconciliationMechanicsScope + method: ReconciliationMechanicsMethod + kind: Literal["reconciliation"] | Unset = "reconciliation" + element_id: None | str | Unset = UNSET + required_for_close: bool | Unset = True + materiality: float | Unset = 0.0 + review_required: bool | Unset = False + separate_reviewer: bool | Unset = False + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + scope = self.scope.value + + method = self.method.value + + kind = self.kind + + element_id: None | str | Unset + if isinstance(self.element_id, Unset): + element_id = UNSET + else: + element_id = self.element_id + + required_for_close = self.required_for_close + + materiality = self.materiality + + review_required = self.review_required + + separate_reviewer = self.separate_reviewer + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "scope": scope, + "method": method, + } + ) + if kind is not UNSET: + field_dict["kind"] = kind + if element_id is not UNSET: + field_dict["element_id"] = element_id + if required_for_close is not UNSET: + field_dict["required_for_close"] = required_for_close + if materiality is not UNSET: + field_dict["materiality"] = materiality + if review_required is not UNSET: + field_dict["review_required"] = review_required + if separate_reviewer is not UNSET: + field_dict["separate_reviewer"] = separate_reviewer + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + scope = ReconciliationMechanicsScope(d.pop("scope")) + + method = ReconciliationMechanicsMethod(d.pop("method")) + + kind = cast(Literal["reconciliation"] | Unset, d.pop("kind", UNSET)) + if kind != "reconciliation" and not isinstance(kind, Unset): + raise ValueError(f"kind must match const 'reconciliation', got '{kind}'") + + def _parse_element_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + element_id = _parse_element_id(d.pop("element_id", UNSET)) + + required_for_close = d.pop("required_for_close", UNSET) + + materiality = d.pop("materiality", UNSET) + + review_required = d.pop("review_required", UNSET) + + separate_reviewer = d.pop("separate_reviewer", UNSET) + + reconciliation_mechanics = cls( + scope=scope, + method=method, + kind=kind, + element_id=element_id, + required_for_close=required_for_close, + materiality=materiality, + review_required=review_required, + separate_reviewer=separate_reviewer, + ) + + reconciliation_mechanics.additional_properties = d + return reconciliation_mechanics + + @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/reconciliation_mechanics_method.py b/robosystems_client/models/reconciliation_mechanics_method.py new file mode 100644 index 0000000..f7fe5ce --- /dev/null +++ b/robosystems_client/models/reconciliation_mechanics_method.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class ReconciliationMechanicsMethod(str, Enum): + SCHEDULE_REGISTER = "schedule_register" + SOURCE_LEDGER = "source_ledger" + STATEMENT = "statement" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/reconciliation_mechanics_scope.py b/robosystems_client/models/reconciliation_mechanics_scope.py new file mode 100644 index 0000000..cfddd53 --- /dev/null +++ b/robosystems_client/models/reconciliation_mechanics_scope.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class ReconciliationMechanicsScope(str, Enum): + ACCOUNT = "account" + LEDGER = "ledger" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/reconciliation_policy_response.py b/robosystems_client/models/reconciliation_policy_response.py new file mode 100644 index 0000000..132a312 --- /dev/null +++ b/robosystems_client/models/reconciliation_policy_response.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ReconciliationPolicyResponse") + + +@_attrs_define +class ReconciliationPolicyResponse: + """A reconciliation's policy after a change. + + Attributes: + structure_id (str): + required_for_close (bool): + materiality (float): + review_required (bool): + separate_reviewer (bool): + """ + + structure_id: str + required_for_close: bool + materiality: float + review_required: bool + separate_reviewer: bool + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + structure_id = self.structure_id + + required_for_close = self.required_for_close + + materiality = self.materiality + + review_required = self.review_required + + separate_reviewer = self.separate_reviewer + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "structure_id": structure_id, + "required_for_close": required_for_close, + "materiality": materiality, + "review_required": review_required, + "separate_reviewer": separate_reviewer, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + structure_id = d.pop("structure_id") + + required_for_close = d.pop("required_for_close") + + materiality = d.pop("materiality") + + review_required = d.pop("review_required") + + separate_reviewer = d.pop("separate_reviewer") + + reconciliation_policy_response = cls( + structure_id=structure_id, + required_for_close=required_for_close, + materiality=materiality, + review_required=review_required, + separate_reviewer=separate_reviewer, + ) + + reconciliation_policy_response.additional_properties = d + return reconciliation_policy_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/reconciliation_preview_response.py b/robosystems_client/models/reconciliation_preview_response.py new file mode 100644 index 0000000..f07f152 --- /dev/null +++ b/robosystems_client/models/reconciliation_preview_response.py @@ -0,0 +1,218 @@ +from __future__ import annotations + +import datetime +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.reconciliation_preview_response_method import ( + ReconciliationPreviewResponseMethod, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.reconciliation_row import ReconciliationRow + + +T = TypeVar("T", bound="ReconciliationPreviewResponse") + + +@_attrs_define +class ReconciliationPreviewResponse: + """The comparison for one period end. Nothing is written. + + Attributes: + period (str): + as_of (datetime.date): The period's last day. + fiscal_year_start (datetime.date): Income-statement accounts are compared from this date to `as_of`. + method (ReconciliationPreviewResponseMethod): + source (str): The system the independent side was read from. + accounts_compared (int): Accounts with a balance on either side; zero on both is left out. + accounts_tied (int): + accounts_different (int): Every account that does not tie, whatever the reason. + total_difference (float): Sum of the absolute differences across accounts, not a net figure: one missing + transaction counts on each account it touches. + rows (list[ReconciliationRow]): Accounts that do not tie, largest difference first; tied accounts follow when + `include_tied` is set. + report_basis (None | str | Unset): Accounting basis the source reported on. + last_sync_at (datetime.datetime | None | Unset): When the source was last synced. A difference on a sync older + than the period end may be activity not yet synced, not a fault in the mirror. + notes (list[str] | Unset): How the comparison was made, and anything that qualifies it. + """ + + period: str + as_of: datetime.date + fiscal_year_start: datetime.date + method: ReconciliationPreviewResponseMethod + source: str + accounts_compared: int + accounts_tied: int + accounts_different: int + total_difference: float + rows: list[ReconciliationRow] + report_basis: None | str | Unset = UNSET + last_sync_at: datetime.datetime | None | Unset = UNSET + notes: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + period = self.period + + as_of = self.as_of.isoformat() + + fiscal_year_start = self.fiscal_year_start.isoformat() + + method = self.method.value + + source = self.source + + accounts_compared = self.accounts_compared + + accounts_tied = self.accounts_tied + + accounts_different = self.accounts_different + + total_difference = self.total_difference + + rows = [] + for rows_item_data in self.rows: + rows_item = rows_item_data.to_dict() + rows.append(rows_item) + + report_basis: None | str | Unset + if isinstance(self.report_basis, Unset): + report_basis = UNSET + else: + report_basis = self.report_basis + + last_sync_at: None | str | Unset + if isinstance(self.last_sync_at, Unset): + last_sync_at = UNSET + elif isinstance(self.last_sync_at, datetime.datetime): + last_sync_at = self.last_sync_at.isoformat() + else: + last_sync_at = self.last_sync_at + + notes: list[str] | Unset = UNSET + if not isinstance(self.notes, Unset): + notes = self.notes + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "period": period, + "as_of": as_of, + "fiscal_year_start": fiscal_year_start, + "method": method, + "source": source, + "accounts_compared": accounts_compared, + "accounts_tied": accounts_tied, + "accounts_different": accounts_different, + "total_difference": total_difference, + "rows": rows, + } + ) + if report_basis is not UNSET: + field_dict["report_basis"] = report_basis + if last_sync_at is not UNSET: + field_dict["last_sync_at"] = last_sync_at + if notes is not UNSET: + field_dict["notes"] = notes + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.reconciliation_row import ReconciliationRow + + d = dict(src_dict) + period = d.pop("period") + + as_of = datetime.date.fromisoformat(d.pop("as_of")) + + fiscal_year_start = datetime.date.fromisoformat(d.pop("fiscal_year_start")) + + method = ReconciliationPreviewResponseMethod(d.pop("method")) + + source = d.pop("source") + + accounts_compared = d.pop("accounts_compared") + + accounts_tied = d.pop("accounts_tied") + + accounts_different = d.pop("accounts_different") + + total_difference = d.pop("total_difference") + + rows = [] + _rows = d.pop("rows") + for rows_item_data in _rows: + rows_item = ReconciliationRow.from_dict(rows_item_data) + + rows.append(rows_item) + + def _parse_report_basis(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + report_basis = _parse_report_basis(d.pop("report_basis", UNSET)) + + def _parse_last_sync_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() + last_sync_at_type_0 = datetime.datetime.fromisoformat(data) + + return last_sync_at_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + last_sync_at = _parse_last_sync_at(d.pop("last_sync_at", UNSET)) + + notes = cast(list[str], d.pop("notes", UNSET)) + + reconciliation_preview_response = cls( + period=period, + as_of=as_of, + fiscal_year_start=fiscal_year_start, + method=method, + source=source, + accounts_compared=accounts_compared, + accounts_tied=accounts_tied, + accounts_different=accounts_different, + total_difference=total_difference, + rows=rows, + report_basis=report_basis, + last_sync_at=last_sync_at, + notes=notes, + ) + + reconciliation_preview_response.additional_properties = d + return reconciliation_preview_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/reconciliation_preview_response_method.py b/robosystems_client/models/reconciliation_preview_response_method.py new file mode 100644 index 0000000..47b8857 --- /dev/null +++ b/robosystems_client/models/reconciliation_preview_response_method.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class ReconciliationPreviewResponseMethod(str, Enum): + SCHEDULE_REGISTER = "schedule_register" + SOURCE_LEDGER = "source_ledger" + STATEMENT = "statement" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/reconciliation_row.py b/robosystems_client/models/reconciliation_row.py new file mode 100644 index 0000000..fbc7a59 --- /dev/null +++ b/robosystems_client/models/reconciliation_row.py @@ -0,0 +1,244 @@ +from __future__ import annotations + +import datetime +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.reconciliation_component import ReconciliationComponent + + +T = TypeVar("T", bound="ReconciliationRow") + + +@_attrs_define +class ReconciliationRow: + """One account: the ledger's balance, the independent balance, the difference. + + Balances are debit-positive, so a credit balance is negative on both sides. + + Attributes: + account_name (str): The account's name in the ledger, else in the source. + ledger_balance (float): What the ledger holds. For `source_ledger`, landed entries only. For + `schedule_register`, the balance as the period's close will leave it: landed entries, drafts awaiting the close, + and schedule entries not yet drafted. + independent_balance (float): What the independent source says. + difference (float): Ledger minus independent. + status (str): `tied`: both sides agree to the cent. `different`: both know the account and disagree. + `not_in_ledger`: the source reports an account the ledger has none for. `not_in_source`: the ledger holds a + balance on an account the source does not have. + element_id (None | str | Unset): The chart account; null when the ledger has none for it. + account_code (None | str | Unset): The chart account's code. + source_account_id (None | str | Unset): The account's id in the source system, when it has one. + statement (None | str | Unset): `balance_sheet` or `income_statement`. Balance-sheet accounts are compared + cumulatively to the period end; income-statement accounts from the start of the fiscal year. Null when the + ledger has no account. + as_of (datetime.date | None | Unset): The date both balances are stated at, when it is not the period's last + day: a statement that ends mid-period is compared with the ledger at the statement's own date. + components (list[ReconciliationComponent] | Unset): Account-scope methods only: what makes up the independent + balance. One entry per schedule for `schedule_register`; the recorded statement for `statement`. + """ + + account_name: str + ledger_balance: float + independent_balance: float + difference: float + status: str + element_id: None | str | Unset = UNSET + account_code: None | str | Unset = UNSET + source_account_id: None | str | Unset = UNSET + statement: None | str | Unset = UNSET + as_of: datetime.date | None | Unset = UNSET + components: list[ReconciliationComponent] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + account_name = self.account_name + + ledger_balance = self.ledger_balance + + independent_balance = self.independent_balance + + difference = self.difference + + status = self.status + + element_id: None | str | Unset + if isinstance(self.element_id, Unset): + element_id = UNSET + else: + element_id = self.element_id + + account_code: None | str | Unset + if isinstance(self.account_code, Unset): + account_code = UNSET + else: + account_code = self.account_code + + source_account_id: None | str | Unset + if isinstance(self.source_account_id, Unset): + source_account_id = UNSET + else: + source_account_id = self.source_account_id + + statement: None | str | Unset + if isinstance(self.statement, Unset): + statement = UNSET + else: + statement = self.statement + + as_of: None | str | Unset + if isinstance(self.as_of, Unset): + as_of = UNSET + elif isinstance(self.as_of, datetime.date): + as_of = self.as_of.isoformat() + else: + as_of = self.as_of + + components: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.components, Unset): + components = [] + for components_item_data in self.components: + components_item = components_item_data.to_dict() + components.append(components_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "account_name": account_name, + "ledger_balance": ledger_balance, + "independent_balance": independent_balance, + "difference": difference, + "status": status, + } + ) + if element_id is not UNSET: + field_dict["element_id"] = element_id + if account_code is not UNSET: + field_dict["account_code"] = account_code + if source_account_id is not UNSET: + field_dict["source_account_id"] = source_account_id + if statement is not UNSET: + field_dict["statement"] = statement + if as_of is not UNSET: + field_dict["as_of"] = as_of + if components is not UNSET: + field_dict["components"] = components + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.reconciliation_component import ReconciliationComponent + + d = dict(src_dict) + account_name = d.pop("account_name") + + ledger_balance = d.pop("ledger_balance") + + independent_balance = d.pop("independent_balance") + + difference = d.pop("difference") + + status = d.pop("status") + + def _parse_element_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + element_id = _parse_element_id(d.pop("element_id", UNSET)) + + def _parse_account_code(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + account_code = _parse_account_code(d.pop("account_code", UNSET)) + + def _parse_source_account_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + source_account_id = _parse_source_account_id(d.pop("source_account_id", UNSET)) + + def _parse_statement(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + statement = _parse_statement(d.pop("statement", UNSET)) + + def _parse_as_of(data: object) -> datetime.date | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + as_of_type_0 = datetime.date.fromisoformat(data) + + return as_of_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.date | None | Unset, data) + + as_of = _parse_as_of(d.pop("as_of", UNSET)) + + _components = d.pop("components", UNSET) + components: list[ReconciliationComponent] | Unset = UNSET + if _components is not UNSET: + components = [] + for components_item_data in _components: + components_item = ReconciliationComponent.from_dict(components_item_data) + + components.append(components_item) + + reconciliation_row = cls( + account_name=account_name, + ledger_balance=ledger_balance, + independent_balance=independent_balance, + difference=difference, + status=status, + element_id=element_id, + account_code=account_code, + source_account_id=source_account_id, + statement=statement, + as_of=as_of, + components=components, + ) + + reconciliation_row.additional_properties = d + return reconciliation_row + + @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/reconciliation_summary.py b/robosystems_client/models/reconciliation_summary.py new file mode 100644 index 0000000..7510e12 --- /dev/null +++ b/robosystems_client/models/reconciliation_summary.py @@ -0,0 +1,544 @@ +from __future__ import annotations + +import datetime +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.reconciliation_component import ReconciliationComponent + from ..models.reconciliation_row import ReconciliationRow + + +T = TypeVar("T", bound="ReconciliationSummary") + + +@_attrs_define +class ReconciliationSummary: + """One reconciliation's standing for a period. + + Attributes: + structure_id (str): The reconciliation block. + name (str): The block's name. + scope (str): `ledger`: the whole ledger against one source. `account`: one account against an independent + balance. + method (str): Where the independent side comes from. `source_ledger` is the synced accounting system's own trial + balance. `schedule_register` is what the account's schedules say it carries. `statement` is the ending balance + of a statement recorded for the account. + required_for_close (bool): Whether the period's close waits on this reconciliation. + materiality (float): A difference up to this amount still counts as reconciled. + period (str): The period, as YYYY-MM. + as_of (datetime.date): The period's last day. + status (str): `not_started`: not compared for this period. `unreconciled`: the sides differ by more than the + materiality. `explained`: they differ and items account for all of it. `reconciled`: nothing is left + unexplained. `reviewed`: reconciled and signed off. + review_required (bool): Whether the close also waits for a sign-off. + separate_reviewer (bool): Whether the reviewer must be someone other than the person who ran the comparison. + element_id (None | str | Unset): The account reconciled; null for a ledger-scope block. + unreconciled_difference (float | None | Unset): What is left unexplained at the last comparison; null when the + period has not been compared. For a ledger-scope block, the sum of every account's absolute difference. For an + account-scope block, the ledger balance minus the independent one. + accounts_compared (int | None | Unset): Ledger-scope only: accounts with a balance on either side. + accounts_different (int | None | Unset): Ledger-scope only: accounts that do not tie. + ledger_balance (float | None | Unset): Account-scope only: the account's balance at the last comparison, debit- + positive, as the period's close will leave it. + independent_balance (float | None | Unset): Account-scope only: what the independent source said at the last + comparison, debit-positive. + balance_as_of (datetime.date | None | Unset): Account-scope only: the date the two balances are stated at. The + period's last day, unless a statement ended earlier in the period. + components (list[ReconciliationComponent] | Unset): Account-scope only: what makes up the independent balance. + One entry per schedule for `schedule_register`; the recorded statement for `statement`. + source (None | str | Unset): The system the independent side was read from. + compared_at (datetime.datetime | None | Unset): When the two sides were last compared. + fact_set_id (None | str | Unset): The FactSet holding the period's comparison. + compared_by (None | str | Unset): The user whose action ran the last comparison. + compared_via (None | str | Unset): `operation` when someone ran refresh-reconciliations; `sync` when a source + sync refreshed it. + reviewed_by (None | str | Unset): The user who signed off the comparison as it stands. Null when nobody has, or + when the balances changed after the sign-off. + reviewed_at (datetime.datetime | None | Unset): When the standing sign-off was made. + self_reviewed (bool | None | Unset): True when the reviewer is the person who ran the comparison they signed + off. Null when there is no standing sign-off. + differences (list[ReconciliationRow] | Unset): Ledger-scope only: the accounts that did not tie at the last + comparison, largest difference first. + """ + + structure_id: str + name: str + scope: str + method: str + required_for_close: bool + materiality: float + period: str + as_of: datetime.date + status: str + review_required: bool + separate_reviewer: bool + element_id: None | str | Unset = UNSET + unreconciled_difference: float | None | Unset = UNSET + accounts_compared: int | None | Unset = UNSET + accounts_different: int | None | Unset = UNSET + ledger_balance: float | None | Unset = UNSET + independent_balance: float | None | Unset = UNSET + balance_as_of: datetime.date | None | Unset = UNSET + components: list[ReconciliationComponent] | Unset = UNSET + source: None | str | Unset = UNSET + compared_at: datetime.datetime | None | Unset = UNSET + fact_set_id: None | str | Unset = UNSET + compared_by: None | str | Unset = UNSET + compared_via: None | str | Unset = UNSET + reviewed_by: None | str | Unset = UNSET + reviewed_at: datetime.datetime | None | Unset = UNSET + self_reviewed: bool | None | Unset = UNSET + differences: list[ReconciliationRow] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + structure_id = self.structure_id + + name = self.name + + scope = self.scope + + method = self.method + + required_for_close = self.required_for_close + + materiality = self.materiality + + period = self.period + + as_of = self.as_of.isoformat() + + status = self.status + + review_required = self.review_required + + separate_reviewer = self.separate_reviewer + + element_id: None | str | Unset + if isinstance(self.element_id, Unset): + element_id = UNSET + else: + element_id = self.element_id + + unreconciled_difference: float | None | Unset + if isinstance(self.unreconciled_difference, Unset): + unreconciled_difference = UNSET + else: + unreconciled_difference = self.unreconciled_difference + + accounts_compared: int | None | Unset + if isinstance(self.accounts_compared, Unset): + accounts_compared = UNSET + else: + accounts_compared = self.accounts_compared + + accounts_different: int | None | Unset + if isinstance(self.accounts_different, Unset): + accounts_different = UNSET + else: + accounts_different = self.accounts_different + + ledger_balance: float | None | Unset + if isinstance(self.ledger_balance, Unset): + ledger_balance = UNSET + else: + ledger_balance = self.ledger_balance + + independent_balance: float | None | Unset + if isinstance(self.independent_balance, Unset): + independent_balance = UNSET + else: + independent_balance = self.independent_balance + + balance_as_of: None | str | Unset + if isinstance(self.balance_as_of, Unset): + balance_as_of = UNSET + elif isinstance(self.balance_as_of, datetime.date): + balance_as_of = self.balance_as_of.isoformat() + else: + balance_as_of = self.balance_as_of + + components: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.components, Unset): + components = [] + for components_item_data in self.components: + components_item = components_item_data.to_dict() + components.append(components_item) + + source: None | str | Unset + if isinstance(self.source, Unset): + source = UNSET + else: + source = self.source + + compared_at: None | str | Unset + if isinstance(self.compared_at, Unset): + compared_at = UNSET + elif isinstance(self.compared_at, datetime.datetime): + compared_at = self.compared_at.isoformat() + else: + compared_at = self.compared_at + + fact_set_id: None | str | Unset + if isinstance(self.fact_set_id, Unset): + fact_set_id = UNSET + else: + fact_set_id = self.fact_set_id + + compared_by: None | str | Unset + if isinstance(self.compared_by, Unset): + compared_by = UNSET + else: + compared_by = self.compared_by + + compared_via: None | str | Unset + if isinstance(self.compared_via, Unset): + compared_via = UNSET + else: + compared_via = self.compared_via + + reviewed_by: None | str | Unset + if isinstance(self.reviewed_by, Unset): + reviewed_by = UNSET + else: + reviewed_by = self.reviewed_by + + reviewed_at: None | str | Unset + if isinstance(self.reviewed_at, Unset): + reviewed_at = UNSET + elif isinstance(self.reviewed_at, datetime.datetime): + reviewed_at = self.reviewed_at.isoformat() + else: + reviewed_at = self.reviewed_at + + self_reviewed: bool | None | Unset + if isinstance(self.self_reviewed, Unset): + self_reviewed = UNSET + else: + self_reviewed = self.self_reviewed + + differences: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.differences, Unset): + differences = [] + for differences_item_data in self.differences: + differences_item = differences_item_data.to_dict() + differences.append(differences_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "structure_id": structure_id, + "name": name, + "scope": scope, + "method": method, + "required_for_close": required_for_close, + "materiality": materiality, + "period": period, + "as_of": as_of, + "status": status, + "review_required": review_required, + "separate_reviewer": separate_reviewer, + } + ) + if element_id is not UNSET: + field_dict["element_id"] = element_id + if unreconciled_difference is not UNSET: + field_dict["unreconciled_difference"] = unreconciled_difference + if accounts_compared is not UNSET: + field_dict["accounts_compared"] = accounts_compared + if accounts_different is not UNSET: + field_dict["accounts_different"] = accounts_different + if ledger_balance is not UNSET: + field_dict["ledger_balance"] = ledger_balance + if independent_balance is not UNSET: + field_dict["independent_balance"] = independent_balance + if balance_as_of is not UNSET: + field_dict["balance_as_of"] = balance_as_of + if components is not UNSET: + field_dict["components"] = components + if source is not UNSET: + field_dict["source"] = source + if compared_at is not UNSET: + field_dict["compared_at"] = compared_at + if fact_set_id is not UNSET: + field_dict["fact_set_id"] = fact_set_id + if compared_by is not UNSET: + field_dict["compared_by"] = compared_by + if compared_via is not UNSET: + field_dict["compared_via"] = compared_via + if reviewed_by is not UNSET: + field_dict["reviewed_by"] = reviewed_by + if reviewed_at is not UNSET: + field_dict["reviewed_at"] = reviewed_at + if self_reviewed is not UNSET: + field_dict["self_reviewed"] = self_reviewed + 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.reconciliation_component import ReconciliationComponent + from ..models.reconciliation_row import ReconciliationRow + + d = dict(src_dict) + structure_id = d.pop("structure_id") + + name = d.pop("name") + + scope = d.pop("scope") + + method = d.pop("method") + + required_for_close = d.pop("required_for_close") + + materiality = d.pop("materiality") + + period = d.pop("period") + + as_of = datetime.date.fromisoformat(d.pop("as_of")) + + status = d.pop("status") + + review_required = d.pop("review_required") + + separate_reviewer = d.pop("separate_reviewer") + + def _parse_element_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + element_id = _parse_element_id(d.pop("element_id", UNSET)) + + def _parse_unreconciled_difference(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + unreconciled_difference = _parse_unreconciled_difference( + d.pop("unreconciled_difference", UNSET) + ) + + def _parse_accounts_compared(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + accounts_compared = _parse_accounts_compared(d.pop("accounts_compared", UNSET)) + + def _parse_accounts_different(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + accounts_different = _parse_accounts_different(d.pop("accounts_different", UNSET)) + + def _parse_ledger_balance(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + ledger_balance = _parse_ledger_balance(d.pop("ledger_balance", UNSET)) + + def _parse_independent_balance(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + independent_balance = _parse_independent_balance( + d.pop("independent_balance", UNSET) + ) + + def _parse_balance_as_of(data: object) -> datetime.date | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + balance_as_of_type_0 = datetime.date.fromisoformat(data) + + return balance_as_of_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.date | None | Unset, data) + + balance_as_of = _parse_balance_as_of(d.pop("balance_as_of", UNSET)) + + _components = d.pop("components", UNSET) + components: list[ReconciliationComponent] | Unset = UNSET + if _components is not UNSET: + components = [] + for components_item_data in _components: + components_item = ReconciliationComponent.from_dict(components_item_data) + + components.append(components_item) + + def _parse_source(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + source = _parse_source(d.pop("source", UNSET)) + + def _parse_compared_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() + compared_at_type_0 = datetime.datetime.fromisoformat(data) + + return compared_at_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + compared_at = _parse_compared_at(d.pop("compared_at", UNSET)) + + def _parse_fact_set_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + fact_set_id = _parse_fact_set_id(d.pop("fact_set_id", UNSET)) + + def _parse_compared_by(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + compared_by = _parse_compared_by(d.pop("compared_by", UNSET)) + + def _parse_compared_via(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + compared_via = _parse_compared_via(d.pop("compared_via", UNSET)) + + def _parse_reviewed_by(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + reviewed_by = _parse_reviewed_by(d.pop("reviewed_by", UNSET)) + + def _parse_reviewed_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() + reviewed_at_type_0 = datetime.datetime.fromisoformat(data) + + return reviewed_at_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + reviewed_at = _parse_reviewed_at(d.pop("reviewed_at", UNSET)) + + def _parse_self_reviewed(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + self_reviewed = _parse_self_reviewed(d.pop("self_reviewed", UNSET)) + + _differences = d.pop("differences", UNSET) + differences: list[ReconciliationRow] | Unset = UNSET + if _differences is not UNSET: + differences = [] + for differences_item_data in _differences: + differences_item = ReconciliationRow.from_dict(differences_item_data) + + differences.append(differences_item) + + reconciliation_summary = cls( + structure_id=structure_id, + name=name, + scope=scope, + method=method, + required_for_close=required_for_close, + materiality=materiality, + period=period, + as_of=as_of, + status=status, + review_required=review_required, + separate_reviewer=separate_reviewer, + element_id=element_id, + unreconciled_difference=unreconciled_difference, + accounts_compared=accounts_compared, + accounts_different=accounts_different, + ledger_balance=ledger_balance, + independent_balance=independent_balance, + balance_as_of=balance_as_of, + components=components, + source=source, + compared_at=compared_at, + fact_set_id=fact_set_id, + compared_by=compared_by, + compared_via=compared_via, + reviewed_by=reviewed_by, + reviewed_at=reviewed_at, + self_reviewed=self_reviewed, + differences=differences, + ) + + reconciliation_summary.additional_properties = d + return reconciliation_summary + + @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/record_statement_balance_request.py b/robosystems_client/models/record_statement_balance_request.py new file mode 100644 index 0000000..865f61f --- /dev/null +++ b/robosystems_client/models/record_statement_balance_request.py @@ -0,0 +1,124 @@ +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="RecordStatementBalanceRequest") + + +@_attrs_define +class RecordStatementBalanceRequest: + """Record the ending balance of a statement for one account. + + Attributes: + element_id (str): The balance-sheet account the statement is for (a chart-of-accounts element id). + as_of (datetime.date): The statement's ending date. + balance (float): The ending balance as the statement shows it, as a positive number in the account's normal + direction: money in a bank account, or the amount owed on a loan or a card. Negative for the opposite, such as + an overdrawn bank account. + document_id (None | str | Unset): The statement itself, as a document already added with create-document. Kept + on the record as evidence. + note (None | str | Unset): Anything worth keeping with the recorded balance. + """ + + element_id: str + as_of: datetime.date + balance: float + document_id: None | str | Unset = UNSET + note: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + element_id = self.element_id + + as_of = self.as_of.isoformat() + + balance = self.balance + + document_id: None | str | Unset + if isinstance(self.document_id, Unset): + document_id = UNSET + else: + document_id = self.document_id + + note: None | str | Unset + if isinstance(self.note, Unset): + note = UNSET + else: + note = self.note + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "element_id": element_id, + "as_of": as_of, + "balance": balance, + } + ) + if document_id is not UNSET: + field_dict["document_id"] = document_id + if note is not UNSET: + field_dict["note"] = note + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + element_id = d.pop("element_id") + + as_of = datetime.date.fromisoformat(d.pop("as_of")) + + balance = d.pop("balance") + + def _parse_document_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + document_id = _parse_document_id(d.pop("document_id", UNSET)) + + def _parse_note(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + note = _parse_note(d.pop("note", UNSET)) + + record_statement_balance_request = cls( + element_id=element_id, + as_of=as_of, + balance=balance, + document_id=document_id, + note=note, + ) + + record_statement_balance_request.additional_properties = d + return record_statement_balance_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/refresh_reconciliations_request.py b/robosystems_client/models/refresh_reconciliations_request.py new file mode 100644 index 0000000..2e27275 --- /dev/null +++ b/robosystems_client/models/refresh_reconciliations_request.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="RefreshReconciliationsRequest") + + +@_attrs_define +class RefreshReconciliationsRequest: + """Compare each reconciliation at a period end and record the result. + + Attributes: + period (str): Period to reconcile at its last day, as YYYY-MM. + """ + + period: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + period = self.period + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "period": period, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + period = d.pop("period") + + refresh_reconciliations_request = cls( + period=period, + ) + + refresh_reconciliations_request.additional_properties = d + return refresh_reconciliations_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/schedule_metadata_request.py b/robosystems_client/models/schedule_metadata_request.py index 975036f..f3ba242 100644 --- a/robosystems_client/models/schedule_metadata_request.py +++ b/robosystems_client/models/schedule_metadata_request.py @@ -1,5 +1,6 @@ from __future__ import annotations +import datetime from collections.abc import Mapping from typing import Any, TypeVar, cast @@ -24,6 +25,11 @@ class ScheduleMetadataRequest: book value ends at it. Default: 0. useful_life_months (int | Unset): Useful life in months Default: 0. asset_element_id (None | str | Unset): BS asset element for net book value + booked_on (datetime.date | None | Unset): The date the cost went on the books, when that is before the + schedule's first period: a policy paid in December that starts amortizing in January, or an asset bought the + month before it is placed in service. From this date until the first period the schedule carries its full cost, + so the schedule reconciliation does not report that balance as unscheduled. Leave it out when the cost is booked + in the first period. periodic_amounts (list[int] | None | Unset): Explicit per-period amounts in cents. When set, the generator uses these values instead of `monthly_amount` — enabling non-straight-line schedules (effective-interest bond discount amortization, day-count interest accrual, variable lease payments, pre-computed effective-yield curves, @@ -37,6 +43,7 @@ class ScheduleMetadataRequest: residual_value: int | Unset = 0 useful_life_months: int | Unset = 0 asset_element_id: None | str | Unset = UNSET + booked_on: datetime.date | None | Unset = UNSET periodic_amounts: list[int] | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -55,6 +62,14 @@ def to_dict(self) -> dict[str, Any]: else: asset_element_id = self.asset_element_id + booked_on: None | str | Unset + if isinstance(self.booked_on, Unset): + booked_on = UNSET + elif isinstance(self.booked_on, datetime.date): + booked_on = self.booked_on.isoformat() + else: + booked_on = self.booked_on + periodic_amounts: list[int] | None | Unset if isinstance(self.periodic_amounts, Unset): periodic_amounts = UNSET @@ -77,6 +92,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["useful_life_months"] = useful_life_months if asset_element_id is not UNSET: field_dict["asset_element_id"] = asset_element_id + if booked_on is not UNSET: + field_dict["booked_on"] = booked_on if periodic_amounts is not UNSET: field_dict["periodic_amounts"] = periodic_amounts @@ -102,6 +119,23 @@ def _parse_asset_element_id(data: object) -> None | str | Unset: asset_element_id = _parse_asset_element_id(d.pop("asset_element_id", UNSET)) + def _parse_booked_on(data: object) -> datetime.date | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + booked_on_type_0 = datetime.date.fromisoformat(data) + + return booked_on_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.date | None | Unset, data) + + booked_on = _parse_booked_on(d.pop("booked_on", UNSET)) + def _parse_periodic_amounts(data: object) -> list[int] | None | Unset: if data is None: return data @@ -125,6 +159,7 @@ def _parse_periodic_amounts(data: object) -> list[int] | None | Unset: residual_value=residual_value, useful_life_months=useful_life_months, asset_element_id=asset_element_id, + booked_on=booked_on, periodic_amounts=periodic_amounts, ) diff --git a/robosystems_client/models/set_reconciliation_policy_request.py b/robosystems_client/models/set_reconciliation_policy_request.py new file mode 100644 index 0000000..64695ad --- /dev/null +++ b/robosystems_client/models/set_reconciliation_policy_request.py @@ -0,0 +1,147 @@ +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="SetReconciliationPolicyRequest") + + +@_attrs_define +class SetReconciliationPolicyRequest: + """Change how much the close cares about one reconciliation. + + Attributes: + structure_id (str): The reconciliation block. + required_for_close (bool | None | Unset): Whether the period's close waits on it. Omit to keep. + materiality (float | None | Unset): A difference up to this amount still counts as reconciled. Omit to keep. + review_required (bool | None | Unset): Whether the close also waits for a sign-off, not just for the two sides + to reconcile. Omit to keep. + separate_reviewer (bool | None | Unset): Whether the person who signs off must be someone other than the person + who ran the comparison. It can only be turned on when the graph has at least two members who can write. Omit to + keep. + """ + + structure_id: str + required_for_close: bool | None | Unset = UNSET + materiality: float | None | Unset = UNSET + review_required: bool | None | Unset = UNSET + separate_reviewer: bool | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + structure_id = self.structure_id + + required_for_close: bool | None | Unset + if isinstance(self.required_for_close, Unset): + required_for_close = UNSET + else: + required_for_close = self.required_for_close + + materiality: float | None | Unset + if isinstance(self.materiality, Unset): + materiality = UNSET + else: + materiality = self.materiality + + review_required: bool | None | Unset + if isinstance(self.review_required, Unset): + review_required = UNSET + else: + review_required = self.review_required + + separate_reviewer: bool | None | Unset + if isinstance(self.separate_reviewer, Unset): + separate_reviewer = UNSET + else: + separate_reviewer = self.separate_reviewer + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "structure_id": structure_id, + } + ) + if required_for_close is not UNSET: + field_dict["required_for_close"] = required_for_close + if materiality is not UNSET: + field_dict["materiality"] = materiality + if review_required is not UNSET: + field_dict["review_required"] = review_required + if separate_reviewer is not UNSET: + field_dict["separate_reviewer"] = separate_reviewer + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + structure_id = d.pop("structure_id") + + def _parse_required_for_close(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + required_for_close = _parse_required_for_close(d.pop("required_for_close", UNSET)) + + def _parse_materiality(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + materiality = _parse_materiality(d.pop("materiality", UNSET)) + + def _parse_review_required(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + review_required = _parse_review_required(d.pop("review_required", UNSET)) + + def _parse_separate_reviewer(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + separate_reviewer = _parse_separate_reviewer(d.pop("separate_reviewer", UNSET)) + + set_reconciliation_policy_request = cls( + structure_id=structure_id, + required_for_close=required_for_close, + materiality=materiality, + review_required=review_required, + separate_reviewer=separate_reviewer, + ) + + set_reconciliation_policy_request.additional_properties = d + return set_reconciliation_policy_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/sign_off_reconciliation_request.py b/robosystems_client/models/sign_off_reconciliation_request.py new file mode 100644 index 0000000..89a9b21 --- /dev/null +++ b/robosystems_client/models/sign_off_reconciliation_request.py @@ -0,0 +1,92 @@ +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="SignOffReconciliationRequest") + + +@_attrs_define +class SignOffReconciliationRequest: + """Sign off a reconciliation for a period as its reviewer. + + Attributes: + structure_id (str): The reconciliation block. + period (str): The period signed off, as YYYY-MM. + note (None | str | Unset): What the reviewer looked at, kept on the sign-off. + """ + + structure_id: str + period: str + note: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + structure_id = self.structure_id + + period = self.period + + note: None | str | Unset + if isinstance(self.note, Unset): + note = UNSET + else: + note = self.note + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "structure_id": structure_id, + "period": period, + } + ) + if note is not UNSET: + field_dict["note"] = note + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + structure_id = d.pop("structure_id") + + period = d.pop("period") + + def _parse_note(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + note = _parse_note(d.pop("note", UNSET)) + + sign_off_reconciliation_request = cls( + structure_id=structure_id, + period=period, + note=note, + ) + + sign_off_reconciliation_request.additional_properties = d + return sign_off_reconciliation_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_legacy_arm_block_type.py b/robosystems_client/models/update_legacy_arm_block_type.py index cee1750..8442ec2 100644 --- a/robosystems_client/models/update_legacy_arm_block_type.py +++ b/robosystems_client/models/update_legacy_arm_block_type.py @@ -8,6 +8,7 @@ class UpdateLegacyArmBlockType(str, Enum): EQUITY_STATEMENT = "equity_statement" INCOME_STATEMENT = "income_statement" METRIC = "metric" + RECONCILIATION = "reconciliation" REGULATORY_DISCLOSURE = "regulatory_disclosure" def __str__(self) -> str: diff --git a/robosystems_client/models/update_schedule_arm.py b/robosystems_client/models/update_schedule_arm.py index 2d10b82..17f4799 100644 --- a/robosystems_client/models/update_schedule_arm.py +++ b/robosystems_client/models/update_schedule_arm.py @@ -34,7 +34,8 @@ class UpdateScheduleArm: entry), then create a fresh schedule via `create-information-block` (`block_type='schedule'`). - Omitted fields are left unchanged. + Omitted fields are left unchanged, and that holds inside + `schedule_metadata` too: name only the fields to change. """ block_type: Literal["schedule"] diff --git a/robosystems_client/models/update_schedule_request.py b/robosystems_client/models/update_schedule_request.py index 5014847..b775579 100644 --- a/robosystems_client/models/update_schedule_request.py +++ b/robosystems_client/models/update_schedule_request.py @@ -30,7 +30,8 @@ class UpdateScheduleRequest: entry), then create a fresh schedule via `create-information-block` (`block_type='schedule'`). - Omitted fields are left unchanged. + Omitted fields are left unchanged, and that holds inside + `schedule_metadata` too: name only the fields to change. Attributes: structure_id (str): diff --git a/tests/test_ledger_client.py b/tests/test_ledger_client.py index e957bfe..4f99542 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -287,6 +287,8 @@ def test_get_fiscal_calendar(self, mock_execute, mock_config, graph_id): "strandedObligationSample": [], "reconcilingItemCount": 0, "reconcilingItemSample": [], + "unreconciledAccountCount": 0, + "unreconciledAccountSample": [], "syncStaleDays": None, "lastCloseAt": None, "initializedAt": "2026-01-01T00:00:00Z", @@ -337,6 +339,8 @@ def test_get_fiscal_calendar_names_blocking_obligations( ], "reconcilingItemCount": 1, "reconcilingItemSample": ["ri_bank_fee_2026-03"], + "unreconciledAccountCount": 1, + "unreconciledAccountSample": ["Equipment Loan (statement): unreconciled"], "syncStaleDays": None, "lastCloseAt": None, "initializedAt": "2026-01-01T00:00:00Z", @@ -423,6 +427,48 @@ def test_close_period(self, mock_op, mock_config, graph_id): body = mock_op.call_args.kwargs["body"] assert body.period == "2026-03" assert body.allow_stale_sync is True + # Left unset, so the server's default applies. + assert "allow_unreconciled_accounts" not in body.to_dict() + + @patch("robosystems_client.clients.ledger_client.op_close_period") + def test_close_period_can_override_the_reconciliation_gate( + self, mock_op, mock_config, graph_id + ): + envelope = _envelope( + "close-period", + { + "period": "2026-03", + "entries_posted": 0, + "target_auto_advanced": False, + "fiscal_calendar": {}, + }, + ) + mock_op.return_value = _mock_response(envelope) + client = LedgerClient(mock_config) + client.close_period(graph_id, "2026-03", allow_unreconciled_accounts=True) + body = mock_op.call_args.kwargs["body"] + assert body.to_dict()["allow_unreconciled_accounts"] is True + + @patch("robosystems_client.clients.ledger_client.op_create_information_block") + def test_create_schedule_carries_the_day_its_cost_was_booked( + self, mock_op, mock_config, graph_id + ): + envelope = _envelope("create-information-block", {"structure_id": "str_1"}) + mock_op.return_value = _mock_response(envelope) + client = LedgerClient(mock_config) + client.create_schedule( + graph_id, + name="Insurance policy", + element_ids=["elem_1"], + period_start="2026-02-01", + period_end="2027-01-31", + monthly_amount=10000, + debit_element_id="elem_insurance", + credit_element_id="elem_prepaid", + booked_on="2026-01-15", + ) + payload = mock_op.call_args.kwargs["body"].payload.to_dict() + assert payload["schedule_metadata"]["booked_on"] == "2026-01-15" @patch("robosystems_client.clients.ledger_client.op_create_information_block") def test_create_schedule(self, mock_op, mock_config, graph_id): @@ -2609,3 +2655,271 @@ def test_initialize_chart_of_accounts_rejects_unknown_template( client = LedgerClient(mock_config) with pytest.raises(ValueError): client.initialize_chart_of_accounts(graph_id, "retail") + + +# ── Reconciliations ──────────────────────────────────────────────────── + +_RECONCILIATION = { + "structure_id": "struct_loan", + "name": "Equipment Loan (statement)", + "scope": "account", + "method": "statement", + "element_id": "elem_loan", + "required_for_close": False, + "materiality": 0.0, + "period": "2026-08", + "as_of": "2026-08-31", + "status": "reconciled", + "unreconciled_difference": 0.0, + "review_required": False, + "separate_reviewer": False, + "components": [], + "differences": [], +} + + +@pytest.mark.unit +class TestLedgerReconciliations: + @patch("robosystems_client.graphql.client.GraphQLClient.execute") + def test_list_reconciliations(self, mock_execute, mock_config, graph_id): + component = { + "name": "Statement ending 2026-08-31", + "amount": 4800.0, + "structureId": None, + "eventId": "evt_1", + "documentId": None, + "note": None, + } + mock_execute.return_value = { + "reconciliations": { + "period": "2026-08", + "asOf": "2026-08-31", + "notes": [], + "reconciliations": [ + { + "structureId": "struct_loan", + "name": "Equipment Loan (statement)", + "scope": "account", + "method": "statement", + "elementId": "elem_loan", + "requiredForClose": False, + "materiality": 0.0, + "period": "2026-08", + "asOf": "2026-08-31", + "status": "unreconciled", + "unreconciledDifference": 200.0, + "accountsCompared": None, + "accountsDifferent": None, + "ledgerBalance": 5000.0, + "independentBalance": 4800.0, + "balanceAsOf": "2026-08-31", + "components": [component], + "source": "statement", + "comparedAt": "2026-09-02T08:00:00Z", + "factSetId": "fs_1", + "comparedBy": "usr_1", + "comparedVia": "operation", + "reviewRequired": False, + "separateReviewer": False, + "reviewedBy": None, + "reviewedAt": None, + "selfReviewed": None, + "differences": [ + { + "elementId": "elem_loan", + "accountCode": "2100", + "accountName": "Equipment Loan", + "sourceAccountId": None, + "statement": "balance_sheet", + "ledgerBalance": 5000.0, + "independentBalance": 4800.0, + "difference": 200.0, + "status": "different", + "asOf": "2026-08-31", + "components": [component], + } + ], + } + ], + } + } + client = LedgerClient(mock_config) + + result = client.list_reconciliations(graph_id, "2026-08") + + assert result is not None + (rec,) = result.reconciliations + assert (rec.status, rec.unreconciled_difference) == ("unreconciled", 200.0) + assert rec.components[0].event_id == "evt_1" + assert rec.differences[0].account_name == "Equipment Loan" + assert mock_execute.call_args.args[2] == {"period": "2026-08"} + + @patch("robosystems_client.graphql.client.GraphQLClient.execute") + def test_list_reconciliations_on_a_graph_without_a_ledger( + self, mock_execute, mock_config, graph_id + ): + mock_execute.return_value = {"reconciliations": None} + client = LedgerClient(mock_config) + assert client.list_reconciliations(graph_id, "2026-08") is None + + @patch("robosystems_client.clients.ledger_client.op_preview_reconciliations") + def test_preview_reconciliations_names_the_check( + self, mock_op, mock_config, graph_id + ): + envelope = _envelope( + "preview-reconciliations", {"period": "2026-08", "accounts_different": 1} + ) + mock_op.return_value = _mock_response(envelope) + client = LedgerClient(mock_config) + + result = client.preview_reconciliations( + graph_id, "2026-08", method="schedule_register", include_tied=True + ) + + assert result["accounts_different"] == 1 + body = mock_op.call_args.kwargs["body"].to_dict() + assert body == { + "period": "2026-08", + "method": "schedule_register", + "include_tied": True, + } + + @patch("robosystems_client.clients.ledger_client.op_preview_reconciliations") + def test_preview_reconciliations_leaves_the_method_to_the_server( + self, mock_op, mock_config, graph_id + ): + mock_op.return_value = _mock_response( + _envelope("preview-reconciliations", {"period": "2026-08"}) + ) + client = LedgerClient(mock_config) + + client.preview_reconciliations(graph_id, "2026-08") + + assert mock_op.call_args.kwargs["body"].to_dict() == {"period": "2026-08"} + + def test_preview_reconciliations_refuses_an_unknown_check( + self, mock_config, graph_id + ): + client = LedgerClient(mock_config) + with pytest.raises(ValueError): + client.preview_reconciliations(graph_id, "2026-08", method="guesswork") + + @patch("robosystems_client.clients.ledger_client.op_refresh_reconciliations") + def test_refresh_reconciliations(self, mock_op, mock_config, graph_id): + envelope = _envelope( + "refresh-reconciliations", + { + "period": "2026-08", + "as_of": "2026-08-31", + "notes": ["Source ledger (QuickBooks) was not compared."], + "reconciliations": [_RECONCILIATION], + }, + ) + mock_op.return_value = _mock_response(envelope) + client = LedgerClient(mock_config) + + result = client.refresh_reconciliations(graph_id, "2026-08") + + assert result["notes"] == ["Source ledger (QuickBooks) was not compared."] + assert mock_op.call_args.kwargs["body"].period == "2026-08" + + @patch("robosystems_client.clients.ledger_client.op_record_statement_balance") + def test_record_statement_balance(self, mock_op, mock_config, graph_id): + mock_op.return_value = _mock_response( + _envelope("record-statement-balance", _RECONCILIATION) + ) + client = LedgerClient(mock_config) + + result = client.record_statement_balance( + graph_id, + element_id="elem_loan", + as_of="2026-08-31", + balance=4800.0, + document_id="doc_1", + ) + + assert result["status"] == "reconciled" + assert mock_op.call_args.kwargs["body"].to_dict() == { + "element_id": "elem_loan", + "as_of": "2026-08-31", + "balance": 4800.0, + "document_id": "doc_1", + } + + @patch("robosystems_client.clients.ledger_client.op_set_reconciliation_policy") + def test_set_reconciliation_policy_sends_only_what_changes( + self, mock_op, mock_config, graph_id + ): + envelope = _envelope( + "set-reconciliation-policy", + { + "structure_id": "struct_loan", + "required_for_close": True, + "materiality": 0.0, + "review_required": False, + "separate_reviewer": False, + }, + ) + mock_op.return_value = _mock_response(envelope) + client = LedgerClient(mock_config) + + result = client.set_reconciliation_policy( + graph_id, "struct_loan", required_for_close=True + ) + + assert result["required_for_close"] is True + assert mock_op.call_args.kwargs["body"].to_dict() == { + "structure_id": "struct_loan", + "required_for_close": True, + } + + @patch("robosystems_client.clients.ledger_client.op_set_reconciliation_policy") + def test_set_reconciliation_policy_can_turn_a_setting_off( + self, mock_op, mock_config, graph_id + ): + mock_op.return_value = _mock_response( + _envelope("set-reconciliation-policy", {"structure_id": "struct_loan"}) + ) + client = LedgerClient(mock_config) + + client.set_reconciliation_policy( + graph_id, "struct_loan", required_for_close=False, materiality=0.0 + ) + + assert mock_op.call_args.kwargs["body"].to_dict() == { + "structure_id": "struct_loan", + "required_for_close": False, + "materiality": 0.0, + } + + @patch("robosystems_client.clients.ledger_client.op_sign_off_reconciliation") + def test_sign_off_reconciliation(self, mock_op, mock_config, graph_id): + mock_op.return_value = _mock_response( + _envelope( + "sign-off-reconciliation", + {**_RECONCILIATION, "status": "reviewed", "reviewed_by": "usr_2"}, + ) + ) + client = LedgerClient(mock_config) + + result = client.sign_off_reconciliation( + graph_id, "struct_loan", "2026-08", note="Agreed to the statement" + ) + + assert (result["status"], result["reviewed_by"]) == ("reviewed", "usr_2") + assert mock_op.call_args.kwargs["body"].to_dict() == { + "structure_id": "struct_loan", + "period": "2026-08", + "note": "Agreed to the statement", + } + + @patch("robosystems_client.clients.ledger_client.op_refresh_reconciliations") + def test_a_refusal_carries_the_server_response(self, mock_op, mock_config, graph_id): + resp = Mock() + resp.status_code = HTTPStatus.CONFLICT + resp.content = b'{"detail":"Nothing to reconcile."}' + mock_op.return_value = resp + client = LedgerClient(mock_config) + + with pytest.raises(RuntimeError, match="Nothing to reconcile"): + client.refresh_reconciliations(graph_id, "2026-08") From 1311e7b63d0baaaf5ab52e7e31bcd5870cf47fc7 Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Thu, 1 Oct 2026 23:42:53 -0500 Subject: [PATCH 2/2] feat: default report downloads to the holon The holon carries the whole report; the Tavi has no home for the Information Block payloads, definition arcs, framework pins or filing lifecycle. Pass format="tavi" for the compiled model. --- robosystems_client/clients/ledger_client.py | 10 +++++----- tests/test_ledger_client.py | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/robosystems_client/clients/ledger_client.py b/robosystems_client/clients/ledger_client.py index 641864a..eec97e0 100644 --- a/robosystems_client/clients/ledger_client.py +++ b/robosystems_client/clients/ledger_client.py @@ -2481,7 +2481,7 @@ def download_report_bundle( graph_id: str, report_id: str, *, - format: str = "tavi", + format: str = "holon-jsonld", to: str | Path | None = None, expires_in: int = 300, ) -> ReportBundleDownload: @@ -2497,10 +2497,10 @@ def download_report_bundle( Args: graph_id: Graph identifier owning the Report. report_id: Report identifier (``rpt_``-prefixed ULID). - format: Serialization flavor — ``"tavi"`` (default, the Project - Tavi compiled model stamped at publish), ``"holon-jsonld"`` - (the dataset-form scene/boundary/projection holon), or - ``"xbrl-2.1"``. The enum names ``"TAVI"`` / + format: Serialization flavor — ``"holon-jsonld"`` (default, the + dataset-form scene/boundary/projection holon, which carries + the whole report), ``"tavi"`` (the Project Tavi compiled + model stamped at publish), or ``"xbrl-2.1"``. The enum names ``"TAVI"`` / ``"HOLON_JSONLD"`` / ``"XBRL_2_1"`` are also accepted. to: Optional file path to write the bytes to. When set, the returned ``ReportBundleDownload.path`` points at the diff --git a/tests/test_ledger_client.py b/tests/test_ledger_client.py index 4f99542..7a44aed 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -2187,25 +2187,26 @@ def _patch_httpx(self, mock_client_cls, artifact: Mock) -> Mock: return mock_client @patch("robosystems_client.clients.ledger_client.httpx.Client") - def test_default_download_is_the_tavi(self, mock_client_cls, mock_config, graph_id): - """No format: the Tavi anchor. GraphQL resolves the URL, client follows it.""" + def test_default_download_is_the_holon(self, mock_client_cls, mock_config, graph_id): + """No format: the holon. GraphQL resolves the URL, client follows it.""" mock_client = self._patch_httpx( - mock_client_cls, self._mock_artifact(b'{"xbrlModel": {}}', "rpt_01-g2.tavi.json") + mock_client_cls, + self._mock_artifact(b'{"@graph": []}', "rpt_01-g2.holon.jsonld"), ) with patch.object( LedgerClient, "_query", - return_value=self._gql_data("application/json", "tavi", gen=2), + return_value=self._gql_data("application/ld+json", "holon-jsonld", gen=2), ) as mock_query: result = LedgerClient(mock_config).download_report_bundle(graph_id, "rpt_01") - assert result.content == b'{"xbrlModel": {}}' - assert result.filename == "rpt_01-g2.tavi.json" - assert result.format == "tavi" - assert result.content_type == "application/json" + assert result.content == b'{"@graph": []}' + assert result.filename == "rpt_01-g2.holon.jsonld" + assert result.format == "holon-jsonld" + assert result.content_type == "application/ld+json" assert result.generation_count == 2 # GraphQL var carries the enum NAME, not the wire flavor string. - assert mock_query.call_args.args[2]["format"] == "TAVI" + assert mock_query.call_args.args[2]["format"] == "HOLON_JSONLD" # The bytes come from following the presigned URL. assert mock_client.get.call_args.args[0].startswith("https://s3.example.com/")