From 769c9ca553b0390f08aa378ac5f6bd2fc5dc99bd Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Wed, 21 Oct 2026 23:21:37 -0500 Subject: [PATCH 1/2] feat(ledger): list_elements can include retired accounts `include_inactive` asks the elements read for retired accounts beside the active ones. Default unchanged. Needs robosystems #1923; the twin of typescript-client #294. Claude-Session: https://claude.ai/code/session_01VPxBBHkqhwkZ6Eq8EXSBR6 --- robosystems_client/clients/ledger_client.py | 5 ++++- robosystems_client/graphql/generated/client.py | 2 ++ robosystems_client/graphql/generated/operations.py | 3 ++- .../graphql/operations/ledger/ListLedgerElements.graphql | 2 ++ robosystems_client/graphql/schema.graphql | 5 +++++ tests/test_ledger_client.py | 1 + 6 files changed, 16 insertions(+), 2 deletions(-) diff --git a/robosystems_client/clients/ledger_client.py b/robosystems_client/clients/ledger_client.py index 7315f253..6640cf08 100644 --- a/robosystems_client/clients/ledger_client.py +++ b/robosystems_client/clients/ledger_client.py @@ -1565,10 +1565,12 @@ def list_elements( source: str | None = None, classification: str | None = None, is_abstract: bool | None = None, + include_inactive: bool = False, limit: int = 100, offset: int = 0, ) -> LedgerElementsPage | None: - """List elements (CoA accounts, GAAP concepts, etc.) with filters.""" + """List elements (CoA accounts, GAAP concepts, etc.) with filters; + ``include_inactive`` adds retired accounts, to reactivate one.""" data = self._query( graph_id, LIST_LEDGER_ELEMENTS_GQL, @@ -1577,6 +1579,7 @@ def list_elements( "source": source, "classification": classification, "isAbstract": is_abstract, + "includeInactive": include_inactive, "limit": limit, "offset": offset, }, diff --git a/robosystems_client/graphql/generated/client.py b/robosystems_client/graphql/generated/client.py index 89673a2d..44e74db8 100644 --- a/robosystems_client/graphql/generated/client.py +++ b/robosystems_client/graphql/generated/client.py @@ -907,6 +907,7 @@ def list_ledger_dimensions( def list_ledger_elements( self, + include_inactive: bool, limit: int, offset: int, taxonomy_id: Union[Optional[str], UnsetType] = UNSET, @@ -920,6 +921,7 @@ def list_ledger_elements( "source": source, "classification": classification, "isAbstract": is_abstract, + "includeInactive": include_inactive, "limit": limit, "offset": offset, } diff --git a/robosystems_client/graphql/generated/operations.py b/robosystems_client/graphql/generated/operations.py index 94985c1e..264b9d18 100644 --- a/robosystems_client/graphql/generated/operations.py +++ b/robosystems_client/graphql/generated/operations.py @@ -1941,12 +1941,13 @@ """ LIST_LEDGER_ELEMENTS_GQL = """ -query ListLedgerElements($taxonomyId: String, $source: String, $classification: String, $isAbstract: Boolean, $limit: Int! = 100, $offset: Int! = 0) { +query ListLedgerElements($taxonomyId: String, $source: String, $classification: String, $isAbstract: Boolean, $includeInactive: Boolean! = false, $limit: Int! = 100, $offset: Int! = 0) { elements( taxonomyId: $taxonomyId source: $source classification: $classification isAbstract: $isAbstract + includeInactive: $includeInactive limit: $limit offset: $offset ) { diff --git a/robosystems_client/graphql/operations/ledger/ListLedgerElements.graphql b/robosystems_client/graphql/operations/ledger/ListLedgerElements.graphql index 9ab7b1ed..fe1cbdc6 100644 --- a/robosystems_client/graphql/operations/ledger/ListLedgerElements.graphql +++ b/robosystems_client/graphql/operations/ledger/ListLedgerElements.graphql @@ -3,6 +3,7 @@ query ListLedgerElements( $source: String $classification: String $isAbstract: Boolean + $includeInactive: Boolean! = false $limit: Int! = 100 $offset: Int! = 0 ) { @@ -11,6 +12,7 @@ query ListLedgerElements( source: $source classification: $classification isAbstract: $isAbstract + includeInactive: $includeInactive limit: $limit offset: $offset ) { diff --git a/robosystems_client/graphql/schema.graphql b/robosystems_client/graphql/schema.graphql index e6e5a930..bfe7d4e6 100644 --- a/robosystems_client/graphql/schema.graphql +++ b/robosystems_client/graphql/schema.graphql @@ -559,6 +559,11 @@ type Query { """ isAbstract: Boolean = null + """ + Include retired accounts (`isActive: false`), so one can be found and reactivated. Omit for active elements only. + """ + includeInactive: Boolean! = false + """ Maximum rows to return, 1-1000. Omit to use this field's own default; out of range raises `INVALID_PAGINATION`. """ diff --git a/tests/test_ledger_client.py b/tests/test_ledger_client.py index b15a856e..fc758d10 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -1841,6 +1841,7 @@ def test_list_elements(self, mock_execute, mock_config, graph_id): variables = mock_execute.call_args[0][2] assert variables["taxonomyId"] == "tax_coa_1" assert variables["classification"] == "asset" + assert variables["includeInactive"] is False @patch("robosystems_client.graphql.client.GraphQLClient.execute") def test_list_unmapped_elements(self, mock_execute, mock_config, graph_id): From d37987ed1d4fcf86fd358b593c7c25bb4f042e60 Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Wed, 21 Oct 2026 23:32:01 -0500 Subject: [PATCH 2/2] feat(sdk): preview-schedule and the schedule fields from robosystems #1922 Regenerated from #1922's OpenAPI (with main merged in): the `preview_schedule` operation, loan terms, schedule legs, the roll-forward rows, rebuild-schedule's editable fields and the envelope's definition; `monthly_amount` becomes optional on create. Generated tier only. Claude-Session: https://claude.ai/code/session_01VPxBBHkqhwkZ6Eq8EXSBR6 --- .../preview_schedule.py | 294 ++++++++++++++++++ .../rebuild_schedule.py | 52 +++- robosystems_client/models/__init__.py | 32 ++ .../models/create_schedule_request.py | 18 +- .../models/entry_template_request.py | 38 ++- .../models/loan_terms_request.py | 259 +++++++++++++++ .../models/loan_terms_request_day_count.py | 10 + .../loan_terms_request_payment_frequency.py | 11 + ...tion_envelope_preview_schedule_response.py | 158 ++++++++++ ...velope_preview_schedule_response_status.py | 10 + .../models/preview_schedule_request.py | 273 ++++++++++++++++ .../models/preview_schedule_response.py | 147 +++++++++ .../models/rebuild_schedule_request.py | 163 +++++++++- .../models/schedule_created_response.py | 12 + .../models/schedule_leg_request.py | 83 +++++ .../schedule_leg_request_amount_source.py | 11 + .../models/schedule_leg_request_side.py | 9 + .../models/schedule_mechanics.py | 147 +++++++++ .../models/schedule_metadata_request.py | 44 ++- .../models/schedule_replacement_summary.py | 111 +++++++ .../schedule_replacement_summary_path.py | 9 + .../models/schedule_roll_forward_row.py | 189 +++++++++++ .../models/schedule_roll_forward_row_scope.py | 9 + 23 files changed, 2071 insertions(+), 18 deletions(-) create mode 100644 robosystems_client/api/robo_ledger_fiscal_close/preview_schedule.py create mode 100644 robosystems_client/models/loan_terms_request.py create mode 100644 robosystems_client/models/loan_terms_request_day_count.py create mode 100644 robosystems_client/models/loan_terms_request_payment_frequency.py create mode 100644 robosystems_client/models/operation_envelope_preview_schedule_response.py create mode 100644 robosystems_client/models/operation_envelope_preview_schedule_response_status.py create mode 100644 robosystems_client/models/preview_schedule_request.py create mode 100644 robosystems_client/models/preview_schedule_response.py create mode 100644 robosystems_client/models/schedule_leg_request.py create mode 100644 robosystems_client/models/schedule_leg_request_amount_source.py create mode 100644 robosystems_client/models/schedule_leg_request_side.py create mode 100644 robosystems_client/models/schedule_replacement_summary.py create mode 100644 robosystems_client/models/schedule_replacement_summary_path.py create mode 100644 robosystems_client/models/schedule_roll_forward_row.py create mode 100644 robosystems_client/models/schedule_roll_forward_row_scope.py diff --git a/robosystems_client/api/robo_ledger_fiscal_close/preview_schedule.py b/robosystems_client/api/robo_ledger_fiscal_close/preview_schedule.py new file mode 100644 index 00000000..c0645c3a --- /dev/null +++ b/robosystems_client/api/robo_ledger_fiscal_close/preview_schedule.py @@ -0,0 +1,294 @@ +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_preview_schedule_response import ( + OperationEnvelopePreviewScheduleResponse, +) +from ...models.preview_schedule_request import PreviewScheduleRequest +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + graph_id: str, + *, + body: PreviewScheduleRequest, + 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-schedule".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 | OperationEnvelopePreviewScheduleResponse | None: + if response.status_code == 200: + response_200 = OperationEnvelopePreviewScheduleResponse.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 | OperationEnvelopePreviewScheduleResponse]: + 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: PreviewScheduleRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopePreviewScheduleResponse]: + """Preview Schedule + + Generate a schedule from a definition without keeping it: the same generator and guards as creating + one (block_type='schedule' on create-information-block), run and rolled back. Returns the envelope + create would leave (its ids provisional) and the period-by-period roll-forward: the balance the + schedule carries on the account it credits (a loan's: the note), opening, the period's amount, + closing, and for a loan the interest, payment, principal and current portion. Show it to the person + before asking to create. A definition create would refuse (a roll-forward that climbs where it + should fall, legs that do not balance) is refused here with the period that breaks. `warnings` names + any active schedule already posting the same accounts in the same months. With + `replaces_structure_id`, `replaces` says what the schedule being edited or recreated has posted, by + month, and the path the change takes: `rebuild` (rebuild-schedule with the new definition) while + nothing has posted, `terminate_and_succeed` after. + + **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 (PreviewScheduleRequest): A schedule's definition, generated without being kept. + + 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 | OperationEnvelopePreviewScheduleResponse] + """ + + 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: PreviewScheduleRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopePreviewScheduleResponse | None: + """Preview Schedule + + Generate a schedule from a definition without keeping it: the same generator and guards as creating + one (block_type='schedule' on create-information-block), run and rolled back. Returns the envelope + create would leave (its ids provisional) and the period-by-period roll-forward: the balance the + schedule carries on the account it credits (a loan's: the note), opening, the period's amount, + closing, and for a loan the interest, payment, principal and current portion. Show it to the person + before asking to create. A definition create would refuse (a roll-forward that climbs where it + should fall, legs that do not balance) is refused here with the period that breaks. `warnings` names + any active schedule already posting the same accounts in the same months. With + `replaces_structure_id`, `replaces` says what the schedule being edited or recreated has posted, by + month, and the path the change takes: `rebuild` (rebuild-schedule with the new definition) while + nothing has posted, `terminate_and_succeed` after. + + **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 (PreviewScheduleRequest): A schedule's definition, generated without being kept. + + 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 | OperationEnvelopePreviewScheduleResponse + """ + + 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: PreviewScheduleRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopePreviewScheduleResponse]: + """Preview Schedule + + Generate a schedule from a definition without keeping it: the same generator and guards as creating + one (block_type='schedule' on create-information-block), run and rolled back. Returns the envelope + create would leave (its ids provisional) and the period-by-period roll-forward: the balance the + schedule carries on the account it credits (a loan's: the note), opening, the period's amount, + closing, and for a loan the interest, payment, principal and current portion. Show it to the person + before asking to create. A definition create would refuse (a roll-forward that climbs where it + should fall, legs that do not balance) is refused here with the period that breaks. `warnings` names + any active schedule already posting the same accounts in the same months. With + `replaces_structure_id`, `replaces` says what the schedule being edited or recreated has posted, by + month, and the path the change takes: `rebuild` (rebuild-schedule with the new definition) while + nothing has posted, `terminate_and_succeed` after. + + **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 (PreviewScheduleRequest): A schedule's definition, generated without being kept. + + 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 | OperationEnvelopePreviewScheduleResponse] + """ + + 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: PreviewScheduleRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopePreviewScheduleResponse | None: + """Preview Schedule + + Generate a schedule from a definition without keeping it: the same generator and guards as creating + one (block_type='schedule' on create-information-block), run and rolled back. Returns the envelope + create would leave (its ids provisional) and the period-by-period roll-forward: the balance the + schedule carries on the account it credits (a loan's: the note), opening, the period's amount, + closing, and for a loan the interest, payment, principal and current portion. Show it to the person + before asking to create. A definition create would refuse (a roll-forward that climbs where it + should fall, legs that do not balance) is refused here with the period that breaks. `warnings` names + any active schedule already posting the same accounts in the same months. With + `replaces_structure_id`, `replaces` says what the schedule being edited or recreated has posted, by + month, and the path the change takes: `rebuild` (rebuild-schedule with the new definition) while + nothing has posted, `terminate_and_succeed` after. + + **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 (PreviewScheduleRequest): A schedule's definition, generated without being kept. + + 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 | OperationEnvelopePreviewScheduleResponse + """ + + 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_fiscal_close/rebuild_schedule.py b/robosystems_client/api/robo_ledger_fiscal_close/rebuild_schedule.py index b6aad38d..36e1f725 100644 --- a/robosystems_client/api/robo_ledger_fiscal_close/rebuild_schedule.py +++ b/robosystems_client/api/robo_ledger_fiscal_close/rebuild_schedule.py @@ -119,7 +119,13 @@ def sync_detailed( regenerates fresh forward facts + a fresh obligation chain from the schedule's stored definition (entry_template / schedule_metadata / monthly_amount / period bounds). The historical-vs-in-scope split is re-derived from the CURRENT fiscal calendar closed_through. Use this to pick up a fixed - generator (e.g. the roll-forward direction fix) without orphaning obligations. + generator (e.g. the roll-forward direction fix) without orphaning obligations. It is also how an + edit is saved while nothing has posted: pass the definition fields to change (period_start, + period_end, monthly_amount, entry_template, schedule_metadata), read back from the schedule's + envelope (artifact.mechanics), and the schedule regenerates from them; omitted fields keep their + stored values. Refused once an entry has posted (the envelope's posted_periods and edit_warning say + so in advance): terminate the schedule and create a successor instead. Run preview-schedule with the + new definition first to see the roll-forward. **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. @@ -143,6 +149,11 @@ def sync_detailed( close state. Use this to pick up a fixed generator (e.g. the roll-forward direction fix) without orphaning obligations. + It is also how an edit is saved while nothing has posted: the fields below + replace the stored definition before it regenerates, as the schedule's + envelope reads them back (`artifact.mechanics`). Omitted fields keep their + stored values, inside `schedule_metadata` too. + 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. @@ -179,7 +190,13 @@ def sync( regenerates fresh forward facts + a fresh obligation chain from the schedule's stored definition (entry_template / schedule_metadata / monthly_amount / period bounds). The historical-vs-in-scope split is re-derived from the CURRENT fiscal calendar closed_through. Use this to pick up a fixed - generator (e.g. the roll-forward direction fix) without orphaning obligations. + generator (e.g. the roll-forward direction fix) without orphaning obligations. It is also how an + edit is saved while nothing has posted: pass the definition fields to change (period_start, + period_end, monthly_amount, entry_template, schedule_metadata), read back from the schedule's + envelope (artifact.mechanics), and the schedule regenerates from them; omitted fields keep their + stored values. Refused once an entry has posted (the envelope's posted_periods and edit_warning say + so in advance): terminate the schedule and create a successor instead. Run preview-schedule with the + new definition first to see the roll-forward. **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. @@ -203,6 +220,11 @@ def sync( close state. Use this to pick up a fixed generator (e.g. the roll-forward direction fix) without orphaning obligations. + It is also how an edit is saved while nothing has posted: the fields below + replace the stored definition before it regenerates, as the schedule's + envelope reads them back (`artifact.mechanics`). Omitted fields keep their + stored values, inside `schedule_metadata` too. + 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. @@ -234,7 +256,13 @@ async def asyncio_detailed( regenerates fresh forward facts + a fresh obligation chain from the schedule's stored definition (entry_template / schedule_metadata / monthly_amount / period bounds). The historical-vs-in-scope split is re-derived from the CURRENT fiscal calendar closed_through. Use this to pick up a fixed - generator (e.g. the roll-forward direction fix) without orphaning obligations. + generator (e.g. the roll-forward direction fix) without orphaning obligations. It is also how an + edit is saved while nothing has posted: pass the definition fields to change (period_start, + period_end, monthly_amount, entry_template, schedule_metadata), read back from the schedule's + envelope (artifact.mechanics), and the schedule regenerates from them; omitted fields keep their + stored values. Refused once an entry has posted (the envelope's posted_periods and edit_warning say + so in advance): terminate the schedule and create a successor instead. Run preview-schedule with the + new definition first to see the roll-forward. **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. @@ -258,6 +286,11 @@ async def asyncio_detailed( close state. Use this to pick up a fixed generator (e.g. the roll-forward direction fix) without orphaning obligations. + It is also how an edit is saved while nothing has posted: the fields below + replace the stored definition before it regenerates, as the schedule's + envelope reads them back (`artifact.mechanics`). Omitted fields keep their + stored values, inside `schedule_metadata` too. + 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. @@ -292,7 +325,13 @@ async def asyncio( regenerates fresh forward facts + a fresh obligation chain from the schedule's stored definition (entry_template / schedule_metadata / monthly_amount / period bounds). The historical-vs-in-scope split is re-derived from the CURRENT fiscal calendar closed_through. Use this to pick up a fixed - generator (e.g. the roll-forward direction fix) without orphaning obligations. + generator (e.g. the roll-forward direction fix) without orphaning obligations. It is also how an + edit is saved while nothing has posted: pass the definition fields to change (period_start, + period_end, monthly_amount, entry_template, schedule_metadata), read back from the schedule's + envelope (artifact.mechanics), and the schedule regenerates from them; omitted fields keep their + stored values. Refused once an entry has posted (the envelope's posted_periods and edit_warning say + so in advance): terminate the schedule and create a successor instead. Run preview-schedule with the + new definition first to see the roll-forward. **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. @@ -316,6 +355,11 @@ async def asyncio( close state. Use this to pick up a fixed generator (e.g. the roll-forward direction fix) without orphaning obligations. + It is also how an edit is saved while nothing has posted: the fields below + replace the stored definition before it regenerates, as the schedule's + envelope reads them back (`artifact.mechanics`). Omitted fields keep their + stored values, inside `schedule_metadata` too. + 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. diff --git a/robosystems_client/models/__init__.py b/robosystems_client/models/__init__.py index 19d2f843..15ce9c26 100644 --- a/robosystems_client/models/__init__.py +++ b/robosystems_client/models/__init__.py @@ -428,6 +428,9 @@ from .live_financial_statement_response import LiveFinancialStatementResponse from .live_statement_fact_row import LiveStatementFactRow from .live_statement_fact_row_origin_type_0 import LiveStatementFactRowOriginType0 +from .loan_terms_request import LoanTermsRequest +from .loan_terms_request_day_count import LoanTermsRequestDayCount +from .loan_terms_request_payment_frequency import LoanTermsRequestPaymentFrequency from .login_request import LoginRequest from .logout_user_response_logoutuser import LogoutUserResponseLogoutuser from .materialize_op import MaterializeOp @@ -715,6 +718,12 @@ from .operation_envelope_preview_event_block_response_status import ( OperationEnvelopePreviewEventBlockResponseStatus, ) +from .operation_envelope_preview_schedule_response import ( + OperationEnvelopePreviewScheduleResponse, +) +from .operation_envelope_preview_schedule_response_status import ( + OperationEnvelopePreviewScheduleResponseStatus, +) from .operation_envelope_promote_obligations_response import ( OperationEnvelopePromoteObligationsResponse, ) @@ -906,6 +915,8 @@ from .preview_reconciliations_request import PreviewReconciliationsRequest from .preview_reconciliations_request_method import PreviewReconciliationsRequestMethod from .preview_reconciling_item_request import PreviewReconcilingItemRequest +from .preview_schedule_request import PreviewScheduleRequest +from .preview_schedule_response import PreviewScheduleResponse from .promote_obligations_request import PromoteObligationsRequest from .promote_obligations_response import PromoteObligationsResponse from .promote_obligations_response_errors_item import ( @@ -998,8 +1009,15 @@ from .schedule_created_response_rule_summary_type_0 import ( ScheduleCreatedResponseRuleSummaryType0, ) +from .schedule_leg_request import ScheduleLegRequest +from .schedule_leg_request_amount_source import ScheduleLegRequestAmountSource +from .schedule_leg_request_side import ScheduleLegRequestSide from .schedule_mechanics import ScheduleMechanics from .schedule_metadata_request import ScheduleMetadataRequest +from .schedule_replacement_summary import ScheduleReplacementSummary +from .schedule_replacement_summary_path import ScheduleReplacementSummaryPath +from .schedule_roll_forward_row import ScheduleRollForwardRow +from .schedule_roll_forward_row_scope import ScheduleRollForwardRowScope from .schema_export_response import SchemaExportResponse from .schema_export_response_data_stats_type_0 import SchemaExportResponseDataStatsType0 from .schema_export_response_schema_definition_type_0 import ( @@ -1565,6 +1583,9 @@ "LiveFinancialStatementResponse", "LiveStatementFactRow", "LiveStatementFactRowOriginType0", + "LoanTermsRequest", + "LoanTermsRequestDayCount", + "LoanTermsRequestPaymentFrequency", "LoginRequest", "LogoutUserResponseLogoutuser", "MaterializeOp", @@ -1686,6 +1707,8 @@ "OperationEnvelopePreviewConsolidationEntriesResponseStatus", "OperationEnvelopePreviewEventBlockResponse", "OperationEnvelopePreviewEventBlockResponseStatus", + "OperationEnvelopePreviewScheduleResponse", + "OperationEnvelopePreviewScheduleResponseStatus", "OperationEnvelopePromoteObligationsResponse", "OperationEnvelopePromoteObligationsResponseStatus", "OperationEnvelopeProposeConsolidationEntriesResponse", @@ -1799,6 +1822,8 @@ "PreviewReconciliationsRequest", "PreviewReconciliationsRequestMethod", "PreviewReconcilingItemRequest", + "PreviewScheduleRequest", + "PreviewScheduleResponse", "PromoteObligationsRequest", "PromoteObligationsResponse", "PromoteObligationsResponseErrorsItem", @@ -1871,8 +1896,15 @@ "RuleVariableLite", "ScheduleCreatedResponse", "ScheduleCreatedResponseRuleSummaryType0", + "ScheduleLegRequest", + "ScheduleLegRequestAmountSource", + "ScheduleLegRequestSide", "ScheduleMechanics", "ScheduleMetadataRequest", + "ScheduleReplacementSummary", + "ScheduleReplacementSummaryPath", + "ScheduleRollForwardRow", + "ScheduleRollForwardRowScope", "SchemaExportResponse", "SchemaExportResponseDataStatsType0", "SchemaExportResponseSchemaDefinitionType0", diff --git a/robosystems_client/models/create_schedule_request.py b/robosystems_client/models/create_schedule_request.py index 59742c6e..906f8474 100644 --- a/robosystems_client/models/create_schedule_request.py +++ b/robosystems_client/models/create_schedule_request.py @@ -26,12 +26,13 @@ class CreateScheduleRequest: qnames) — typically the same debit + credit ids used in entry_template. period_start (datetime.date): First period start period_end (datetime.date): Last period end - monthly_amount (int): Monthly amount in cents entry_template (EntryTemplateRequest): entity_id (None | str | Unset): The entity whose books the schedule belongs to, by id. Omit for the group parent. Its facts, obligations and closing entries land in that entity's books, against that entity's own accounts. taxonomy_id (None | str | Unset): Taxonomy ID (auto-creates if omitted) + monthly_amount (int | Unset): Monthly amount in cents. Required for a straight-line schedule; a custom or loan + schedule takes its amounts from its curve. Default: 0. schedule_metadata (None | ScheduleMetadataRequest | Unset): closed_through (datetime.date | None | Unset): Watermark for onboarding. Facts with period_end ≤ this date are flagged 'historical' and their schedule_entry_due obligations are emitted 'voided', so the close workflow starts @@ -47,10 +48,10 @@ class CreateScheduleRequest: element_ids: list[str] period_start: datetime.date period_end: datetime.date - monthly_amount: int entry_template: EntryTemplateRequest entity_id: None | str | Unset = UNSET taxonomy_id: None | str | Unset = UNSET + monthly_amount: int | Unset = 0 schedule_metadata: None | ScheduleMetadataRequest | Unset = UNSET closed_through: datetime.date | None | Unset = UNSET source_transaction_id: None | str | Unset = UNSET @@ -67,8 +68,6 @@ def to_dict(self) -> dict[str, Any]: period_end = self.period_end.isoformat() - monthly_amount = self.monthly_amount - entry_template = self.entry_template.to_dict() entity_id: None | str | Unset @@ -83,6 +82,8 @@ def to_dict(self) -> dict[str, Any]: else: taxonomy_id = self.taxonomy_id + monthly_amount = self.monthly_amount + schedule_metadata: dict[str, Any] | None | Unset if isinstance(self.schedule_metadata, Unset): schedule_metadata = UNSET @@ -113,7 +114,6 @@ def to_dict(self) -> dict[str, Any]: "element_ids": element_ids, "period_start": period_start, "period_end": period_end, - "monthly_amount": monthly_amount, "entry_template": entry_template, } ) @@ -121,6 +121,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["entity_id"] = entity_id if taxonomy_id is not UNSET: field_dict["taxonomy_id"] = taxonomy_id + if monthly_amount is not UNSET: + field_dict["monthly_amount"] = monthly_amount if schedule_metadata is not UNSET: field_dict["schedule_metadata"] = schedule_metadata if closed_through is not UNSET: @@ -144,8 +146,6 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: period_end = datetime.date.fromisoformat(d.pop("period_end")) - monthly_amount = d.pop("monthly_amount") - entry_template = EntryTemplateRequest.from_dict(d.pop("entry_template")) def _parse_entity_id(data: object) -> None | str | Unset: @@ -166,6 +166,8 @@ def _parse_taxonomy_id(data: object) -> None | str | Unset: taxonomy_id = _parse_taxonomy_id(d.pop("taxonomy_id", UNSET)) + monthly_amount = d.pop("monthly_amount", UNSET) + def _parse_schedule_metadata( data: object, ) -> None | ScheduleMetadataRequest | Unset: @@ -218,10 +220,10 @@ def _parse_source_transaction_id(data: object) -> None | str | Unset: element_ids=element_ids, period_start=period_start, period_end=period_end, - monthly_amount=monthly_amount, entry_template=entry_template, entity_id=entity_id, taxonomy_id=taxonomy_id, + monthly_amount=monthly_amount, schedule_metadata=schedule_metadata, closed_through=closed_through, source_transaction_id=source_transaction_id, diff --git a/robosystems_client/models/entry_template_request.py b/robosystems_client/models/entry_template_request.py index aefa2a56..a508b016 100644 --- a/robosystems_client/models/entry_template_request.py +++ b/robosystems_client/models/entry_template_request.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Any, TypeVar, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -9,6 +9,10 @@ from ..models.entry_template_request_entry_type import EntryTemplateRequestEntryType from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.schedule_leg_request import ScheduleLegRequest + + T = TypeVar("T", bound="EntryTemplateRequest") @@ -19,8 +23,8 @@ class EntryTemplateRequest: debit_element_id (str): CoA element id to debit (e.g. Depreciation Expense). This is a chart-of-accounts element id — the `id` returned by get-unmapped-elements / get-graph-schema — NOT a taxonomy qname. credit_element_id (str): CoA element id to credit (e.g. Accumulated Depreciation). A chart-of-accounts element - id (see get-unmapped-elements), not a taxonomy qname. One template = one debit/credit pair; model a multi- - account entry as several schedules. + id (see get-unmapped-elements), not a taxonomy qname. A straight-line or custom schedule posts this one + debit/credit pair; a loan schedule posts its `legs`. entry_type (EntryTemplateRequestEntryType | Unset): Entry type for generated entries Default: EntryTemplateRequestEntryType.CLOSING. memo_template (str | Unset): Memo template ({structure_name} is replaced) Default: ''. @@ -28,6 +32,12 @@ class EntryTemplateRequest: dimension_ids (list[str] | Unset): The management dimension members (department, class, location, project) every entry the schedule drafts carries on both lines, one per axis: a department's depreciation lands in that department. + legs (list[ScheduleLegRequest] | Unset): The lines of a loan schedule's entry (`method='effective_interest'` + only), each taking one column of the amortization table; each period's lines must balance. Accrue interest and + pay through the bank: debit Interest Expense and credit Accrued Interest, both `interest`. The schedule settles + the payment itself: debit the note `principal`, debit Interest Expense `interest`, credit cash or a due-to + `payment`. `debit_element_id` names the interest debit leg and `credit_element_id` one credit leg. Empty for + every other method, which posts the debit/credit pair. """ debit_element_id: str @@ -38,6 +48,7 @@ class EntryTemplateRequest: memo_template: str | Unset = "" auto_reverse: bool | Unset = False dimension_ids: list[str] | Unset = UNSET + legs: list[ScheduleLegRequest] | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -57,6 +68,13 @@ def to_dict(self) -> dict[str, Any]: if not isinstance(self.dimension_ids, Unset): dimension_ids = self.dimension_ids + legs: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.legs, Unset): + legs = [] + for legs_item_data in self.legs: + legs_item = legs_item_data.to_dict() + legs.append(legs_item) + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -73,11 +91,15 @@ def to_dict(self) -> dict[str, Any]: field_dict["auto_reverse"] = auto_reverse if dimension_ids is not UNSET: field_dict["dimension_ids"] = dimension_ids + if legs is not UNSET: + field_dict["legs"] = legs return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.schedule_leg_request import ScheduleLegRequest + d = dict(src_dict) debit_element_id = d.pop("debit_element_id") @@ -96,6 +118,15 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: dimension_ids = cast(list[str], d.pop("dimension_ids", UNSET)) + _legs = d.pop("legs", UNSET) + legs: list[ScheduleLegRequest] | Unset = UNSET + if _legs is not UNSET: + legs = [] + for legs_item_data in _legs: + legs_item = ScheduleLegRequest.from_dict(legs_item_data) + + legs.append(legs_item) + entry_template_request = cls( debit_element_id=debit_element_id, credit_element_id=credit_element_id, @@ -103,6 +134,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: memo_template=memo_template, auto_reverse=auto_reverse, dimension_ids=dimension_ids, + legs=legs, ) entry_template_request.additional_properties = d diff --git a/robosystems_client/models/loan_terms_request.py b/robosystems_client/models/loan_terms_request.py new file mode 100644 index 00000000..6bdd51b5 --- /dev/null +++ b/robosystems_client/models/loan_terms_request.py @@ -0,0 +1,259 @@ +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 ..models.loan_terms_request_day_count import LoanTermsRequestDayCount +from ..models.loan_terms_request_payment_frequency import ( + LoanTermsRequestPaymentFrequency, +) +from ..types import UNSET, Unset + +T = TypeVar("T", bound="LoanTermsRequest") + + +@_attrs_define +class LoanTermsRequest: + """The terms a loan schedule's amortization table is computed from. + + Attributes: + principal (int): Principal drawn, in cents. + annual_rate (float): Annual interest rate as a fraction: 0.099 for 9.9%. + start_date (datetime.date): The day the principal was drawn; interest accrues from it. Falls in the schedule's + first month. + note_element_id (str): The liability account the principal is carried in. The schedule states its balance each + period, which the schedule reconciliation compares with the ledger. + day_count (LoanTermsRequestDayCount | Unset): How a period's interest counts days: `30/360` (every whole month + is 30 days), `actual/365` or `actual/360` (the days elapsed, over a 365- or 360-day year; actual/360 is the + usual bank line of credit). Default: LoanTermsRequestDayCount.VALUE_0. + payment_frequency (LoanTermsRequestPaymentFrequency | Unset): How often a payment falls due. A payment is + applied at the end of the month it falls in. Default: LoanTermsRequestPaymentFrequency.MONTHLY. + first_payment_date (datetime.date | None | Unset): The first payment's date. Omit for one frequency after the + schedule's first month. + payment_amount (int | None | Unset): Each scheduled payment in cents, interest included. Omit for the level + payment that retires the principal by `maturity_date`, or for the accrued interest when `interest_only`. + interest_only (bool | Unset): Payments settle interest only; the principal is due at maturity. Default: False. + maturity_date (datetime.date | None | Unset): When the remaining principal is due: it is repaid in that month, + which is the schedule's last. Omit for a line of credit with no maturity, whose balance carries past the + schedule. + capitalize_interest (bool | Unset): Interest is added to the note rather than accrued apart (a PIK note): the + balance grows by each period's interest, and payments reduce it whole. Default: False. + current_portion_element_id (None | str | Unset): A current-liability account for the principal due within twelve + months. When set, each period's entry also reclasses between it and `note_element_id` so it holds the next + twelve months' principal. + """ + + principal: int + annual_rate: float + start_date: datetime.date + note_element_id: str + day_count: LoanTermsRequestDayCount | Unset = LoanTermsRequestDayCount.VALUE_0 + payment_frequency: LoanTermsRequestPaymentFrequency | Unset = ( + LoanTermsRequestPaymentFrequency.MONTHLY + ) + first_payment_date: datetime.date | None | Unset = UNSET + payment_amount: int | None | Unset = UNSET + interest_only: bool | Unset = False + maturity_date: datetime.date | None | Unset = UNSET + capitalize_interest: bool | Unset = False + current_portion_element_id: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + principal = self.principal + + annual_rate = self.annual_rate + + start_date = self.start_date.isoformat() + + note_element_id = self.note_element_id + + day_count: str | Unset = UNSET + if not isinstance(self.day_count, Unset): + day_count = self.day_count.value + + payment_frequency: str | Unset = UNSET + if not isinstance(self.payment_frequency, Unset): + payment_frequency = self.payment_frequency.value + + first_payment_date: None | str | Unset + if isinstance(self.first_payment_date, Unset): + first_payment_date = UNSET + elif isinstance(self.first_payment_date, datetime.date): + first_payment_date = self.first_payment_date.isoformat() + else: + first_payment_date = self.first_payment_date + + payment_amount: int | None | Unset + if isinstance(self.payment_amount, Unset): + payment_amount = UNSET + else: + payment_amount = self.payment_amount + + interest_only = self.interest_only + + maturity_date: None | str | Unset + if isinstance(self.maturity_date, Unset): + maturity_date = UNSET + elif isinstance(self.maturity_date, datetime.date): + maturity_date = self.maturity_date.isoformat() + else: + maturity_date = self.maturity_date + + capitalize_interest = self.capitalize_interest + + current_portion_element_id: None | str | Unset + if isinstance(self.current_portion_element_id, Unset): + current_portion_element_id = UNSET + else: + current_portion_element_id = self.current_portion_element_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "principal": principal, + "annual_rate": annual_rate, + "start_date": start_date, + "note_element_id": note_element_id, + } + ) + if day_count is not UNSET: + field_dict["day_count"] = day_count + if payment_frequency is not UNSET: + field_dict["payment_frequency"] = payment_frequency + if first_payment_date is not UNSET: + field_dict["first_payment_date"] = first_payment_date + if payment_amount is not UNSET: + field_dict["payment_amount"] = payment_amount + if interest_only is not UNSET: + field_dict["interest_only"] = interest_only + if maturity_date is not UNSET: + field_dict["maturity_date"] = maturity_date + if capitalize_interest is not UNSET: + field_dict["capitalize_interest"] = capitalize_interest + if current_portion_element_id is not UNSET: + field_dict["current_portion_element_id"] = current_portion_element_id + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + principal = d.pop("principal") + + annual_rate = d.pop("annual_rate") + + start_date = datetime.date.fromisoformat(d.pop("start_date")) + + note_element_id = d.pop("note_element_id") + + _day_count = d.pop("day_count", UNSET) + day_count: LoanTermsRequestDayCount | Unset + if isinstance(_day_count, Unset): + day_count = UNSET + else: + day_count = LoanTermsRequestDayCount(_day_count) + + _payment_frequency = d.pop("payment_frequency", UNSET) + payment_frequency: LoanTermsRequestPaymentFrequency | Unset + if isinstance(_payment_frequency, Unset): + payment_frequency = UNSET + else: + payment_frequency = LoanTermsRequestPaymentFrequency(_payment_frequency) + + def _parse_first_payment_date(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() + first_payment_date_type_0 = datetime.date.fromisoformat(data) + + return first_payment_date_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.date | None | Unset, data) + + first_payment_date = _parse_first_payment_date(d.pop("first_payment_date", UNSET)) + + def _parse_payment_amount(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + payment_amount = _parse_payment_amount(d.pop("payment_amount", UNSET)) + + interest_only = d.pop("interest_only", UNSET) + + def _parse_maturity_date(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() + maturity_date_type_0 = datetime.date.fromisoformat(data) + + return maturity_date_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.date | None | Unset, data) + + maturity_date = _parse_maturity_date(d.pop("maturity_date", UNSET)) + + capitalize_interest = d.pop("capitalize_interest", UNSET) + + def _parse_current_portion_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) + + current_portion_element_id = _parse_current_portion_element_id( + d.pop("current_portion_element_id", UNSET) + ) + + loan_terms_request = cls( + principal=principal, + annual_rate=annual_rate, + start_date=start_date, + note_element_id=note_element_id, + day_count=day_count, + payment_frequency=payment_frequency, + first_payment_date=first_payment_date, + payment_amount=payment_amount, + interest_only=interest_only, + maturity_date=maturity_date, + capitalize_interest=capitalize_interest, + current_portion_element_id=current_portion_element_id, + ) + + loan_terms_request.additional_properties = d + return loan_terms_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/loan_terms_request_day_count.py b/robosystems_client/models/loan_terms_request_day_count.py new file mode 100644 index 00000000..78cea6d2 --- /dev/null +++ b/robosystems_client/models/loan_terms_request_day_count.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class LoanTermsRequestDayCount(str, Enum): + ACTUAL360 = "actual/360" + ACTUAL365 = "actual/365" + VALUE_0 = "30/360" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/loan_terms_request_payment_frequency.py b/robosystems_client/models/loan_terms_request_payment_frequency.py new file mode 100644 index 00000000..9fdfa3bd --- /dev/null +++ b/robosystems_client/models/loan_terms_request_payment_frequency.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class LoanTermsRequestPaymentFrequency(str, Enum): + ANNUAL = "annual" + AT_MATURITY = "at_maturity" + MONTHLY = "monthly" + QUARTERLY = "quarterly" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/operation_envelope_preview_schedule_response.py b/robosystems_client/models/operation_envelope_preview_schedule_response.py new file mode 100644 index 00000000..945cd727 --- /dev/null +++ b/robosystems_client/models/operation_envelope_preview_schedule_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_preview_schedule_response_status import ( + OperationEnvelopePreviewScheduleResponseStatus, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.preview_schedule_response import PreviewScheduleResponse + + +T = TypeVar("T", bound="OperationEnvelopePreviewScheduleResponse") + + +@_attrs_define +class OperationEnvelopePreviewScheduleResponse: + """ + Attributes: + operation (str): Kebab-case operation name + operation_id (str): op_-prefixed ULID for audit and SSE correlation + status (OperationEnvelopePreviewScheduleResponseStatus): Operation lifecycle state + at (str): ISO-8601 UTC timestamp + result (None | PreviewScheduleResponse | 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: OperationEnvelopePreviewScheduleResponseStatus + at: str + result: None | PreviewScheduleResponse | 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.preview_schedule_response import PreviewScheduleResponse + + 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, PreviewScheduleResponse): + 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.preview_schedule_response import PreviewScheduleResponse + + d = dict(src_dict) + operation = d.pop("operation") + + operation_id = d.pop("operationId") + + status = OperationEnvelopePreviewScheduleResponseStatus(d.pop("status")) + + at = d.pop("at") + + def _parse_result(data: object) -> None | PreviewScheduleResponse | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + result_type_0 = PreviewScheduleResponse.from_dict(data) + + return result_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PreviewScheduleResponse | 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_preview_schedule_response = cls( + operation=operation, + operation_id=operation_id, + status=status, + at=at, + result=result, + created_by=created_by, + idempotent_replay=idempotent_replay, + ) + + operation_envelope_preview_schedule_response.additional_properties = d + return operation_envelope_preview_schedule_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_preview_schedule_response_status.py b/robosystems_client/models/operation_envelope_preview_schedule_response_status.py new file mode 100644 index 00000000..2cfcb071 --- /dev/null +++ b/robosystems_client/models/operation_envelope_preview_schedule_response_status.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class OperationEnvelopePreviewScheduleResponseStatus(str, Enum): + COMPLETED = "completed" + FAILED = "failed" + PENDING = "pending" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/preview_schedule_request.py b/robosystems_client/models/preview_schedule_request.py new file mode 100644 index 00000000..2b419280 --- /dev/null +++ b/robosystems_client/models/preview_schedule_request.py @@ -0,0 +1,273 @@ +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.entry_template_request import EntryTemplateRequest + from ..models.schedule_metadata_request import ScheduleMetadataRequest + + +T = TypeVar("T", bound="PreviewScheduleRequest") + + +@_attrs_define +class PreviewScheduleRequest: + """A schedule's definition, generated without being kept. + + Attributes: + name (str): Schedule name + element_ids (list[str]): CoA element ids the schedule touches (the `id` from get-unmapped-elements, not taxonomy + qnames) — typically the same debit + credit ids used in entry_template. + period_start (datetime.date): First period start + period_end (datetime.date): Last period end + entry_template (EntryTemplateRequest): + entity_id (None | str | Unset): The entity whose books the schedule belongs to, by id. Omit for the group + parent. Its facts, obligations and closing entries land in that entity's books, against that entity's own + accounts. + taxonomy_id (None | str | Unset): Taxonomy ID (auto-creates if omitted) + monthly_amount (int | Unset): Monthly amount in cents. Required for a straight-line schedule; a custom or loan + schedule takes its amounts from its curve. Default: 0. + schedule_metadata (None | ScheduleMetadataRequest | Unset): + closed_through (datetime.date | None | Unset): Watermark for onboarding. Facts with period_end ≤ this date are + flagged 'historical' and their schedule_entry_due obligations are emitted 'voided', so the close workflow starts + drafting at the first open period. Defaults to the calendar's history end: the last day of its closed_through + month (calendar '2026-05' → '2026-05-31'), or the day before its first month when nothing has closed yet. An + earlier date is raised to it, since an obligation there could never post; set a later one only to treat more + months as already booked. + source_transaction_id (None | str | Unset): Free-form reference to the originating GL transaction (e.g. an + import ID, ledger entry ID, or external system key). Stored in artifact_mechanics for audit; no FK constraint. + replaces_structure_id (None | str | Unset): The schedule this definition would replace, when editing or + recreating one: the preview then says what its posted entries mean for the change. + """ + + name: str + element_ids: list[str] + period_start: datetime.date + period_end: datetime.date + entry_template: EntryTemplateRequest + entity_id: None | str | Unset = UNSET + taxonomy_id: None | str | Unset = UNSET + monthly_amount: int | Unset = 0 + schedule_metadata: None | ScheduleMetadataRequest | Unset = UNSET + closed_through: datetime.date | None | Unset = UNSET + source_transaction_id: None | str | Unset = UNSET + replaces_structure_id: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.schedule_metadata_request import ScheduleMetadataRequest + + name = self.name + + element_ids = self.element_ids + + period_start = self.period_start.isoformat() + + period_end = self.period_end.isoformat() + + entry_template = self.entry_template.to_dict() + + entity_id: None | str | Unset + if isinstance(self.entity_id, Unset): + entity_id = UNSET + else: + entity_id = self.entity_id + + taxonomy_id: None | str | Unset + if isinstance(self.taxonomy_id, Unset): + taxonomy_id = UNSET + else: + taxonomy_id = self.taxonomy_id + + monthly_amount = self.monthly_amount + + schedule_metadata: dict[str, Any] | None | Unset + if isinstance(self.schedule_metadata, Unset): + schedule_metadata = UNSET + elif isinstance(self.schedule_metadata, ScheduleMetadataRequest): + schedule_metadata = self.schedule_metadata.to_dict() + else: + schedule_metadata = self.schedule_metadata + + closed_through: None | str | Unset + if isinstance(self.closed_through, Unset): + closed_through = UNSET + elif isinstance(self.closed_through, datetime.date): + closed_through = self.closed_through.isoformat() + else: + closed_through = self.closed_through + + source_transaction_id: None | str | Unset + if isinstance(self.source_transaction_id, Unset): + source_transaction_id = UNSET + else: + source_transaction_id = self.source_transaction_id + + replaces_structure_id: None | str | Unset + if isinstance(self.replaces_structure_id, Unset): + replaces_structure_id = UNSET + else: + replaces_structure_id = self.replaces_structure_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "name": name, + "element_ids": element_ids, + "period_start": period_start, + "period_end": period_end, + "entry_template": entry_template, + } + ) + if entity_id is not UNSET: + field_dict["entity_id"] = entity_id + if taxonomy_id is not UNSET: + field_dict["taxonomy_id"] = taxonomy_id + if monthly_amount is not UNSET: + field_dict["monthly_amount"] = monthly_amount + if schedule_metadata is not UNSET: + field_dict["schedule_metadata"] = schedule_metadata + if closed_through is not UNSET: + field_dict["closed_through"] = closed_through + if source_transaction_id is not UNSET: + field_dict["source_transaction_id"] = source_transaction_id + if replaces_structure_id is not UNSET: + field_dict["replaces_structure_id"] = replaces_structure_id + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.entry_template_request import EntryTemplateRequest + from ..models.schedule_metadata_request import ScheduleMetadataRequest + + d = dict(src_dict) + name = d.pop("name") + + element_ids = cast(list[str], d.pop("element_ids")) + + period_start = datetime.date.fromisoformat(d.pop("period_start")) + + period_end = datetime.date.fromisoformat(d.pop("period_end")) + + entry_template = EntryTemplateRequest.from_dict(d.pop("entry_template")) + + def _parse_entity_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + entity_id = _parse_entity_id(d.pop("entity_id", UNSET)) + + def _parse_taxonomy_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + taxonomy_id = _parse_taxonomy_id(d.pop("taxonomy_id", UNSET)) + + monthly_amount = d.pop("monthly_amount", UNSET) + + def _parse_schedule_metadata( + data: object, + ) -> None | ScheduleMetadataRequest | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + schedule_metadata_type_0 = ScheduleMetadataRequest.from_dict(data) + + return schedule_metadata_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | ScheduleMetadataRequest | Unset, data) + + schedule_metadata = _parse_schedule_metadata(d.pop("schedule_metadata", UNSET)) + + def _parse_closed_through(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() + closed_through_type_0 = datetime.date.fromisoformat(data) + + return closed_through_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.date | None | Unset, data) + + closed_through = _parse_closed_through(d.pop("closed_through", UNSET)) + + def _parse_source_transaction_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_transaction_id = _parse_source_transaction_id( + d.pop("source_transaction_id", UNSET) + ) + + def _parse_replaces_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) + + replaces_structure_id = _parse_replaces_structure_id( + d.pop("replaces_structure_id", UNSET) + ) + + preview_schedule_request = cls( + name=name, + element_ids=element_ids, + period_start=period_start, + period_end=period_end, + entry_template=entry_template, + entity_id=entity_id, + taxonomy_id=taxonomy_id, + monthly_amount=monthly_amount, + schedule_metadata=schedule_metadata, + closed_through=closed_through, + source_transaction_id=source_transaction_id, + replaces_structure_id=replaces_structure_id, + ) + + preview_schedule_request.additional_properties = d + return preview_schedule_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_schedule_response.py b/robosystems_client/models/preview_schedule_response.py new file mode 100644 index 00000000..e4507f69 --- /dev/null +++ b/robosystems_client/models/preview_schedule_response.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.information_block_envelope import InformationBlockEnvelope + from ..models.schedule_replacement_summary import ScheduleReplacementSummary + from ..models.schedule_roll_forward_row import ScheduleRollForwardRow + + +T = TypeVar("T", bound="PreviewScheduleResponse") + + +@_attrs_define +class PreviewScheduleResponse: + """A generated schedule nobody kept: its envelope as create would leave + it, period by period. + + Attributes: + envelope (InformationBlockEnvelope): The Information Block exchange format. + + One envelope per block instance. Carries the block's identity + type, + Information-Model attributes, the Artifact branch (mechanics + + topic/template), and bundled atoms (elements, connections, facts). + Rules / dimensions / FactSet / verification_results are present-but- + empty for blocks where the upstream content (rule engine, FactSet + expand, dimension catalog) has not yet been implemented. + roll_forward (list[ScheduleRollForwardRow] | Unset): + warnings (list[str] | Unset): Things create would go ahead with that deserve a look: an active schedule already + posting to the same accounts over the same months. + replaces (None | ScheduleReplacementSummary | Unset): With `replaces_structure_id`: the schedule being replaced. + """ + + envelope: InformationBlockEnvelope + roll_forward: list[ScheduleRollForwardRow] | Unset = UNSET + warnings: list[str] | Unset = UNSET + replaces: None | ScheduleReplacementSummary | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.schedule_replacement_summary import ScheduleReplacementSummary + + envelope = self.envelope.to_dict() + + roll_forward: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.roll_forward, Unset): + roll_forward = [] + for roll_forward_item_data in self.roll_forward: + roll_forward_item = roll_forward_item_data.to_dict() + roll_forward.append(roll_forward_item) + + warnings: list[str] | Unset = UNSET + if not isinstance(self.warnings, Unset): + warnings = self.warnings + + replaces: dict[str, Any] | None | Unset + if isinstance(self.replaces, Unset): + replaces = UNSET + elif isinstance(self.replaces, ScheduleReplacementSummary): + replaces = self.replaces.to_dict() + else: + replaces = self.replaces + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "envelope": envelope, + } + ) + if roll_forward is not UNSET: + field_dict["roll_forward"] = roll_forward + if warnings is not UNSET: + field_dict["warnings"] = warnings + if replaces is not UNSET: + field_dict["replaces"] = replaces + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.information_block_envelope import InformationBlockEnvelope + from ..models.schedule_replacement_summary import ScheduleReplacementSummary + from ..models.schedule_roll_forward_row import ScheduleRollForwardRow + + d = dict(src_dict) + envelope = InformationBlockEnvelope.from_dict(d.pop("envelope")) + + _roll_forward = d.pop("roll_forward", UNSET) + roll_forward: list[ScheduleRollForwardRow] | Unset = UNSET + if _roll_forward is not UNSET: + roll_forward = [] + for roll_forward_item_data in _roll_forward: + roll_forward_item = ScheduleRollForwardRow.from_dict(roll_forward_item_data) + + roll_forward.append(roll_forward_item) + + warnings = cast(list[str], d.pop("warnings", UNSET)) + + def _parse_replaces(data: object) -> None | ScheduleReplacementSummary | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + replaces_type_0 = ScheduleReplacementSummary.from_dict(data) + + return replaces_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | ScheduleReplacementSummary | Unset, data) + + replaces = _parse_replaces(d.pop("replaces", UNSET)) + + preview_schedule_response = cls( + envelope=envelope, + roll_forward=roll_forward, + warnings=warnings, + replaces=replaces, + ) + + preview_schedule_response.additional_properties = d + return preview_schedule_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/rebuild_schedule_request.py b/robosystems_client/models/rebuild_schedule_request.py index f2087d82..11c65835 100644 --- a/robosystems_client/models/rebuild_schedule_request.py +++ b/robosystems_client/models/rebuild_schedule_request.py @@ -1,11 +1,19 @@ from __future__ import annotations +import datetime from collections.abc import Mapping -from typing import Any, TypeVar +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.entry_template_request import EntryTemplateRequest + from ..models.schedule_metadata_request import ScheduleMetadataRequest + + T = TypeVar("T", bound="RebuildScheduleRequest") @@ -26,16 +34,72 @@ class RebuildScheduleRequest: close state. Use this to pick up a fixed generator (e.g. the roll-forward direction fix) without orphaning obligations. + It is also how an edit is saved while nothing has posted: the fields below + replace the stored definition before it regenerates, as the schedule's + envelope reads them back (`artifact.mechanics`). Omitted fields keep their + stored values, inside `schedule_metadata` too. + Attributes: structure_id (str): The schedule structure to regenerate in place. + period_start (datetime.date | None | Unset): A new first period start. + period_end (datetime.date | None | Unset): A new last period end. + monthly_amount (int | None | Unset): A new straight-line monthly amount, in cents. + entry_template (EntryTemplateRequest | None | Unset): A new entry template, replacing the stored one whole. + schedule_metadata (None | ScheduleMetadataRequest | Unset): The schedule_metadata fields to change. """ structure_id: str + period_start: datetime.date | None | Unset = UNSET + period_end: datetime.date | None | Unset = UNSET + monthly_amount: int | None | Unset = UNSET + entry_template: EntryTemplateRequest | None | Unset = UNSET + schedule_metadata: None | ScheduleMetadataRequest | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + from ..models.entry_template_request import EntryTemplateRequest + from ..models.schedule_metadata_request import ScheduleMetadataRequest + structure_id = self.structure_id + period_start: None | str | Unset + if isinstance(self.period_start, Unset): + period_start = UNSET + elif isinstance(self.period_start, datetime.date): + period_start = self.period_start.isoformat() + else: + period_start = self.period_start + + period_end: None | str | Unset + if isinstance(self.period_end, Unset): + period_end = UNSET + elif isinstance(self.period_end, datetime.date): + period_end = self.period_end.isoformat() + else: + period_end = self.period_end + + monthly_amount: int | None | Unset + if isinstance(self.monthly_amount, Unset): + monthly_amount = UNSET + else: + monthly_amount = self.monthly_amount + + entry_template: dict[str, Any] | None | Unset + if isinstance(self.entry_template, Unset): + entry_template = UNSET + elif isinstance(self.entry_template, EntryTemplateRequest): + entry_template = self.entry_template.to_dict() + else: + entry_template = self.entry_template + + schedule_metadata: dict[str, Any] | None | Unset + if isinstance(self.schedule_metadata, Unset): + schedule_metadata = UNSET + elif isinstance(self.schedule_metadata, ScheduleMetadataRequest): + schedule_metadata = self.schedule_metadata.to_dict() + else: + schedule_metadata = self.schedule_metadata + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -43,16 +107,113 @@ def to_dict(self) -> dict[str, Any]: "structure_id": structure_id, } ) + if period_start is not UNSET: + field_dict["period_start"] = period_start + if period_end is not UNSET: + field_dict["period_end"] = period_end + if monthly_amount is not UNSET: + field_dict["monthly_amount"] = monthly_amount + if entry_template is not UNSET: + field_dict["entry_template"] = entry_template + if schedule_metadata is not UNSET: + field_dict["schedule_metadata"] = schedule_metadata return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.entry_template_request import EntryTemplateRequest + from ..models.schedule_metadata_request import ScheduleMetadataRequest + d = dict(src_dict) structure_id = d.pop("structure_id") + def _parse_period_start(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() + period_start_type_0 = datetime.date.fromisoformat(data) + + return period_start_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.date | None | Unset, data) + + period_start = _parse_period_start(d.pop("period_start", UNSET)) + + def _parse_period_end(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() + period_end_type_0 = datetime.date.fromisoformat(data) + + return period_end_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.date | None | Unset, data) + + period_end = _parse_period_end(d.pop("period_end", UNSET)) + + def _parse_monthly_amount(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + monthly_amount = _parse_monthly_amount(d.pop("monthly_amount", UNSET)) + + def _parse_entry_template(data: object) -> EntryTemplateRequest | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + entry_template_type_0 = EntryTemplateRequest.from_dict(data) + + return entry_template_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(EntryTemplateRequest | None | Unset, data) + + entry_template = _parse_entry_template(d.pop("entry_template", UNSET)) + + def _parse_schedule_metadata( + data: object, + ) -> None | ScheduleMetadataRequest | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + schedule_metadata_type_0 = ScheduleMetadataRequest.from_dict(data) + + return schedule_metadata_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | ScheduleMetadataRequest | Unset, data) + + schedule_metadata = _parse_schedule_metadata(d.pop("schedule_metadata", UNSET)) + rebuild_schedule_request = cls( structure_id=structure_id, + period_start=period_start, + period_end=period_end, + monthly_amount=monthly_amount, + entry_template=entry_template, + schedule_metadata=schedule_metadata, ) rebuild_schedule_request.additional_properties = d diff --git a/robosystems_client/models/schedule_created_response.py b/robosystems_client/models/schedule_created_response.py index 521764c7..84e0bc23 100644 --- a/robosystems_client/models/schedule_created_response.py +++ b/robosystems_client/models/schedule_created_response.py @@ -29,6 +29,8 @@ class ScheduleCreatedResponse: rule_summary (None | ScheduleCreatedResponseRuleSummaryType0 | Unset): schedule_created_event_id (None | str | Unset): pending_event_count (int | Unset): Default: 0. + warnings (list[str] | Unset): What the write went ahead with that deserves a look: another active schedule + posting to the same accounts over the same months. """ structure_id: str @@ -39,6 +41,7 @@ class ScheduleCreatedResponse: rule_summary: None | ScheduleCreatedResponseRuleSummaryType0 | Unset = UNSET schedule_created_event_id: None | str | Unset = UNSET pending_event_count: int | Unset = 0 + warnings: list[str] | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -72,6 +75,10 @@ def to_dict(self) -> dict[str, Any]: pending_event_count = self.pending_event_count + warnings: list[str] | Unset = UNSET + if not isinstance(self.warnings, Unset): + warnings = self.warnings + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -89,6 +96,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["schedule_created_event_id"] = schedule_created_event_id if pending_event_count is not UNSET: field_dict["pending_event_count"] = pending_event_count + if warnings is not UNSET: + field_dict["warnings"] = warnings return field_dict @@ -141,6 +150,8 @@ def _parse_schedule_created_event_id(data: object) -> None | str | Unset: pending_event_count = d.pop("pending_event_count", UNSET) + warnings = cast(list[str], d.pop("warnings", UNSET)) + schedule_created_response = cls( structure_id=structure_id, name=name, @@ -150,6 +161,7 @@ def _parse_schedule_created_event_id(data: object) -> None | str | Unset: rule_summary=rule_summary, schedule_created_event_id=schedule_created_event_id, pending_event_count=pending_event_count, + warnings=warnings, ) schedule_created_response.additional_properties = d diff --git a/robosystems_client/models/schedule_leg_request.py b/robosystems_client/models/schedule_leg_request.py new file mode 100644 index 00000000..cd439970 --- /dev/null +++ b/robosystems_client/models/schedule_leg_request.py @@ -0,0 +1,83 @@ +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.schedule_leg_request_amount_source import ScheduleLegRequestAmountSource +from ..models.schedule_leg_request_side import ScheduleLegRequestSide + +T = TypeVar("T", bound="ScheduleLegRequest") + + +@_attrs_define +class ScheduleLegRequest: + """One line of the entry a loan schedule drafts each period. + + Attributes: + element_id (str): CoA element id the line posts to. + side (ScheduleLegRequestSide): The line's side. + amount_source (ScheduleLegRequestAmountSource): The amortization-table column the line takes each period: + `interest` (accrued in the period), `interest_paid` (accrued interest settled by the period's payment), + `principal` (principal repaid), `payment` (interest_paid + principal). + """ + + element_id: str + side: ScheduleLegRequestSide + amount_source: ScheduleLegRequestAmountSource + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + element_id = self.element_id + + side = self.side.value + + amount_source = self.amount_source.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "element_id": element_id, + "side": side, + "amount_source": amount_source, + } + ) + + 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") + + side = ScheduleLegRequestSide(d.pop("side")) + + amount_source = ScheduleLegRequestAmountSource(d.pop("amount_source")) + + schedule_leg_request = cls( + element_id=element_id, + side=side, + amount_source=amount_source, + ) + + schedule_leg_request.additional_properties = d + return schedule_leg_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_leg_request_amount_source.py b/robosystems_client/models/schedule_leg_request_amount_source.py new file mode 100644 index 00000000..80468bfd --- /dev/null +++ b/robosystems_client/models/schedule_leg_request_amount_source.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class ScheduleLegRequestAmountSource(str, Enum): + INTEREST = "interest" + INTEREST_PAID = "interest_paid" + PAYMENT = "payment" + PRINCIPAL = "principal" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/schedule_leg_request_side.py b/robosystems_client/models/schedule_leg_request_side.py new file mode 100644 index 00000000..465d9c0e --- /dev/null +++ b/robosystems_client/models/schedule_leg_request_side.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class ScheduleLegRequestSide(str, Enum): + CREDIT = "credit" + DEBIT = "debit" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/schedule_mechanics.py b/robosystems_client/models/schedule_mechanics.py index 9b44af22..eab86660 100644 --- a/robosystems_client/models/schedule_mechanics.py +++ b/robosystems_client/models/schedule_mechanics.py @@ -1,5 +1,6 @@ from __future__ import annotations +import datetime from collections.abc import Mapping from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast @@ -33,12 +34,29 @@ class ScheduleMechanics: book-value cross-reference. periods_with_entries (int | Unset): Number of in-scope periods that have at least one closing entry posted. Runtime state derived at envelope-build time from the Entry table. Default: 0. + period_start (datetime.date | None | Unset): The schedule's first period start, as defined. + period_end (datetime.date | None | Unset): The schedule's last period end, as defined. An early end is + `end_date`; rebuild-schedule regenerates to this and ends at it again. + monthly_amount (int | None | Unset): The straight-line monthly amount in cents. With `period_start`, + `period_end`, `entry_template` and `schedule_metadata` this is the whole definition: rebuild-schedule takes the + same fields back. + end_date (datetime.date | None | Unset): Where terminate-schedule ended it, when it did. + posted_periods (list[str] | Unset): The months (YYYY-MM) holding a posted or shadowed closing entry of the + schedule, which no edit may rewrite. + edit_warning (None | str | Unset): Set once an entry has posted: what deleting, rebuilding or recreating the + schedule would do to that history, and the path an edit takes instead. """ entry_template: EntryTemplateRequest kind: Literal["closing_entry_generator"] | Unset = "closing_entry_generator" schedule_metadata: None | ScheduleMetadataRequest | Unset = UNSET periods_with_entries: int | Unset = 0 + period_start: datetime.date | None | Unset = UNSET + period_end: datetime.date | None | Unset = UNSET + monthly_amount: int | None | Unset = UNSET + end_date: datetime.date | None | Unset = UNSET + posted_periods: list[str] | Unset = UNSET + edit_warning: None | str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -58,6 +76,46 @@ def to_dict(self) -> dict[str, Any]: periods_with_entries = self.periods_with_entries + period_start: None | str | Unset + if isinstance(self.period_start, Unset): + period_start = UNSET + elif isinstance(self.period_start, datetime.date): + period_start = self.period_start.isoformat() + else: + period_start = self.period_start + + period_end: None | str | Unset + if isinstance(self.period_end, Unset): + period_end = UNSET + elif isinstance(self.period_end, datetime.date): + period_end = self.period_end.isoformat() + else: + period_end = self.period_end + + monthly_amount: int | None | Unset + if isinstance(self.monthly_amount, Unset): + monthly_amount = UNSET + else: + monthly_amount = self.monthly_amount + + end_date: None | str | Unset + if isinstance(self.end_date, Unset): + end_date = UNSET + elif isinstance(self.end_date, datetime.date): + end_date = self.end_date.isoformat() + else: + end_date = self.end_date + + posted_periods: list[str] | Unset = UNSET + if not isinstance(self.posted_periods, Unset): + posted_periods = self.posted_periods + + edit_warning: None | str | Unset + if isinstance(self.edit_warning, Unset): + edit_warning = UNSET + else: + edit_warning = self.edit_warning + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -71,6 +129,18 @@ def to_dict(self) -> dict[str, Any]: field_dict["schedule_metadata"] = schedule_metadata if periods_with_entries is not UNSET: field_dict["periods_with_entries"] = periods_with_entries + if period_start is not UNSET: + field_dict["period_start"] = period_start + if period_end is not UNSET: + field_dict["period_end"] = period_end + if monthly_amount is not UNSET: + field_dict["monthly_amount"] = monthly_amount + if end_date is not UNSET: + field_dict["end_date"] = end_date + if posted_periods is not UNSET: + field_dict["posted_periods"] = posted_periods + if edit_warning is not UNSET: + field_dict["edit_warning"] = edit_warning return field_dict @@ -107,11 +177,88 @@ def _parse_schedule_metadata( periods_with_entries = d.pop("periods_with_entries", UNSET) + def _parse_period_start(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() + period_start_type_0 = datetime.date.fromisoformat(data) + + return period_start_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.date | None | Unset, data) + + period_start = _parse_period_start(d.pop("period_start", UNSET)) + + def _parse_period_end(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() + period_end_type_0 = datetime.date.fromisoformat(data) + + return period_end_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.date | None | Unset, data) + + period_end = _parse_period_end(d.pop("period_end", UNSET)) + + def _parse_monthly_amount(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + monthly_amount = _parse_monthly_amount(d.pop("monthly_amount", UNSET)) + + def _parse_end_date(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() + end_date_type_0 = datetime.date.fromisoformat(data) + + return end_date_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.date | None | Unset, data) + + end_date = _parse_end_date(d.pop("end_date", UNSET)) + + posted_periods = cast(list[str], d.pop("posted_periods", UNSET)) + + def _parse_edit_warning(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + edit_warning = _parse_edit_warning(d.pop("edit_warning", UNSET)) + schedule_mechanics = cls( entry_template=entry_template, kind=kind, schedule_metadata=schedule_metadata, periods_with_entries=periods_with_entries, + period_start=period_start, + period_end=period_end, + monthly_amount=monthly_amount, + end_date=end_date, + posted_periods=posted_periods, + edit_warning=edit_warning, ) schedule_mechanics.additional_properties = d diff --git a/robosystems_client/models/schedule_metadata_request.py b/robosystems_client/models/schedule_metadata_request.py index f3ba2424..42dc767d 100644 --- a/robosystems_client/models/schedule_metadata_request.py +++ b/robosystems_client/models/schedule_metadata_request.py @@ -2,13 +2,17 @@ import datetime from collections.abc import Mapping -from typing import Any, TypeVar, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.loan_terms_request import LoanTermsRequest + + T = TypeVar("T", bound="ScheduleMetadataRequest") @@ -18,8 +22,11 @@ class ScheduleMetadataRequest: Attributes: method (str | Unset): Calculation method. 'straight_line' (default) distributes `monthly_amount` evenly across periods with the final period absorbing rounding. 'custom' requires `periodic_amounts` — the generator uses - those values verbatim instead of computing. Other strings are labels only; fact values still come from + those values verbatim instead of computing. 'effective_interest' requires `loan_terms` and the template's + `legs`: the generator computes the amortization table (opening balance, interest, payment, principal, closing + balance) and drafts the legs from it. Other strings are labels only; fact values still come from `monthly_amount` or `periodic_amounts`. Default: 'straight_line'. + loan_terms (LoanTermsRequest | None | Unset): The loan's terms, for `method='effective_interest'`. original_amount (int | Unset): Cost basis in cents Default: 0. residual_value (int | Unset): Salvage value in cents. The schedule expenses `original_amount` less this; net book value ends at it. Default: 0. @@ -39,6 +46,7 @@ class ScheduleMetadataRequest: """ method: str | Unset = "straight_line" + loan_terms: LoanTermsRequest | None | Unset = UNSET original_amount: int | Unset = 0 residual_value: int | Unset = 0 useful_life_months: int | Unset = 0 @@ -48,8 +56,18 @@ class ScheduleMetadataRequest: additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + from ..models.loan_terms_request import LoanTermsRequest + method = self.method + loan_terms: dict[str, Any] | None | Unset + if isinstance(self.loan_terms, Unset): + loan_terms = UNSET + elif isinstance(self.loan_terms, LoanTermsRequest): + loan_terms = self.loan_terms.to_dict() + else: + loan_terms = self.loan_terms + original_amount = self.original_amount residual_value = self.residual_value @@ -84,6 +102,8 @@ def to_dict(self) -> dict[str, Any]: field_dict.update({}) if method is not UNSET: field_dict["method"] = method + if loan_terms is not UNSET: + field_dict["loan_terms"] = loan_terms if original_amount is not UNSET: field_dict["original_amount"] = original_amount if residual_value is not UNSET: @@ -101,9 +121,28 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.loan_terms_request import LoanTermsRequest + d = dict(src_dict) method = d.pop("method", UNSET) + def _parse_loan_terms(data: object) -> LoanTermsRequest | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + loan_terms_type_0 = LoanTermsRequest.from_dict(data) + + return loan_terms_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(LoanTermsRequest | None | Unset, data) + + loan_terms = _parse_loan_terms(d.pop("loan_terms", UNSET)) + original_amount = d.pop("original_amount", UNSET) residual_value = d.pop("residual_value", UNSET) @@ -155,6 +194,7 @@ def _parse_periodic_amounts(data: object) -> list[int] | None | Unset: schedule_metadata_request = cls( method=method, + loan_terms=loan_terms, original_amount=original_amount, residual_value=residual_value, useful_life_months=useful_life_months, diff --git a/robosystems_client/models/schedule_replacement_summary.py b/robosystems_client/models/schedule_replacement_summary.py new file mode 100644 index 00000000..ebb68cbb --- /dev/null +++ b/robosystems_client/models/schedule_replacement_summary.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.schedule_replacement_summary_path import ScheduleReplacementSummaryPath +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ScheduleReplacementSummary") + + +@_attrs_define +class ScheduleReplacementSummary: + """What an existing schedule's entries mean for replacing it. + + Attributes: + structure_id (str): + name (str): + posted_entry_count (int): Its posted or shadowed closing entries. + path (ScheduleReplacementSummaryPath): `rebuild` when nothing has posted: rebuild-schedule with the new + definition replaces it in place. `terminate_and_succeed` once an entry has: terminate it after its last posted + month and create the successor from the month after. + posted_periods (list[str] | Unset): The months those entries are in (YYYY-MM). + draft_entry_count (int | Unset): Default: 0. + """ + + structure_id: str + name: str + posted_entry_count: int + path: ScheduleReplacementSummaryPath + posted_periods: list[str] | Unset = UNSET + draft_entry_count: int | Unset = 0 + 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 + + posted_entry_count = self.posted_entry_count + + path = self.path.value + + posted_periods: list[str] | Unset = UNSET + if not isinstance(self.posted_periods, Unset): + posted_periods = self.posted_periods + + draft_entry_count = self.draft_entry_count + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "structure_id": structure_id, + "name": name, + "posted_entry_count": posted_entry_count, + "path": path, + } + ) + if posted_periods is not UNSET: + field_dict["posted_periods"] = posted_periods + if draft_entry_count is not UNSET: + field_dict["draft_entry_count"] = draft_entry_count + + 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") + + name = d.pop("name") + + posted_entry_count = d.pop("posted_entry_count") + + path = ScheduleReplacementSummaryPath(d.pop("path")) + + posted_periods = cast(list[str], d.pop("posted_periods", UNSET)) + + draft_entry_count = d.pop("draft_entry_count", UNSET) + + schedule_replacement_summary = cls( + structure_id=structure_id, + name=name, + posted_entry_count=posted_entry_count, + path=path, + posted_periods=posted_periods, + draft_entry_count=draft_entry_count, + ) + + schedule_replacement_summary.additional_properties = d + return schedule_replacement_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/schedule_replacement_summary_path.py b/robosystems_client/models/schedule_replacement_summary_path.py new file mode 100644 index 00000000..fcb3f7b6 --- /dev/null +++ b/robosystems_client/models/schedule_replacement_summary_path.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class ScheduleReplacementSummaryPath(str, Enum): + REBUILD = "rebuild" + TERMINATE_AND_SUCCEED = "terminate_and_succeed" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/schedule_roll_forward_row.py b/robosystems_client/models/schedule_roll_forward_row.py new file mode 100644 index 00000000..0941fee6 --- /dev/null +++ b/robosystems_client/models/schedule_roll_forward_row.py @@ -0,0 +1,189 @@ +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 ..models.schedule_roll_forward_row_scope import ScheduleRollForwardRowScope +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ScheduleRollForwardRow") + + +@_attrs_define +class ScheduleRollForwardRow: + """One period of a previewed schedule: the balance it carries, and what + moves it. + + Attributes: + period_start (datetime.date): + period_end (datetime.date): + opening (float): The carried balance at the period's start. + amount (float): What the period recognizes: the entry's debit amount. + closing (float): The carried balance at the period's end. + scope (ScheduleRollForwardRowScope): `historical` for a month already in the books' history, which the schedule + records but never drafts; `in_scope` for one a close drafts. + interest (float | None | Unset): Loan schedules: interest accrued. + payment (float | None | Unset): Loan schedules: the payment. + principal (float | None | Unset): Loan schedules: principal repaid. + current_portion (float | None | Unset): Loan schedules with a current-portion account: the principal due in the + next twelve months. + """ + + period_start: datetime.date + period_end: datetime.date + opening: float + amount: float + closing: float + scope: ScheduleRollForwardRowScope + interest: float | None | Unset = UNSET + payment: float | None | Unset = UNSET + principal: float | None | Unset = UNSET + current_portion: float | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + period_start = self.period_start.isoformat() + + period_end = self.period_end.isoformat() + + opening = self.opening + + amount = self.amount + + closing = self.closing + + scope = self.scope.value + + interest: float | None | Unset + if isinstance(self.interest, Unset): + interest = UNSET + else: + interest = self.interest + + payment: float | None | Unset + if isinstance(self.payment, Unset): + payment = UNSET + else: + payment = self.payment + + principal: float | None | Unset + if isinstance(self.principal, Unset): + principal = UNSET + else: + principal = self.principal + + current_portion: float | None | Unset + if isinstance(self.current_portion, Unset): + current_portion = UNSET + else: + current_portion = self.current_portion + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "period_start": period_start, + "period_end": period_end, + "opening": opening, + "amount": amount, + "closing": closing, + "scope": scope, + } + ) + if interest is not UNSET: + field_dict["interest"] = interest + if payment is not UNSET: + field_dict["payment"] = payment + if principal is not UNSET: + field_dict["principal"] = principal + if current_portion is not UNSET: + field_dict["current_portion"] = current_portion + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + period_start = datetime.date.fromisoformat(d.pop("period_start")) + + period_end = datetime.date.fromisoformat(d.pop("period_end")) + + opening = d.pop("opening") + + amount = d.pop("amount") + + closing = d.pop("closing") + + scope = ScheduleRollForwardRowScope(d.pop("scope")) + + def _parse_interest(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + interest = _parse_interest(d.pop("interest", UNSET)) + + def _parse_payment(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + payment = _parse_payment(d.pop("payment", UNSET)) + + def _parse_principal(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + principal = _parse_principal(d.pop("principal", UNSET)) + + def _parse_current_portion(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + current_portion = _parse_current_portion(d.pop("current_portion", UNSET)) + + schedule_roll_forward_row = cls( + period_start=period_start, + period_end=period_end, + opening=opening, + amount=amount, + closing=closing, + scope=scope, + interest=interest, + payment=payment, + principal=principal, + current_portion=current_portion, + ) + + schedule_roll_forward_row.additional_properties = d + return schedule_roll_forward_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/schedule_roll_forward_row_scope.py b/robosystems_client/models/schedule_roll_forward_row_scope.py new file mode 100644 index 00000000..32f31400 --- /dev/null +++ b/robosystems_client/models/schedule_roll_forward_row_scope.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class ScheduleRollForwardRowScope(str, Enum): + HISTORICAL = "historical" + IN_SCOPE = "in_scope" + + def __str__(self) -> str: + return str(self.value)