diff --git a/robosystems_client/api/ledger/close_fiscal_period.py b/robosystems_client/api/ledger/close_fiscal_period.py new file mode 100644 index 0000000..c5f8631 --- /dev/null +++ b/robosystems_client/api/ledger/close_fiscal_period.py @@ -0,0 +1,221 @@ +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.close_period_request import ClosePeriodRequest +from ...models.close_period_response import ClosePeriodResponse +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + graph_id: str, + period: str, + *, + body: ClosePeriodRequest | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/v1/ledger/{graph_id}/periods/{period}/close".format( + graph_id=quote(str(graph_id), safe=""), + period=quote(str(period), safe=""), + ), + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ClosePeriodResponse | HTTPValidationError | None: + if response.status_code == 200: + response_200 = ClosePeriodResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + 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[ClosePeriodResponse | HTTPValidationError]: + 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, + period: str, + *, + client: AuthenticatedClient, + body: ClosePeriodRequest | Unset = UNSET, +) -> Response[ClosePeriodResponse | HTTPValidationError]: + """Close Fiscal Period + + Close a fiscal period — the final commit action. + + All mechanics live in `PeriodCloseService.close()`. This endpoint just + resolves auth + QB sync state, invokes the service, and translates + domain exceptions into HTTP responses. + + Args: + graph_id (str): + period (str): Target period in YYYY-MM format + body (ClosePeriodRequest | Unset): + + 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[ClosePeriodResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + period=period, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + graph_id: str, + period: str, + *, + client: AuthenticatedClient, + body: ClosePeriodRequest | Unset = UNSET, +) -> ClosePeriodResponse | HTTPValidationError | None: + """Close Fiscal Period + + Close a fiscal period — the final commit action. + + All mechanics live in `PeriodCloseService.close()`. This endpoint just + resolves auth + QB sync state, invokes the service, and translates + domain exceptions into HTTP responses. + + Args: + graph_id (str): + period (str): Target period in YYYY-MM format + body (ClosePeriodRequest | Unset): + + 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: + ClosePeriodResponse | HTTPValidationError + """ + + return sync_detailed( + graph_id=graph_id, + period=period, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + graph_id: str, + period: str, + *, + client: AuthenticatedClient, + body: ClosePeriodRequest | Unset = UNSET, +) -> Response[ClosePeriodResponse | HTTPValidationError]: + """Close Fiscal Period + + Close a fiscal period — the final commit action. + + All mechanics live in `PeriodCloseService.close()`. This endpoint just + resolves auth + QB sync state, invokes the service, and translates + domain exceptions into HTTP responses. + + Args: + graph_id (str): + period (str): Target period in YYYY-MM format + body (ClosePeriodRequest | Unset): + + 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[ClosePeriodResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + period=period, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + graph_id: str, + period: str, + *, + client: AuthenticatedClient, + body: ClosePeriodRequest | Unset = UNSET, +) -> ClosePeriodResponse | HTTPValidationError | None: + """Close Fiscal Period + + Close a fiscal period — the final commit action. + + All mechanics live in `PeriodCloseService.close()`. This endpoint just + resolves auth + QB sync state, invokes the service, and translates + domain exceptions into HTTP responses. + + Args: + graph_id (str): + period (str): Target period in YYYY-MM format + body (ClosePeriodRequest | Unset): + + 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: + ClosePeriodResponse | HTTPValidationError + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + period=period, + client=client, + body=body, + ) + ).parsed diff --git a/robosystems_client/api/ledger/create_manual_closing_entry.py b/robosystems_client/api/ledger/create_manual_closing_entry.py new file mode 100644 index 0000000..bf414f3 --- /dev/null +++ b/robosystems_client/api/ledger/create_manual_closing_entry.py @@ -0,0 +1,236 @@ +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.closing_entry_response import ClosingEntryResponse +from ...models.create_manual_closing_entry_request import ( + CreateManualClosingEntryRequest, +) +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + graph_id: str, + *, + body: CreateManualClosingEntryRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/v1/ledger/{graph_id}/manual-closing-entry".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 +) -> ClosingEntryResponse | HTTPValidationError | None: + if response.status_code == 201: + response_201 = ClosingEntryResponse.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + 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[ClosingEntryResponse | HTTPValidationError]: + 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: CreateManualClosingEntryRequest, +) -> Response[ClosingEntryResponse | HTTPValidationError]: + """Create Manual Closing Entry + + Create a manual (non-schedule) draft closing entry. + + Used for one-off adjustments that aren't derived from a schedule: asset + disposals, impairments, reclassifications, correcting entries. + + The entry is drafted like any schedule-derived entry and flows through + the same review and close pipeline — `list-period-drafts` shows it, + `close-period` commits it along with the rest. + + Line items can be any count (not just 2 like schedule entries). Total + debits must equal total credits. `provenance` is set to 'manual_entry' + and `source_structure_id` is null. + + Args: + graph_id (str): + body (CreateManualClosingEntryRequest): + + 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[ClosingEntryResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + graph_id: str, + *, + client: AuthenticatedClient, + body: CreateManualClosingEntryRequest, +) -> ClosingEntryResponse | HTTPValidationError | None: + """Create Manual Closing Entry + + Create a manual (non-schedule) draft closing entry. + + Used for one-off adjustments that aren't derived from a schedule: asset + disposals, impairments, reclassifications, correcting entries. + + The entry is drafted like any schedule-derived entry and flows through + the same review and close pipeline — `list-period-drafts` shows it, + `close-period` commits it along with the rest. + + Line items can be any count (not just 2 like schedule entries). Total + debits must equal total credits. `provenance` is set to 'manual_entry' + and `source_structure_id` is null. + + Args: + graph_id (str): + body (CreateManualClosingEntryRequest): + + 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: + ClosingEntryResponse | HTTPValidationError + """ + + return sync_detailed( + graph_id=graph_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + graph_id: str, + *, + client: AuthenticatedClient, + body: CreateManualClosingEntryRequest, +) -> Response[ClosingEntryResponse | HTTPValidationError]: + """Create Manual Closing Entry + + Create a manual (non-schedule) draft closing entry. + + Used for one-off adjustments that aren't derived from a schedule: asset + disposals, impairments, reclassifications, correcting entries. + + The entry is drafted like any schedule-derived entry and flows through + the same review and close pipeline — `list-period-drafts` shows it, + `close-period` commits it along with the rest. + + Line items can be any count (not just 2 like schedule entries). Total + debits must equal total credits. `provenance` is set to 'manual_entry' + and `source_structure_id` is null. + + Args: + graph_id (str): + body (CreateManualClosingEntryRequest): + + 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[ClosingEntryResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + body=body, + ) + + 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: CreateManualClosingEntryRequest, +) -> ClosingEntryResponse | HTTPValidationError | None: + """Create Manual Closing Entry + + Create a manual (non-schedule) draft closing entry. + + Used for one-off adjustments that aren't derived from a schedule: asset + disposals, impairments, reclassifications, correcting entries. + + The entry is drafted like any schedule-derived entry and flows through + the same review and close pipeline — `list-period-drafts` shows it, + `close-period` commits it along with the rest. + + Line items can be any count (not just 2 like schedule entries). Total + debits must equal total credits. `provenance` is set to 'manual_entry' + and `source_structure_id` is null. + + Args: + graph_id (str): + body (CreateManualClosingEntryRequest): + + 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: + ClosingEntryResponse | HTTPValidationError + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + client=client, + body=body, + ) + ).parsed diff --git a/robosystems_client/api/ledger/get_fiscal_calendar.py b/robosystems_client/api/ledger/get_fiscal_calendar.py new file mode 100644 index 0000000..4d798c7 --- /dev/null +++ b/robosystems_client/api/ledger/get_fiscal_calendar.py @@ -0,0 +1,168 @@ +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.fiscal_calendar_response import FiscalCalendarResponse +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + graph_id: str, +) -> dict[str, Any]: + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v1/ledger/{graph_id}/fiscal-calendar".format( + graph_id=quote(str(graph_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> FiscalCalendarResponse | HTTPValidationError | None: + if response.status_code == 200: + response_200 = FiscalCalendarResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + 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[FiscalCalendarResponse | HTTPValidationError]: + 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, +) -> Response[FiscalCalendarResponse | HTTPValidationError]: + """Get Fiscal Calendar + + Return the current fiscal calendar state — pointers, gap, closeable status. + + Args: + graph_id (str): + + 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[FiscalCalendarResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + graph_id: str, + *, + client: AuthenticatedClient, +) -> FiscalCalendarResponse | HTTPValidationError | None: + """Get Fiscal Calendar + + Return the current fiscal calendar state — pointers, gap, closeable status. + + Args: + graph_id (str): + + 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: + FiscalCalendarResponse | HTTPValidationError + """ + + return sync_detailed( + graph_id=graph_id, + client=client, + ).parsed + + +async def asyncio_detailed( + graph_id: str, + *, + client: AuthenticatedClient, +) -> Response[FiscalCalendarResponse | HTTPValidationError]: + """Get Fiscal Calendar + + Return the current fiscal calendar state — pointers, gap, closeable status. + + Args: + graph_id (str): + + 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[FiscalCalendarResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + graph_id: str, + *, + client: AuthenticatedClient, +) -> FiscalCalendarResponse | HTTPValidationError | None: + """Get Fiscal Calendar + + Return the current fiscal calendar state — pointers, gap, closeable status. + + Args: + graph_id (str): + + 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: + FiscalCalendarResponse | HTTPValidationError + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + client=client, + ) + ).parsed diff --git a/robosystems_client/api/ledger/get_statement.py b/robosystems_client/api/ledger/get_statement.py index a9e3755..5863db5 100644 --- a/robosystems_client/api/ledger/get_statement.py +++ b/robosystems_client/api/ledger/get_statement.py @@ -74,7 +74,7 @@ def sync_detailed( Args: graph_id (str): report_id (str): Report definition ID - structure_type (str): Structure type: income_statement, balance_sheet, cash_flow_statement + structure_type (str): Structure type: income_statement, balance_sheet, equity_statement Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -113,7 +113,7 @@ def sync( Args: graph_id (str): report_id (str): Report definition ID - structure_type (str): Structure type: income_statement, balance_sheet, cash_flow_statement + structure_type (str): Structure type: income_statement, balance_sheet, equity_statement Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -147,7 +147,7 @@ async def asyncio_detailed( Args: graph_id (str): report_id (str): Report definition ID - structure_type (str): Structure type: income_statement, balance_sheet, cash_flow_statement + structure_type (str): Structure type: income_statement, balance_sheet, equity_statement Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -184,7 +184,7 @@ async def asyncio( Args: graph_id (str): report_id (str): Report definition ID - structure_type (str): Structure type: income_statement, balance_sheet, cash_flow_statement + structure_type (str): Structure type: income_statement, balance_sheet, equity_statement Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. diff --git a/robosystems_client/api/ledger/initialize_ledger.py b/robosystems_client/api/ledger/initialize_ledger.py new file mode 100644 index 0000000..d614453 --- /dev/null +++ b/robosystems_client/api/ledger/initialize_ledger.py @@ -0,0 +1,218 @@ +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.http_validation_error import HTTPValidationError +from ...models.initialize_ledger_request import InitializeLedgerRequest +from ...models.initialize_ledger_response import InitializeLedgerResponse +from ...types import Response + + +def _get_kwargs( + graph_id: str, + *, + body: InitializeLedgerRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/v1/ledger/{graph_id}/initialize".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 +) -> HTTPValidationError | InitializeLedgerResponse | None: + if response.status_code == 201: + response_201 = InitializeLedgerResponse.from_dict(response.json()) + + return response_201 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + 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[HTTPValidationError | InitializeLedgerResponse]: + 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: InitializeLedgerRequest, +) -> Response[HTTPValidationError | InitializeLedgerResponse]: + """Initialize Ledger + + One-time ledger initialization. + + Creates the fiscal calendar, seeds `FiscalPeriod` rows for the data window, + and sets `closed_through` / `close_target`. Fails if the calendar already + exists — use the reopen flow to undo prior closes instead of re-initializing. + + `auto_seed_schedules=true` is accepted but is a no-op in v1; schedule + creation is deferred to the SchedulerAgent (Phase 5). + + Args: + graph_id (str): + body (InitializeLedgerRequest): + + 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[HTTPValidationError | InitializeLedgerResponse] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + graph_id: str, + *, + client: AuthenticatedClient, + body: InitializeLedgerRequest, +) -> HTTPValidationError | InitializeLedgerResponse | None: + """Initialize Ledger + + One-time ledger initialization. + + Creates the fiscal calendar, seeds `FiscalPeriod` rows for the data window, + and sets `closed_through` / `close_target`. Fails if the calendar already + exists — use the reopen flow to undo prior closes instead of re-initializing. + + `auto_seed_schedules=true` is accepted but is a no-op in v1; schedule + creation is deferred to the SchedulerAgent (Phase 5). + + Args: + graph_id (str): + body (InitializeLedgerRequest): + + 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: + HTTPValidationError | InitializeLedgerResponse + """ + + return sync_detailed( + graph_id=graph_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + graph_id: str, + *, + client: AuthenticatedClient, + body: InitializeLedgerRequest, +) -> Response[HTTPValidationError | InitializeLedgerResponse]: + """Initialize Ledger + + One-time ledger initialization. + + Creates the fiscal calendar, seeds `FiscalPeriod` rows for the data window, + and sets `closed_through` / `close_target`. Fails if the calendar already + exists — use the reopen flow to undo prior closes instead of re-initializing. + + `auto_seed_schedules=true` is accepted but is a no-op in v1; schedule + creation is deferred to the SchedulerAgent (Phase 5). + + Args: + graph_id (str): + body (InitializeLedgerRequest): + + 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[HTTPValidationError | InitializeLedgerResponse] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + body=body, + ) + + 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: InitializeLedgerRequest, +) -> HTTPValidationError | InitializeLedgerResponse | None: + """Initialize Ledger + + One-time ledger initialization. + + Creates the fiscal calendar, seeds `FiscalPeriod` rows for the data window, + and sets `closed_through` / `close_target`. Fails if the calendar already + exists — use the reopen flow to undo prior closes instead of re-initializing. + + `auto_seed_schedules=true` is accepted but is a no-op in v1; schedule + creation is deferred to the SchedulerAgent (Phase 5). + + Args: + graph_id (str): + body (InitializeLedgerRequest): + + 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: + HTTPValidationError | InitializeLedgerResponse + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + client=client, + body=body, + ) + ).parsed diff --git a/robosystems_client/api/ledger/list_period_drafts.py b/robosystems_client/api/ledger/list_period_drafts.py new file mode 100644 index 0000000..056b9b6 --- /dev/null +++ b/robosystems_client/api/ledger/list_period_drafts.py @@ -0,0 +1,230 @@ +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.http_validation_error import HTTPValidationError +from ...models.period_drafts_response import PeriodDraftsResponse +from ...types import Response + + +def _get_kwargs( + graph_id: str, + period: str, +) -> dict[str, Any]: + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v1/ledger/{graph_id}/periods/{period}/drafts".format( + graph_id=quote(str(graph_id), safe=""), + period=quote(str(period), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> HTTPValidationError | PeriodDraftsResponse | None: + if response.status_code == 200: + response_200 = PeriodDraftsResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + 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[HTTPValidationError | PeriodDraftsResponse]: + 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, + period: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | PeriodDraftsResponse]: + """List Draft Entries For Review + + List all draft entries in a fiscal period for review before close. + + Returns every draft entry whose `posting_date` falls within the period, + fully expanded with line items, element names/codes, source schedule + structure name, and per-entry balance check. + + Use this to review exactly what `close-period` will commit. Typical flow: + + 1. Draft entries via `create-closing-entry` (one per schedule) + 2. Call this endpoint to review the full set + 3. Call `POST /periods/{period}/close` to atomically post + close + + This is a pure read — no side effects. It can be called repeatedly. + + Args: + graph_id (str): + period (str): Period in YYYY-MM format + + 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[HTTPValidationError | PeriodDraftsResponse] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + period=period, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + graph_id: str, + period: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | PeriodDraftsResponse | None: + """List Draft Entries For Review + + List all draft entries in a fiscal period for review before close. + + Returns every draft entry whose `posting_date` falls within the period, + fully expanded with line items, element names/codes, source schedule + structure name, and per-entry balance check. + + Use this to review exactly what `close-period` will commit. Typical flow: + + 1. Draft entries via `create-closing-entry` (one per schedule) + 2. Call this endpoint to review the full set + 3. Call `POST /periods/{period}/close` to atomically post + close + + This is a pure read — no side effects. It can be called repeatedly. + + Args: + graph_id (str): + period (str): Period in YYYY-MM format + + 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: + HTTPValidationError | PeriodDraftsResponse + """ + + return sync_detailed( + graph_id=graph_id, + period=period, + client=client, + ).parsed + + +async def asyncio_detailed( + graph_id: str, + period: str, + *, + client: AuthenticatedClient, +) -> Response[HTTPValidationError | PeriodDraftsResponse]: + """List Draft Entries For Review + + List all draft entries in a fiscal period for review before close. + + Returns every draft entry whose `posting_date` falls within the period, + fully expanded with line items, element names/codes, source schedule + structure name, and per-entry balance check. + + Use this to review exactly what `close-period` will commit. Typical flow: + + 1. Draft entries via `create-closing-entry` (one per schedule) + 2. Call this endpoint to review the full set + 3. Call `POST /periods/{period}/close` to atomically post + close + + This is a pure read — no side effects. It can be called repeatedly. + + Args: + graph_id (str): + period (str): Period in YYYY-MM format + + 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[HTTPValidationError | PeriodDraftsResponse] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + period=period, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + graph_id: str, + period: str, + *, + client: AuthenticatedClient, +) -> HTTPValidationError | PeriodDraftsResponse | None: + """List Draft Entries For Review + + List all draft entries in a fiscal period for review before close. + + Returns every draft entry whose `posting_date` falls within the period, + fully expanded with line items, element names/codes, source schedule + structure name, and per-entry balance check. + + Use this to review exactly what `close-period` will commit. Typical flow: + + 1. Draft entries via `create-closing-entry` (one per schedule) + 2. Call this endpoint to review the full set + 3. Call `POST /periods/{period}/close` to atomically post + close + + This is a pure read — no side effects. It can be called repeatedly. + + Args: + graph_id (str): + period (str): Period in YYYY-MM format + + 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: + HTTPValidationError | PeriodDraftsResponse + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + period=period, + client=client, + ) + ).parsed diff --git a/robosystems_client/api/ledger/reopen_fiscal_period.py b/robosystems_client/api/ledger/reopen_fiscal_period.py new file mode 100644 index 0000000..cc55bde --- /dev/null +++ b/robosystems_client/api/ledger/reopen_fiscal_period.py @@ -0,0 +1,240 @@ +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.fiscal_calendar_response import FiscalCalendarResponse +from ...models.http_validation_error import HTTPValidationError +from ...models.reopen_period_request import ReopenPeriodRequest +from ...types import Response + + +def _get_kwargs( + graph_id: str, + period: str, + *, + body: ReopenPeriodRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/v1/ledger/{graph_id}/periods/{period}/reopen".format( + graph_id=quote(str(graph_id), safe=""), + period=quote(str(period), 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 +) -> FiscalCalendarResponse | HTTPValidationError | None: + if response.status_code == 200: + response_200 = FiscalCalendarResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + 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[FiscalCalendarResponse | HTTPValidationError]: + 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, + period: str, + *, + client: AuthenticatedClient, + body: ReopenPeriodRequest, +) -> Response[FiscalCalendarResponse | HTTPValidationError]: + """Reopen Fiscal Period + + Reopen a closed fiscal period. + + Transitions the period status from 'closed' to 'closing' (drafts may still + exist for this period after reopening). If the reopened period is the + current `closed_through`, decrements the pointer. Requires a non-empty + `reason` for the audit log. Does NOT modify `close_target` — that's a + separate user decision. + + Posted entries in the reopened period stay posted. The user can then post + additional adjustments, review, and close the period again. + + Args: + graph_id (str): + period (str): Period to reopen (YYYY-MM) + body (ReopenPeriodRequest): + + 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[FiscalCalendarResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + period=period, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + graph_id: str, + period: str, + *, + client: AuthenticatedClient, + body: ReopenPeriodRequest, +) -> FiscalCalendarResponse | HTTPValidationError | None: + """Reopen Fiscal Period + + Reopen a closed fiscal period. + + Transitions the period status from 'closed' to 'closing' (drafts may still + exist for this period after reopening). If the reopened period is the + current `closed_through`, decrements the pointer. Requires a non-empty + `reason` for the audit log. Does NOT modify `close_target` — that's a + separate user decision. + + Posted entries in the reopened period stay posted. The user can then post + additional adjustments, review, and close the period again. + + Args: + graph_id (str): + period (str): Period to reopen (YYYY-MM) + body (ReopenPeriodRequest): + + 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: + FiscalCalendarResponse | HTTPValidationError + """ + + return sync_detailed( + graph_id=graph_id, + period=period, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + graph_id: str, + period: str, + *, + client: AuthenticatedClient, + body: ReopenPeriodRequest, +) -> Response[FiscalCalendarResponse | HTTPValidationError]: + """Reopen Fiscal Period + + Reopen a closed fiscal period. + + Transitions the period status from 'closed' to 'closing' (drafts may still + exist for this period after reopening). If the reopened period is the + current `closed_through`, decrements the pointer. Requires a non-empty + `reason` for the audit log. Does NOT modify `close_target` — that's a + separate user decision. + + Posted entries in the reopened period stay posted. The user can then post + additional adjustments, review, and close the period again. + + Args: + graph_id (str): + period (str): Period to reopen (YYYY-MM) + body (ReopenPeriodRequest): + + 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[FiscalCalendarResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + period=period, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + graph_id: str, + period: str, + *, + client: AuthenticatedClient, + body: ReopenPeriodRequest, +) -> FiscalCalendarResponse | HTTPValidationError | None: + """Reopen Fiscal Period + + Reopen a closed fiscal period. + + Transitions the period status from 'closed' to 'closing' (drafts may still + exist for this period after reopening). If the reopened period is the + current `closed_through`, decrements the pointer. Requires a non-empty + `reason` for the audit log. Does NOT modify `close_target` — that's a + separate user decision. + + Posted entries in the reopened period stay posted. The user can then post + additional adjustments, review, and close the period again. + + Args: + graph_id (str): + period (str): Period to reopen (YYYY-MM) + body (ReopenPeriodRequest): + + 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: + FiscalCalendarResponse | HTTPValidationError + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + period=period, + client=client, + body=body, + ) + ).parsed diff --git a/robosystems_client/api/ledger/set_close_target.py b/robosystems_client/api/ledger/set_close_target.py new file mode 100644 index 0000000..cd19de9 --- /dev/null +++ b/robosystems_client/api/ledger/set_close_target.py @@ -0,0 +1,206 @@ +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.fiscal_calendar_response import FiscalCalendarResponse +from ...models.http_validation_error import HTTPValidationError +from ...models.set_close_target_request import SetCloseTargetRequest +from ...types import Response + + +def _get_kwargs( + graph_id: str, + *, + body: SetCloseTargetRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/v1/ledger/{graph_id}/fiscal-calendar/close-target".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 +) -> FiscalCalendarResponse | HTTPValidationError | None: + if response.status_code == 200: + response_200 = FiscalCalendarResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + 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[FiscalCalendarResponse | HTTPValidationError]: + 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: SetCloseTargetRequest, +) -> Response[FiscalCalendarResponse | HTTPValidationError]: + """Set Close Target + + Set the close target for a graph. + + Validates that the target is a real YYYY-MM period, is not in the future, + and is not before the current `closed_through`. Emits a `target_changed` + audit event. Returns the updated calendar state. + + Args: + graph_id (str): + body (SetCloseTargetRequest): + + 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[FiscalCalendarResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + graph_id: str, + *, + client: AuthenticatedClient, + body: SetCloseTargetRequest, +) -> FiscalCalendarResponse | HTTPValidationError | None: + """Set Close Target + + Set the close target for a graph. + + Validates that the target is a real YYYY-MM period, is not in the future, + and is not before the current `closed_through`. Emits a `target_changed` + audit event. Returns the updated calendar state. + + Args: + graph_id (str): + body (SetCloseTargetRequest): + + 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: + FiscalCalendarResponse | HTTPValidationError + """ + + return sync_detailed( + graph_id=graph_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + graph_id: str, + *, + client: AuthenticatedClient, + body: SetCloseTargetRequest, +) -> Response[FiscalCalendarResponse | HTTPValidationError]: + """Set Close Target + + Set the close target for a graph. + + Validates that the target is a real YYYY-MM period, is not in the future, + and is not before the current `closed_through`. Emits a `target_changed` + audit event. Returns the updated calendar state. + + Args: + graph_id (str): + body (SetCloseTargetRequest): + + 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[FiscalCalendarResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + body=body, + ) + + 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: SetCloseTargetRequest, +) -> FiscalCalendarResponse | HTTPValidationError | None: + """Set Close Target + + Set the close target for a graph. + + Validates that the target is a real YYYY-MM period, is not in the future, + and is not before the current `closed_through`. Emits a `target_changed` + audit event. Returns the updated calendar state. + + Args: + graph_id (str): + body (SetCloseTargetRequest): + + 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: + FiscalCalendarResponse | HTTPValidationError + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + client=client, + body=body, + ) + ).parsed diff --git a/robosystems_client/api/ledger/truncate_schedule.py b/robosystems_client/api/ledger/truncate_schedule.py new file mode 100644 index 0000000..6053891 --- /dev/null +++ b/robosystems_client/api/ledger/truncate_schedule.py @@ -0,0 +1,248 @@ +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.http_validation_error import HTTPValidationError +from ...models.truncate_schedule_request import TruncateScheduleRequest +from ...models.truncate_schedule_response import TruncateScheduleResponse +from ...types import Response + + +def _get_kwargs( + graph_id: str, + structure_id: str, + *, + body: TruncateScheduleRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "patch", + "url": "/v1/ledger/{graph_id}/schedules/{structure_id}/truncate".format( + graph_id=quote(str(graph_id), safe=""), + structure_id=quote(str(structure_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 +) -> HTTPValidationError | TruncateScheduleResponse | None: + if response.status_code == 200: + response_200 = TruncateScheduleResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + 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[HTTPValidationError | TruncateScheduleResponse]: + 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, + structure_id: str, + *, + client: AuthenticatedClient, + body: TruncateScheduleRequest, +) -> Response[HTTPValidationError | TruncateScheduleResponse]: + """Truncate Schedule (End Early) + + End a schedule early. + + Used for events that cut a schedule's lifespan short — an asset is sold, + a prepaid is cancelled, a contract is terminated. Deletes all facts with + `period_start > new_end_date` and any stale draft entries that were + produced from them. + + Posted entries are preserved — if any period after `new_end_date` has a + posted closing entry, the truncate fails with 422 and the caller must + reopen that period first. + + The truncation is logged to the schedule's metadata for audit. + + Args: + graph_id (str): + structure_id (str): Schedule structure ID + body (TruncateScheduleRequest): + + 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[HTTPValidationError | TruncateScheduleResponse] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + structure_id=structure_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + graph_id: str, + structure_id: str, + *, + client: AuthenticatedClient, + body: TruncateScheduleRequest, +) -> HTTPValidationError | TruncateScheduleResponse | None: + """Truncate Schedule (End Early) + + End a schedule early. + + Used for events that cut a schedule's lifespan short — an asset is sold, + a prepaid is cancelled, a contract is terminated. Deletes all facts with + `period_start > new_end_date` and any stale draft entries that were + produced from them. + + Posted entries are preserved — if any period after `new_end_date` has a + posted closing entry, the truncate fails with 422 and the caller must + reopen that period first. + + The truncation is logged to the schedule's metadata for audit. + + Args: + graph_id (str): + structure_id (str): Schedule structure ID + body (TruncateScheduleRequest): + + 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: + HTTPValidationError | TruncateScheduleResponse + """ + + return sync_detailed( + graph_id=graph_id, + structure_id=structure_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + graph_id: str, + structure_id: str, + *, + client: AuthenticatedClient, + body: TruncateScheduleRequest, +) -> Response[HTTPValidationError | TruncateScheduleResponse]: + """Truncate Schedule (End Early) + + End a schedule early. + + Used for events that cut a schedule's lifespan short — an asset is sold, + a prepaid is cancelled, a contract is terminated. Deletes all facts with + `period_start > new_end_date` and any stale draft entries that were + produced from them. + + Posted entries are preserved — if any period after `new_end_date` has a + posted closing entry, the truncate fails with 422 and the caller must + reopen that period first. + + The truncation is logged to the schedule's metadata for audit. + + Args: + graph_id (str): + structure_id (str): Schedule structure ID + body (TruncateScheduleRequest): + + 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[HTTPValidationError | TruncateScheduleResponse] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + structure_id=structure_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + graph_id: str, + structure_id: str, + *, + client: AuthenticatedClient, + body: TruncateScheduleRequest, +) -> HTTPValidationError | TruncateScheduleResponse | None: + """Truncate Schedule (End Early) + + End a schedule early. + + Used for events that cut a schedule's lifespan short — an asset is sold, + a prepaid is cancelled, a contract is terminated. Deletes all facts with + `period_start > new_end_date` and any stale draft entries that were + produced from them. + + Posted entries are preserved — if any period after `new_end_date` has a + posted closing entry, the truncate fails with 422 and the caller must + reopen that period first. + + The truncation is logged to the schedule's metadata for audit. + + Args: + graph_id (str): + structure_id (str): Schedule structure ID + body (TruncateScheduleRequest): + + 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: + HTTPValidationError | TruncateScheduleResponse + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + structure_id=structure_id, + client=client, + body=body, + ) + ).parsed diff --git a/robosystems_client/extensions/ledger_client.py b/robosystems_client/extensions/ledger_client.py index 4cca900..386d793 100644 --- a/robosystems_client/extensions/ledger_client.py +++ b/robosystems_client/extensions/ledger_client.py @@ -6,11 +6,16 @@ from __future__ import annotations +import datetime from http import HTTPStatus from typing import Any from ..api.ledger.auto_map_elements import sync_detailed as auto_map_elements +from ..api.ledger.close_fiscal_period import sync_detailed as close_fiscal_period from ..api.ledger.create_closing_entry import sync_detailed as create_closing_entry +from ..api.ledger.create_manual_closing_entry import ( + sync_detailed as create_manual_closing_entry, +) from ..api.ledger.create_mapping_association import ( sync_detailed as create_mapping_association, ) @@ -23,6 +28,7 @@ from ..api.ledger.get_closing_book_structures import ( sync_detailed as get_closing_book_structures, ) +from ..api.ledger.get_fiscal_calendar import sync_detailed as get_fiscal_calendar from ..api.ledger.get_ledger_account_tree import ( sync_detailed as get_ledger_account_tree, ) @@ -48,6 +54,7 @@ sync_detailed as get_reporting_taxonomy, ) from ..api.ledger.get_schedule_facts import sync_detailed as get_schedule_facts +from ..api.ledger.initialize_ledger import sync_detailed as initialize_ledger from ..api.ledger.list_elements import sync_detailed as list_elements from ..api.ledger.list_ledger_accounts import ( sync_detailed as list_ledger_accounts, @@ -56,9 +63,14 @@ sync_detailed as list_ledger_transactions, ) from ..api.ledger.list_mappings import sync_detailed as list_mappings +from ..api.ledger.list_period_drafts import sync_detailed as list_period_drafts from ..api.ledger.list_schedules import sync_detailed as list_schedules from ..api.ledger.list_structures import sync_detailed as list_structures +from ..api.ledger.reopen_fiscal_period import sync_detailed as reopen_fiscal_period +from ..api.ledger.set_close_target import sync_detailed as set_close_target +from ..api.ledger.truncate_schedule import sync_detailed as truncate_schedule from ..client import AuthenticatedClient +from ..types import UNSET class LedgerClient: @@ -485,3 +497,221 @@ def get_account_rollups( if response.status_code != HTTPStatus.OK: raise RuntimeError(f"Get account rollups failed: {response.status_code}") return response.parsed + + # ── Fiscal Calendar ───────────────────────────────────────────────── + + def initialize_ledger( + self, + graph_id: str, + *, + closed_through: str | None = None, + fiscal_year_start_month: int | None = None, + earliest_data_period: str | None = None, + auto_seed_schedules: bool | None = None, + note: str | None = None, + ) -> Any: + """Initialize the fiscal calendar for a graph. + + Creates FiscalPeriod rows for the data window, sets `closed_through` / + `close_target`, and emits an `initialized` audit event. Fails with 409 + if already initialized. + """ + from ..models.initialize_ledger_request import InitializeLedgerRequest + + body = InitializeLedgerRequest( + closed_through=closed_through if closed_through is not None else UNSET, + fiscal_year_start_month=fiscal_year_start_month + if fiscal_year_start_month is not None + else UNSET, + earliest_data_period=earliest_data_period + if earliest_data_period is not None + else UNSET, + auto_seed_schedules=auto_seed_schedules + if auto_seed_schedules is not None + else UNSET, + note=note if note is not None else UNSET, + ) + response = initialize_ledger( + graph_id=graph_id, body=body, client=self._get_client() + ) + if response.status_code != HTTPStatus.CREATED: + raise RuntimeError(f"Initialize ledger failed: {response.status_code}") + return response.parsed + + def get_fiscal_calendar(self, graph_id: str) -> Any: + """Get the current fiscal calendar state — pointers, gap, closeable status.""" + response = get_fiscal_calendar(graph_id=graph_id, client=self._get_client()) + if response.status_code != HTTPStatus.OK: + raise RuntimeError(f"Get fiscal calendar failed: {response.status_code}") + return response.parsed + + def set_close_target( + self, + graph_id: str, + period: str, + note: str | None = None, + ) -> Any: + """Set the close target for a graph. + + Validates that the target is not in the future and not before + `closed_through`. + """ + from ..models.set_close_target_request import SetCloseTargetRequest + + body = SetCloseTargetRequest( + period=period, + note=note if note is not None else UNSET, + ) + response = set_close_target(graph_id=graph_id, body=body, client=self._get_client()) + if response.status_code != HTTPStatus.OK: + raise RuntimeError(f"Set close target failed: {response.status_code}") + return response.parsed + + def close_period( + self, + graph_id: str, + period: str, + note: str | None = None, + allow_stale_sync: bool | None = None, + ) -> Any: + """Close a fiscal period — the final commit action. + + Validates closeable gates, transitions all draft entries in the period + to `posted`, marks the FiscalPeriod closed, and advances `closed_through` + (auto-advancing `close_target` when reached). + """ + from ..models.close_period_request import ClosePeriodRequest + + body = ClosePeriodRequest( + note=note if note is not None else UNSET, + allow_stale_sync=allow_stale_sync if allow_stale_sync is not None else UNSET, + ) + response = close_fiscal_period( + graph_id=graph_id, period=period, body=body, client=self._get_client() + ) + if response.status_code != HTTPStatus.OK: + raise RuntimeError(f"Close period failed: {response.status_code}") + return response.parsed + + def reopen_period( + self, + graph_id: str, + period: str, + reason: str, + note: str | None = None, + ) -> Any: + """Reopen a closed fiscal period. + + Requires a non-empty `reason` for the audit log. Posted entries stay + posted; the period transitions to `closing` so the user can post + adjustments and re-close. + """ + from ..models.reopen_period_request import ReopenPeriodRequest + + body = ReopenPeriodRequest( + reason=reason, + note=note if note is not None else UNSET, + ) + response = reopen_fiscal_period( + graph_id=graph_id, period=period, body=body, client=self._get_client() + ) + if response.status_code != HTTPStatus.OK: + raise RuntimeError(f"Reopen period failed: {response.status_code}") + return response.parsed + + def list_period_drafts(self, graph_id: str, period: str) -> Any: + """List all draft entries in a fiscal period for review before close. + + Fully expanded with line items, element metadata, and per-entry balance. + Pure read — call repeatedly without side effects. + """ + response = list_period_drafts( + graph_id=graph_id, period=period, client=self._get_client() + ) + if response.status_code != HTTPStatus.OK: + raise RuntimeError(f"List period drafts failed: {response.status_code}") + return response.parsed + + # ── Schedule mutations ────────────────────────────────────────────── + + def truncate_schedule( + self, + graph_id: str, + structure_id: str, + new_end_date: str, + reason: str, + ) -> Any: + """Truncate a schedule — end it early. + + Deletes facts with `period_start > new_end_date` along with any stale + draft entries they produced. Historical posted facts are preserved. + `new_end_date` must be a month-end date (service enforces this). + """ + from ..models.truncate_schedule_request import TruncateScheduleRequest + + body = TruncateScheduleRequest( + new_end_date=datetime.date.fromisoformat(new_end_date), + reason=reason, + ) + response = truncate_schedule( + graph_id=graph_id, + structure_id=structure_id, + body=body, + client=self._get_client(), + ) + if response.status_code != HTTPStatus.OK: + raise RuntimeError(f"Truncate schedule failed: {response.status_code}") + return response.parsed + + def create_manual_closing_entry( + self, + graph_id: str, + *, + posting_date: str, + memo: str, + line_items: list[dict[str, Any]], + entry_type: str | None = None, + ) -> Any: + """Create a manual draft closing entry with arbitrary balanced line items. + + Not tied to a schedule — used for disposals, adjustments, and other + one-off closing events. Line items must sum to balanced debits/credits. + Rejects entries targeting an already-closed period. + + Each line item dict should have: + element_id (str), debit_amount (int, cents), credit_amount (int, cents), + description (str | None, optional). + """ + from ..models.create_manual_closing_entry_request import ( + CreateManualClosingEntryRequest, + ) + from ..models.create_manual_closing_entry_request_entry_type import ( + CreateManualClosingEntryRequestEntryType, + ) + from ..models.manual_line_item_request import ManualLineItemRequest + + items = [ + ManualLineItemRequest( + element_id=li["element_id"], + debit_amount=li.get("debit_amount", 0), + credit_amount=li.get("credit_amount", 0), + description=li.get("description") + if li.get("description") is not None + else UNSET, + ) + for li in line_items + ] + body = CreateManualClosingEntryRequest( + posting_date=datetime.date.fromisoformat(posting_date), + memo=memo, + line_items=items, + entry_type=CreateManualClosingEntryRequestEntryType(entry_type) + if entry_type is not None + else UNSET, + ) + response = create_manual_closing_entry( + graph_id=graph_id, body=body, client=self._get_client() + ) + if response.status_code != HTTPStatus.CREATED: + raise RuntimeError(f"Create manual closing entry failed: {response.status_code}") + return response.parsed diff --git a/robosystems_client/models/__init__.py b/robosystems_client/models/__init__.py index 86285be..0daee6d 100644 --- a/robosystems_client/models/__init__.py +++ b/robosystems_client/models/__init__.py @@ -59,6 +59,8 @@ ) from .checkout_response import CheckoutResponse from .checkout_status_response import CheckoutStatusResponse +from .close_period_request import ClosePeriodRequest +from .close_period_response import ClosePeriodResponse from .closing_book_category import ClosingBookCategory from .closing_book_item import ClosingBookItem from .closing_book_structures_response import ClosingBookStructuresResponse @@ -83,6 +85,10 @@ from .create_connection_request import CreateConnectionRequest from .create_connection_request_provider import CreateConnectionRequestProvider from .create_graph_request import CreateGraphRequest +from .create_manual_closing_entry_request import CreateManualClosingEntryRequest +from .create_manual_closing_entry_request_entry_type import ( + CreateManualClosingEntryRequestEntryType, +) from .create_portfolio_request import CreatePortfolioRequest from .create_position_request import CreatePositionRequest from .create_publish_list_request import CreatePublishListRequest @@ -129,6 +135,8 @@ from .document_upload_request import DocumentUploadRequest from .document_upload_response import DocumentUploadResponse from .download_quota import DownloadQuota +from .draft_entry_response import DraftEntryResponse +from .draft_line_item import DraftLineItem from .element_list_response import ElementListResponse from .element_response import ElementResponse from .email_verification_request import EmailVerificationRequest @@ -138,6 +146,7 @@ ) from .enhanced_file_status_layers import EnhancedFileStatusLayers from .entry_template_request import EntryTemplateRequest +from .entry_template_request_entry_type import EntryTemplateRequestEntryType from .error_response import ErrorResponse from .execute_cypher_query_response_200 import ExecuteCypherQueryResponse200 from .execute_cypher_query_response_200_data_item import ( @@ -149,6 +158,8 @@ from .file_status_update import FileStatusUpdate from .file_upload_request import FileUploadRequest from .file_upload_response import FileUploadResponse +from .fiscal_calendar_response import FiscalCalendarResponse +from .fiscal_period_summary import FiscalPeriodSummary from .forgot_password_request import ForgotPasswordRequest from .forgot_password_response_forgotpassword import ( ForgotPasswordResponseForgotpassword, @@ -188,6 +199,8 @@ from .holdings_list_response import HoldingsListResponse from .http_validation_error import HTTPValidationError from .initial_entity_data import InitialEntityData +from .initialize_ledger_request import InitializeLedgerRequest +from .initialize_ledger_response import InitializeLedgerResponse from .instance_usage import InstanceUsage from .invite_member_request import InviteMemberRequest from .invoice import Invoice @@ -206,6 +219,7 @@ from .list_table_files_response import ListTableFilesResponse from .login_request import LoginRequest from .logout_user_response_logoutuser import LogoutUserResponseLogoutuser +from .manual_line_item_request import ManualLineItemRequest from .mapping_coverage_response import MappingCoverageResponse from .mapping_detail_response import MappingDetailResponse from .materialize_request import MaterializeRequest @@ -257,6 +271,7 @@ from .performance_insights_slow_queries_item import PerformanceInsightsSlowQueriesItem from .period_close_item_response import PeriodCloseItemResponse from .period_close_status_response import PeriodCloseStatusResponse +from .period_drafts_response import PeriodDraftsResponse from .period_spec import PeriodSpec from .portal_session_response import PortalSessionResponse from .portfolio_list_response import PortfolioListResponse @@ -272,6 +287,7 @@ from .rate_limits import RateLimits from .regenerate_report_request import RegenerateReportRequest from .register_request import RegisterRequest +from .reopen_period_request import ReopenPeriodRequest from .report_list_response import ReportListResponse from .report_response import ReportResponse from .repository_info import RepositoryInfo @@ -320,6 +336,7 @@ from .selection_criteria import SelectionCriteria from .service_offering_summary import ServiceOfferingSummary from .service_offerings_response import ServiceOfferingsResponse +from .set_close_target_request import SetCloseTargetRequest from .share_report_request import ShareReportRequest from .share_report_response import ShareReportResponse from .share_result_item import ShareResultItem @@ -359,6 +376,8 @@ from .transaction_summary_response import TransactionSummaryResponse from .trial_balance_response import TrialBalanceResponse from .trial_balance_row import TrialBalanceRow +from .truncate_schedule_request import TruncateScheduleRequest +from .truncate_schedule_response import TruncateScheduleResponse from .unmapped_element_response import UnmappedElementResponse from .upcoming_invoice import UpcomingInvoice from .update_api_key_request import UpdateAPIKeyRequest @@ -436,6 +455,8 @@ "CancelOperationResponseCanceloperation", "CheckoutResponse", "CheckoutStatusResponse", + "ClosePeriodRequest", + "ClosePeriodResponse", "ClosingBookCategory", "ClosingBookItem", "ClosingBookStructuresResponse", @@ -458,6 +479,8 @@ "CreateConnectionRequest", "CreateConnectionRequestProvider", "CreateGraphRequest", + "CreateManualClosingEntryRequest", + "CreateManualClosingEntryRequestEntryType", "CreatePortfolioRequest", "CreatePositionRequest", "CreatePublishListRequest", @@ -500,6 +523,8 @@ "DocumentUploadRequest", "DocumentUploadResponse", "DownloadQuota", + "DraftEntryResponse", + "DraftLineItem", "ElementListResponse", "ElementResponse", "EmailVerificationRequest", @@ -507,6 +532,7 @@ "EnhancedCreditTransactionResponseMetadata", "EnhancedFileStatusLayers", "EntryTemplateRequest", + "EntryTemplateRequestEntryType", "ErrorResponse", "ExecuteCypherQueryResponse200", "ExecuteCypherQueryResponse200DataItem", @@ -516,6 +542,8 @@ "FileStatusUpdate", "FileUploadRequest", "FileUploadResponse", + "FiscalCalendarResponse", + "FiscalPeriodSummary", "ForgotPasswordRequest", "ForgotPasswordResponseForgotpassword", "GetCurrentAuthUserResponseGetcurrentauthuser", @@ -547,6 +575,8 @@ "HoldingsListResponse", "HTTPValidationError", "InitialEntityData", + "InitializeLedgerRequest", + "InitializeLedgerResponse", "InstanceUsage", "InviteMemberRequest", "Invoice", @@ -565,6 +595,7 @@ "ListTableFilesResponse", "LoginRequest", "LogoutUserResponseLogoutuser", + "ManualLineItemRequest", "MappingCoverageResponse", "MappingDetailResponse", "MaterializeRequest", @@ -612,6 +643,7 @@ "PerformanceInsightsSlowQueriesItem", "PeriodCloseItemResponse", "PeriodCloseStatusResponse", + "PeriodDraftsResponse", "PeriodSpec", "PortalSessionResponse", "PortfolioListResponse", @@ -627,6 +659,7 @@ "RateLimits", "RegenerateReportRequest", "RegisterRequest", + "ReopenPeriodRequest", "ReportListResponse", "ReportResponse", "RepositoryInfo", @@ -663,6 +696,7 @@ "SelectionCriteria", "ServiceOfferingsResponse", "ServiceOfferingSummary", + "SetCloseTargetRequest", "ShareReportRequest", "ShareReportResponse", "ShareResultItem", @@ -698,6 +732,8 @@ "TransactionSummaryResponse", "TrialBalanceResponse", "TrialBalanceRow", + "TruncateScheduleRequest", + "TruncateScheduleResponse", "UnmappedElementResponse", "UpcomingInvoice", "UpdateAPIKeyRequest", diff --git a/robosystems_client/models/close_period_request.py b/robosystems_client/models/close_period_request.py new file mode 100644 index 0000000..50535b7 --- /dev/null +++ b/robosystems_client/models/close_period_request.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ClosePeriodRequest") + + +@_attrs_define +class ClosePeriodRequest: + """ + Attributes: + note (None | str | Unset): Free-form note attached to the close event + allow_stale_sync (bool | Unset): Override the sync-currency gate. Only use when you have manually verified that + the source data for the period is complete. Default: False. + """ + + note: None | str | Unset = UNSET + allow_stale_sync: bool | Unset = False + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + note: None | str | Unset + if isinstance(self.note, Unset): + note = UNSET + else: + note = self.note + + allow_stale_sync = self.allow_stale_sync + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if note is not UNSET: + field_dict["note"] = note + if allow_stale_sync is not UNSET: + field_dict["allow_stale_sync"] = allow_stale_sync + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_note(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + note = _parse_note(d.pop("note", UNSET)) + + allow_stale_sync = d.pop("allow_stale_sync", UNSET) + + close_period_request = cls( + note=note, + allow_stale_sync=allow_stale_sync, + ) + + close_period_request.additional_properties = d + return close_period_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/close_period_response.py b/robosystems_client/models/close_period_response.py new file mode 100644 index 0000000..014dd00 --- /dev/null +++ b/robosystems_client/models/close_period_response.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.fiscal_calendar_response import FiscalCalendarResponse + + +T = TypeVar("T", bound="ClosePeriodResponse") + + +@_attrs_define +class ClosePeriodResponse: + """Response from a single-period close operation. + + Attributes: + fiscal_calendar (FiscalCalendarResponse): Current fiscal calendar state for a graph. + period (str): + entries_posted (int | Unset): Number of draft entries transitioned to posted Default: 0. + target_auto_advanced (bool | Unset): Whether close_target was auto-advanced because it was reached Default: + False. + """ + + fiscal_calendar: FiscalCalendarResponse + period: str + entries_posted: int | Unset = 0 + target_auto_advanced: bool | Unset = False + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + fiscal_calendar = self.fiscal_calendar.to_dict() + + period = self.period + + entries_posted = self.entries_posted + + target_auto_advanced = self.target_auto_advanced + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "fiscal_calendar": fiscal_calendar, + "period": period, + } + ) + if entries_posted is not UNSET: + field_dict["entries_posted"] = entries_posted + if target_auto_advanced is not UNSET: + field_dict["target_auto_advanced"] = target_auto_advanced + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.fiscal_calendar_response import FiscalCalendarResponse + + d = dict(src_dict) + fiscal_calendar = FiscalCalendarResponse.from_dict(d.pop("fiscal_calendar")) + + period = d.pop("period") + + entries_posted = d.pop("entries_posted", UNSET) + + target_auto_advanced = d.pop("target_auto_advanced", UNSET) + + close_period_response = cls( + fiscal_calendar=fiscal_calendar, + period=period, + entries_posted=entries_posted, + target_auto_advanced=target_auto_advanced, + ) + + close_period_response.additional_properties = d + return close_period_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/closing_entry_response.py b/robosystems_client/models/closing_entry_response.py index 9cbe8e5..9fa21d0 100644 --- a/robosystems_client/models/closing_entry_response.py +++ b/robosystems_client/models/closing_entry_response.py @@ -17,40 +17,84 @@ class ClosingEntryResponse: """ Attributes: - entry_id (str): - status (str): - posting_date (datetime.date): - memo (str): - debit_element_id (str): - credit_element_id (str): - amount (float): + outcome (str): What the idempotent call did: 'created' (new draft), 'unchanged' (existing draft still matches), + 'regenerated' (stale draft replaced with fresh one), 'removed' (stale draft deleted; schedule no longer covers + this period), 'skipped' (nothing to do — no draft and no in-scope fact). + entry_id (None | str | Unset): The draft entry ID. None for 'removed' and 'skipped' outcomes. + status (None | str | Unset): Entry status (always 'draft' when present). + posting_date (datetime.date | None | Unset): + memo (None | str | Unset): + debit_element_id (None | str | Unset): + credit_element_id (None | str | Unset): + amount (float | None | Unset): Entry amount in dollars. None for 'removed' and 'skipped'. + reason (None | str | Unset): Explanation for 'removed' and 'skipped' outcomes. reversal (ClosingEntryResponse | None | Unset): """ - entry_id: str - status: str - posting_date: datetime.date - memo: str - debit_element_id: str - credit_element_id: str - amount: float + outcome: str + entry_id: None | str | Unset = UNSET + status: None | str | Unset = UNSET + posting_date: datetime.date | None | Unset = UNSET + memo: None | str | Unset = UNSET + debit_element_id: None | str | Unset = UNSET + credit_element_id: None | str | Unset = UNSET + amount: float | None | Unset = UNSET + reason: None | str | Unset = UNSET reversal: ClosingEntryResponse | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - entry_id = self.entry_id + outcome = self.outcome - status = self.status + entry_id: None | str | Unset + if isinstance(self.entry_id, Unset): + entry_id = UNSET + else: + entry_id = self.entry_id + + status: None | str | Unset + if isinstance(self.status, Unset): + status = UNSET + else: + status = self.status - posting_date = self.posting_date.isoformat() + posting_date: None | str | Unset + if isinstance(self.posting_date, Unset): + posting_date = UNSET + elif isinstance(self.posting_date, datetime.date): + posting_date = self.posting_date.isoformat() + else: + posting_date = self.posting_date - memo = self.memo + memo: None | str | Unset + if isinstance(self.memo, Unset): + memo = UNSET + else: + memo = self.memo - debit_element_id = self.debit_element_id + debit_element_id: None | str | Unset + if isinstance(self.debit_element_id, Unset): + debit_element_id = UNSET + else: + debit_element_id = self.debit_element_id - credit_element_id = self.credit_element_id + credit_element_id: None | str | Unset + if isinstance(self.credit_element_id, Unset): + credit_element_id = UNSET + else: + credit_element_id = self.credit_element_id - amount = self.amount + amount: float | None | Unset + if isinstance(self.amount, Unset): + amount = UNSET + else: + amount = self.amount + + reason: None | str | Unset + if isinstance(self.reason, Unset): + reason = UNSET + else: + reason = self.reason reversal: dict[str, Any] | None | Unset if isinstance(self.reversal, Unset): @@ -64,15 +108,25 @@ def to_dict(self) -> dict[str, Any]: field_dict.update(self.additional_properties) field_dict.update( { - "entry_id": entry_id, - "status": status, - "posting_date": posting_date, - "memo": memo, - "debit_element_id": debit_element_id, - "credit_element_id": credit_element_id, - "amount": amount, + "outcome": outcome, } ) + if entry_id is not UNSET: + field_dict["entry_id"] = entry_id + if status is not UNSET: + field_dict["status"] = status + if posting_date is not UNSET: + field_dict["posting_date"] = posting_date + if memo is not UNSET: + field_dict["memo"] = memo + if debit_element_id is not UNSET: + field_dict["debit_element_id"] = debit_element_id + if credit_element_id is not UNSET: + field_dict["credit_element_id"] = credit_element_id + if amount is not UNSET: + field_dict["amount"] = amount + if reason is not UNSET: + field_dict["reason"] = reason if reversal is not UNSET: field_dict["reversal"] = reversal @@ -81,19 +135,87 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) - entry_id = d.pop("entry_id") + outcome = d.pop("outcome") - status = d.pop("status") + def _parse_entry_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) - posting_date = isoparse(d.pop("posting_date")).date() + entry_id = _parse_entry_id(d.pop("entry_id", UNSET)) - memo = d.pop("memo") + def _parse_status(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + status = _parse_status(d.pop("status", UNSET)) + + def _parse_posting_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() + posting_date_type_0 = isoparse(data).date() + + return posting_date_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.date | None | Unset, data) - debit_element_id = d.pop("debit_element_id") + posting_date = _parse_posting_date(d.pop("posting_date", UNSET)) - credit_element_id = d.pop("credit_element_id") + def _parse_memo(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + memo = _parse_memo(d.pop("memo", UNSET)) + + def _parse_debit_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) + + debit_element_id = _parse_debit_element_id(d.pop("debit_element_id", UNSET)) + + def _parse_credit_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) + + credit_element_id = _parse_credit_element_id(d.pop("credit_element_id", UNSET)) + + def _parse_amount(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + amount = _parse_amount(d.pop("amount", UNSET)) + + def _parse_reason(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) - amount = d.pop("amount") + reason = _parse_reason(d.pop("reason", UNSET)) def _parse_reversal(data: object) -> ClosingEntryResponse | None | Unset: if data is None: @@ -113,6 +235,7 @@ def _parse_reversal(data: object) -> ClosingEntryResponse | None | Unset: reversal = _parse_reversal(d.pop("reversal", UNSET)) closing_entry_response = cls( + outcome=outcome, entry_id=entry_id, status=status, posting_date=posting_date, @@ -120,6 +243,7 @@ def _parse_reversal(data: object) -> ClosingEntryResponse | None | Unset: debit_element_id=debit_element_id, credit_element_id=credit_element_id, amount=amount, + reason=reason, reversal=reversal, ) diff --git a/robosystems_client/models/create_manual_closing_entry_request.py b/robosystems_client/models/create_manual_closing_entry_request.py new file mode 100644 index 0000000..a2cc68b --- /dev/null +++ b/robosystems_client/models/create_manual_closing_entry_request.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field +from dateutil.parser import isoparse + +from ..models.create_manual_closing_entry_request_entry_type import ( + CreateManualClosingEntryRequestEntryType, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.manual_line_item_request import ManualLineItemRequest + + +T = TypeVar("T", bound="CreateManualClosingEntryRequest") + + +@_attrs_define +class CreateManualClosingEntryRequest: + """ + Attributes: + posting_date (datetime.date): Posting date for the entry + memo (str): Memo describing the business event (e.g., 'Sale of computer to Vendor X on 3/15') + line_items (list[ManualLineItemRequest]): Line items; must balance (total DR = total CR) + entry_type (CreateManualClosingEntryRequestEntryType | Unset): Entry type: 'closing' (default), 'adjusting', + 'standard', 'reversing' Default: CreateManualClosingEntryRequestEntryType.CLOSING. + """ + + posting_date: datetime.date + memo: str + line_items: list[ManualLineItemRequest] + entry_type: CreateManualClosingEntryRequestEntryType | Unset = ( + CreateManualClosingEntryRequestEntryType.CLOSING + ) + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + posting_date = self.posting_date.isoformat() + + memo = self.memo + + line_items = [] + for line_items_item_data in self.line_items: + line_items_item = line_items_item_data.to_dict() + line_items.append(line_items_item) + + entry_type: str | Unset = UNSET + if not isinstance(self.entry_type, Unset): + entry_type = self.entry_type.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "posting_date": posting_date, + "memo": memo, + "line_items": line_items, + } + ) + if entry_type is not UNSET: + field_dict["entry_type"] = entry_type + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.manual_line_item_request import ManualLineItemRequest + + d = dict(src_dict) + posting_date = isoparse(d.pop("posting_date")).date() + + memo = d.pop("memo") + + line_items = [] + _line_items = d.pop("line_items") + for line_items_item_data in _line_items: + line_items_item = ManualLineItemRequest.from_dict(line_items_item_data) + + line_items.append(line_items_item) + + _entry_type = d.pop("entry_type", UNSET) + entry_type: CreateManualClosingEntryRequestEntryType | Unset + if isinstance(_entry_type, Unset): + entry_type = UNSET + else: + entry_type = CreateManualClosingEntryRequestEntryType(_entry_type) + + create_manual_closing_entry_request = cls( + posting_date=posting_date, + memo=memo, + line_items=line_items, + entry_type=entry_type, + ) + + create_manual_closing_entry_request.additional_properties = d + return create_manual_closing_entry_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/create_manual_closing_entry_request_entry_type.py b/robosystems_client/models/create_manual_closing_entry_request_entry_type.py new file mode 100644 index 0000000..fbfbc62 --- /dev/null +++ b/robosystems_client/models/create_manual_closing_entry_request_entry_type.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class CreateManualClosingEntryRequestEntryType(str, Enum): + ADJUSTING = "adjusting" + CLOSING = "closing" + REVERSING = "reversing" + STANDARD = "standard" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/create_schedule_request.py b/robosystems_client/models/create_schedule_request.py index 65653e1..d01324a 100644 --- a/robosystems_client/models/create_schedule_request.py +++ b/robosystems_client/models/create_schedule_request.py @@ -30,6 +30,9 @@ class CreateScheduleRequest: entry_template (EntryTemplateRequest): taxonomy_id (None | str | Unset): Taxonomy ID (auto-creates if omitted) schedule_metadata (None | ScheduleMetadataRequest | Unset): + closed_through (datetime.date | None | Unset): If provided, facts with period_end ≤ this date are flagged as + 'historical' (already reflected in opening balances, ignored by the close workflow). Used during initial ledger + setup to create schedules whose early facts have already been captured elsewhere. """ name: str @@ -40,6 +43,7 @@ class CreateScheduleRequest: entry_template: EntryTemplateRequest taxonomy_id: None | str | Unset = UNSET schedule_metadata: None | ScheduleMetadataRequest | Unset = UNSET + closed_through: datetime.date | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -71,6 +75,14 @@ def to_dict(self) -> dict[str, Any]: 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 + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -87,6 +99,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["taxonomy_id"] = taxonomy_id if schedule_metadata is not UNSET: field_dict["schedule_metadata"] = schedule_metadata + if closed_through is not UNSET: + field_dict["closed_through"] = closed_through return field_dict @@ -136,6 +150,23 @@ def _parse_schedule_metadata( 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 = isoparse(data).date() + + 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)) + create_schedule_request = cls( name=name, element_ids=element_ids, @@ -145,6 +176,7 @@ def _parse_schedule_metadata( entry_template=entry_template, taxonomy_id=taxonomy_id, schedule_metadata=schedule_metadata, + closed_through=closed_through, ) create_schedule_request.additional_properties = d diff --git a/robosystems_client/models/create_structure_request_structure_type.py b/robosystems_client/models/create_structure_request_structure_type.py index cd48efc..574781c 100644 --- a/robosystems_client/models/create_structure_request_structure_type.py +++ b/robosystems_client/models/create_structure_request_structure_type.py @@ -3,7 +3,6 @@ class CreateStructureRequestStructureType(str, Enum): BALANCE_SHEET = "balance_sheet" - CASH_FLOW_STATEMENT = "cash_flow_statement" CHART_OF_ACCOUNTS = "chart_of_accounts" COA_MAPPING = "coa_mapping" CUSTOM = "custom" diff --git a/robosystems_client/models/draft_entry_response.py b/robosystems_client/models/draft_entry_response.py new file mode 100644 index 0000000..435bc78 --- /dev/null +++ b/robosystems_client/models/draft_entry_response.py @@ -0,0 +1,212 @@ +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 dateutil.parser import isoparse + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.draft_line_item import DraftLineItem + + +T = TypeVar("T", bound="DraftEntryResponse") + + +@_attrs_define +class DraftEntryResponse: + """A single draft entry with full line item detail for review. + + Attributes: + entry_id (str): + posting_date (datetime.date): + type_ (str): Entry type (e.g., 'closing', 'adjusting') + line_items (list[DraftLineItem]): + total_debit (int): Sum of debit amounts in cents + total_credit (int): Sum of credit amounts in cents + balanced (bool): True if total_debit == total_credit + memo (None | str | Unset): + provenance (None | str | Unset): Where the entry came from: 'ai_generated', 'manual_entry', etc. + source_structure_id (None | str | Unset): Schedule structure that generated this entry (if any) + source_structure_name (None | str | Unset): Human-readable name of the source schedule + """ + + entry_id: str + posting_date: datetime.date + type_: str + line_items: list[DraftLineItem] + total_debit: int + total_credit: int + balanced: bool + memo: None | str | Unset = UNSET + provenance: None | str | Unset = UNSET + source_structure_id: None | str | Unset = UNSET + source_structure_name: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + entry_id = self.entry_id + + posting_date = self.posting_date.isoformat() + + type_ = self.type_ + + line_items = [] + for line_items_item_data in self.line_items: + line_items_item = line_items_item_data.to_dict() + line_items.append(line_items_item) + + total_debit = self.total_debit + + total_credit = self.total_credit + + balanced = self.balanced + + memo: None | str | Unset + if isinstance(self.memo, Unset): + memo = UNSET + else: + memo = self.memo + + provenance: None | str | Unset + if isinstance(self.provenance, Unset): + provenance = UNSET + else: + provenance = self.provenance + + source_structure_id: None | str | Unset + if isinstance(self.source_structure_id, Unset): + source_structure_id = UNSET + else: + source_structure_id = self.source_structure_id + + source_structure_name: None | str | Unset + if isinstance(self.source_structure_name, Unset): + source_structure_name = UNSET + else: + source_structure_name = self.source_structure_name + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "entry_id": entry_id, + "posting_date": posting_date, + "type": type_, + "line_items": line_items, + "total_debit": total_debit, + "total_credit": total_credit, + "balanced": balanced, + } + ) + if memo is not UNSET: + field_dict["memo"] = memo + if provenance is not UNSET: + field_dict["provenance"] = provenance + if source_structure_id is not UNSET: + field_dict["source_structure_id"] = source_structure_id + if source_structure_name is not UNSET: + field_dict["source_structure_name"] = source_structure_name + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.draft_line_item import DraftLineItem + + d = dict(src_dict) + entry_id = d.pop("entry_id") + + posting_date = isoparse(d.pop("posting_date")).date() + + type_ = d.pop("type") + + line_items = [] + _line_items = d.pop("line_items") + for line_items_item_data in _line_items: + line_items_item = DraftLineItem.from_dict(line_items_item_data) + + line_items.append(line_items_item) + + total_debit = d.pop("total_debit") + + total_credit = d.pop("total_credit") + + balanced = d.pop("balanced") + + def _parse_memo(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + memo = _parse_memo(d.pop("memo", UNSET)) + + def _parse_provenance(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + provenance = _parse_provenance(d.pop("provenance", UNSET)) + + def _parse_source_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) + + source_structure_id = _parse_source_structure_id( + d.pop("source_structure_id", UNSET) + ) + + def _parse_source_structure_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + source_structure_name = _parse_source_structure_name( + d.pop("source_structure_name", UNSET) + ) + + draft_entry_response = cls( + entry_id=entry_id, + posting_date=posting_date, + type_=type_, + line_items=line_items, + total_debit=total_debit, + total_credit=total_credit, + balanced=balanced, + memo=memo, + provenance=provenance, + source_structure_id=source_structure_id, + source_structure_name=source_structure_name, + ) + + draft_entry_response.additional_properties = d + return draft_entry_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/draft_line_item.py b/robosystems_client/models/draft_line_item.py new file mode 100644 index 0000000..4ccc708 --- /dev/null +++ b/robosystems_client/models/draft_line_item.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="DraftLineItem") + + +@_attrs_define +class DraftLineItem: + """A single line item within a draft entry. + + Attributes: + line_item_id (str): + element_id (str): + element_name (str): + debit_amount (int): Debit amount in cents + credit_amount (int): Credit amount in cents + element_code (None | str | Unset): + description (None | str | Unset): + """ + + line_item_id: str + element_id: str + element_name: str + debit_amount: int + credit_amount: int + element_code: None | str | Unset = UNSET + description: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + line_item_id = self.line_item_id + + element_id = self.element_id + + element_name = self.element_name + + debit_amount = self.debit_amount + + credit_amount = self.credit_amount + + element_code: None | str | Unset + if isinstance(self.element_code, Unset): + element_code = UNSET + else: + element_code = self.element_code + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "line_item_id": line_item_id, + "element_id": element_id, + "element_name": element_name, + "debit_amount": debit_amount, + "credit_amount": credit_amount, + } + ) + if element_code is not UNSET: + field_dict["element_code"] = element_code + if description is not UNSET: + field_dict["description"] = description + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + line_item_id = d.pop("line_item_id") + + element_id = d.pop("element_id") + + element_name = d.pop("element_name") + + debit_amount = d.pop("debit_amount") + + credit_amount = d.pop("credit_amount") + + def _parse_element_code(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + element_code = _parse_element_code(d.pop("element_code", UNSET)) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + draft_line_item = cls( + line_item_id=line_item_id, + element_id=element_id, + element_name=element_name, + debit_amount=debit_amount, + credit_amount=credit_amount, + element_code=element_code, + description=description, + ) + + draft_line_item.additional_properties = d + return draft_line_item + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/entry_template_request.py b/robosystems_client/models/entry_template_request.py index 2351740..ac35d55 100644 --- a/robosystems_client/models/entry_template_request.py +++ b/robosystems_client/models/entry_template_request.py @@ -6,6 +6,7 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field +from ..models.entry_template_request_entry_type import EntryTemplateRequestEntryType from ..types import UNSET, Unset T = TypeVar("T", bound="EntryTemplateRequest") @@ -17,14 +18,17 @@ class EntryTemplateRequest: Attributes: debit_element_id (str): Element to debit (e.g., Depreciation Expense) credit_element_id (str): Element to credit (e.g., Accumulated Depreciation) - entry_type (str | Unset): Entry type for generated entries Default: 'closing'. + entry_type (EntryTemplateRequestEntryType | Unset): Entry type for generated entries Default: + EntryTemplateRequestEntryType.CLOSING. memo_template (str | Unset): Memo template ({structure_name} is replaced) Default: ''. auto_reverse (bool | Unset): Auto-generate a reversing entry on the first day of the next period Default: False. """ debit_element_id: str credit_element_id: str - entry_type: str | Unset = "closing" + entry_type: EntryTemplateRequestEntryType | Unset = ( + EntryTemplateRequestEntryType.CLOSING + ) memo_template: str | Unset = "" auto_reverse: bool | Unset = False additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -34,7 +38,9 @@ def to_dict(self) -> dict[str, Any]: credit_element_id = self.credit_element_id - entry_type = self.entry_type + entry_type: str | Unset = UNSET + if not isinstance(self.entry_type, Unset): + entry_type = self.entry_type.value memo_template = self.memo_template @@ -64,7 +70,12 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: credit_element_id = d.pop("credit_element_id") - entry_type = d.pop("entry_type", UNSET) + _entry_type = d.pop("entry_type", UNSET) + entry_type: EntryTemplateRequestEntryType | Unset + if isinstance(_entry_type, Unset): + entry_type = UNSET + else: + entry_type = EntryTemplateRequestEntryType(_entry_type) memo_template = d.pop("memo_template", UNSET) diff --git a/robosystems_client/models/entry_template_request_entry_type.py b/robosystems_client/models/entry_template_request_entry_type.py new file mode 100644 index 0000000..e9b043f --- /dev/null +++ b/robosystems_client/models/entry_template_request_entry_type.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class EntryTemplateRequestEntryType(str, Enum): + ADJUSTING = "adjusting" + CLOSING = "closing" + REVERSING = "reversing" + STANDARD = "standard" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/fiscal_calendar_response.py b/robosystems_client/models/fiscal_calendar_response.py new file mode 100644 index 0000000..e40ba23 --- /dev/null +++ b/robosystems_client/models/fiscal_calendar_response.py @@ -0,0 +1,274 @@ +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 dateutil.parser import isoparse + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.fiscal_period_summary import FiscalPeriodSummary + + +T = TypeVar("T", bound="FiscalCalendarResponse") + + +@_attrs_define +class FiscalCalendarResponse: + """Current fiscal calendar state for a graph. + + Attributes: + graph_id (str): + fiscal_year_start_month (int): + closed_through (None | str | Unset): Latest closed period (YYYY-MM), or null if nothing closed + close_target (None | str | Unset): Target period the user wants closed through (YYYY-MM) + gap_periods (int | Unset): Number of periods between closed_through and close_target (inclusive of + close_target). 0 means caught up. Default: 0. + catch_up_sequence (list[str] | Unset): Ordered list of periods that a close run would process + closeable_now (bool | Unset): Whether the next period in the catch-up sequence passes all closeable gates + Default: False. + blockers (list[str] | Unset): Structured blocker codes when closeable_now is False: 'sequence_violation', + 'period_incomplete', 'sync_stale', 'calendar_not_initialized', 'period_already_closed' + last_close_at (datetime.datetime | None | Unset): + initialized_at (datetime.datetime | None | Unset): + last_sync_at (datetime.datetime | None | Unset): Most recent QB sync timestamp (if connected) + periods (list[FiscalPeriodSummary] | Unset): Fiscal period rows for this graph + """ + + graph_id: str + fiscal_year_start_month: int + closed_through: None | str | Unset = UNSET + close_target: None | str | Unset = UNSET + gap_periods: int | Unset = 0 + catch_up_sequence: list[str] | Unset = UNSET + closeable_now: bool | Unset = False + blockers: list[str] | Unset = UNSET + last_close_at: datetime.datetime | None | Unset = UNSET + initialized_at: datetime.datetime | None | Unset = UNSET + last_sync_at: datetime.datetime | None | Unset = UNSET + periods: list[FiscalPeriodSummary] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + graph_id = self.graph_id + + fiscal_year_start_month = self.fiscal_year_start_month + + closed_through: None | str | Unset + if isinstance(self.closed_through, Unset): + closed_through = UNSET + else: + closed_through = self.closed_through + + close_target: None | str | Unset + if isinstance(self.close_target, Unset): + close_target = UNSET + else: + close_target = self.close_target + + gap_periods = self.gap_periods + + catch_up_sequence: list[str] | Unset = UNSET + if not isinstance(self.catch_up_sequence, Unset): + catch_up_sequence = self.catch_up_sequence + + closeable_now = self.closeable_now + + blockers: list[str] | Unset = UNSET + if not isinstance(self.blockers, Unset): + blockers = self.blockers + + last_close_at: None | str | Unset + if isinstance(self.last_close_at, Unset): + last_close_at = UNSET + elif isinstance(self.last_close_at, datetime.datetime): + last_close_at = self.last_close_at.isoformat() + else: + last_close_at = self.last_close_at + + initialized_at: None | str | Unset + if isinstance(self.initialized_at, Unset): + initialized_at = UNSET + elif isinstance(self.initialized_at, datetime.datetime): + initialized_at = self.initialized_at.isoformat() + else: + initialized_at = self.initialized_at + + last_sync_at: None | str | Unset + if isinstance(self.last_sync_at, Unset): + last_sync_at = UNSET + elif isinstance(self.last_sync_at, datetime.datetime): + last_sync_at = self.last_sync_at.isoformat() + else: + last_sync_at = self.last_sync_at + + periods: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.periods, Unset): + periods = [] + for periods_item_data in self.periods: + periods_item = periods_item_data.to_dict() + periods.append(periods_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "graph_id": graph_id, + "fiscal_year_start_month": fiscal_year_start_month, + } + ) + if closed_through is not UNSET: + field_dict["closed_through"] = closed_through + if close_target is not UNSET: + field_dict["close_target"] = close_target + if gap_periods is not UNSET: + field_dict["gap_periods"] = gap_periods + if catch_up_sequence is not UNSET: + field_dict["catch_up_sequence"] = catch_up_sequence + if closeable_now is not UNSET: + field_dict["closeable_now"] = closeable_now + if blockers is not UNSET: + field_dict["blockers"] = blockers + if last_close_at is not UNSET: + field_dict["last_close_at"] = last_close_at + if initialized_at is not UNSET: + field_dict["initialized_at"] = initialized_at + if last_sync_at is not UNSET: + field_dict["last_sync_at"] = last_sync_at + if periods is not UNSET: + field_dict["periods"] = periods + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.fiscal_period_summary import FiscalPeriodSummary + + d = dict(src_dict) + graph_id = d.pop("graph_id") + + fiscal_year_start_month = d.pop("fiscal_year_start_month") + + def _parse_closed_through(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + closed_through = _parse_closed_through(d.pop("closed_through", UNSET)) + + def _parse_close_target(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + close_target = _parse_close_target(d.pop("close_target", UNSET)) + + gap_periods = d.pop("gap_periods", UNSET) + + catch_up_sequence = cast(list[str], d.pop("catch_up_sequence", UNSET)) + + closeable_now = d.pop("closeable_now", UNSET) + + blockers = cast(list[str], d.pop("blockers", UNSET)) + + def _parse_last_close_at(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + last_close_at_type_0 = isoparse(data) + + return last_close_at_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + last_close_at = _parse_last_close_at(d.pop("last_close_at", UNSET)) + + def _parse_initialized_at(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + initialized_at_type_0 = isoparse(data) + + return initialized_at_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + initialized_at = _parse_initialized_at(d.pop("initialized_at", UNSET)) + + def _parse_last_sync_at(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + last_sync_at_type_0 = isoparse(data) + + return last_sync_at_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + last_sync_at = _parse_last_sync_at(d.pop("last_sync_at", UNSET)) + + _periods = d.pop("periods", UNSET) + periods: list[FiscalPeriodSummary] | Unset = UNSET + if _periods is not UNSET: + periods = [] + for periods_item_data in _periods: + periods_item = FiscalPeriodSummary.from_dict(periods_item_data) + + periods.append(periods_item) + + fiscal_calendar_response = cls( + graph_id=graph_id, + fiscal_year_start_month=fiscal_year_start_month, + closed_through=closed_through, + close_target=close_target, + gap_periods=gap_periods, + catch_up_sequence=catch_up_sequence, + closeable_now=closeable_now, + blockers=blockers, + last_close_at=last_close_at, + initialized_at=initialized_at, + last_sync_at=last_sync_at, + periods=periods, + ) + + fiscal_calendar_response.additional_properties = d + return fiscal_calendar_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/fiscal_period_summary.py b/robosystems_client/models/fiscal_period_summary.py new file mode 100644 index 0000000..4d527a7 --- /dev/null +++ b/robosystems_client/models/fiscal_period_summary.py @@ -0,0 +1,119 @@ +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 dateutil.parser import isoparse + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="FiscalPeriodSummary") + + +@_attrs_define +class FiscalPeriodSummary: + """ + Attributes: + name (str): Period name (YYYY-MM) + start_date (datetime.date): + end_date (datetime.date): + status (str): 'open' | 'closing' | 'closed' + closed_at (datetime.datetime | None | Unset): + """ + + name: str + start_date: datetime.date + end_date: datetime.date + status: str + closed_at: datetime.datetime | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + start_date = self.start_date.isoformat() + + end_date = self.end_date.isoformat() + + status = self.status + + closed_at: None | str | Unset + if isinstance(self.closed_at, Unset): + closed_at = UNSET + elif isinstance(self.closed_at, datetime.datetime): + closed_at = self.closed_at.isoformat() + else: + closed_at = self.closed_at + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "name": name, + "start_date": start_date, + "end_date": end_date, + "status": status, + } + ) + if closed_at is not UNSET: + field_dict["closed_at"] = closed_at + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + name = d.pop("name") + + start_date = isoparse(d.pop("start_date")).date() + + end_date = isoparse(d.pop("end_date")).date() + + status = d.pop("status") + + def _parse_closed_at(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + closed_at_type_0 = isoparse(data) + + return closed_at_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + closed_at = _parse_closed_at(d.pop("closed_at", UNSET)) + + fiscal_period_summary = cls( + name=name, + start_date=start_date, + end_date=end_date, + status=status, + closed_at=closed_at, + ) + + fiscal_period_summary.additional_properties = d + return fiscal_period_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/initial_entity_data.py b/robosystems_client/models/initial_entity_data.py index c128cda..97fd2ba 100644 --- a/robosystems_client/models/initial_entity_data.py +++ b/robosystems_client/models/initial_entity_data.py @@ -21,6 +21,8 @@ class InitialEntityData: Attributes: name (str): Entity name uri (str): Entity website or URI + ticker (None | str | Unset): Entity symbol/ticker (e.g., 'HARB', 'NVDA'). Auto-generated from name if not + provided. cik (None | str | Unset): CIK number for SEC filings sic (None | str | Unset): SIC code sic_description (None | str | Unset): SIC description @@ -32,6 +34,7 @@ class InitialEntityData: name: str uri: str + ticker: None | str | Unset = UNSET cik: None | str | Unset = UNSET sic: None | str | Unset = UNSET sic_description: None | str | Unset = UNSET @@ -46,6 +49,12 @@ def to_dict(self) -> dict[str, Any]: uri = self.uri + ticker: None | str | Unset + if isinstance(self.ticker, Unset): + ticker = UNSET + else: + ticker = self.ticker + cik: None | str | Unset if isinstance(self.cik, Unset): cik = UNSET @@ -96,6 +105,8 @@ def to_dict(self) -> dict[str, Any]: "uri": uri, } ) + if ticker is not UNSET: + field_dict["ticker"] = ticker if cik is not UNSET: field_dict["cik"] = cik if sic is not UNSET: @@ -120,6 +131,15 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: uri = d.pop("uri") + def _parse_ticker(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + ticker = _parse_ticker(d.pop("ticker", UNSET)) + def _parse_cik(data: object) -> None | str | Unset: if data is None: return data @@ -188,6 +208,7 @@ def _parse_ein(data: object) -> None | str | Unset: initial_entity_data = cls( name=name, uri=uri, + ticker=ticker, cik=cik, sic=sic, sic_description=sic_description, diff --git a/robosystems_client/models/initialize_ledger_request.py b/robosystems_client/models/initialize_ledger_request.py new file mode 100644 index 0000000..c75d38b --- /dev/null +++ b/robosystems_client/models/initialize_ledger_request.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="InitializeLedgerRequest") + + +@_attrs_define +class InitializeLedgerRequest: + """ + Attributes: + closed_through (None | str | Unset): YYYY-MM period. Periods ≤ this date are treated as historical (already + closed before the user joined). Set to null for a fresh business with no prior close state. + fiscal_year_start_month (int | Unset): Fiscal year start month (1-12). Defaults to calendar year. Default: 1. + auto_seed_schedules (bool | Unset): If true, run the SchedulerAgent to create schedules from historical BS + activity. NOT YET IMPLEMENTED — returns a warning in v1. Default: False. + earliest_data_period (None | str | Unset): YYYY-MM period representing the earliest month that has transaction + data. Used to create FiscalPeriod rows. Defaults to 24 months before the current month. + note (None | str | Unset): Free-form note attached to the audit event + """ + + closed_through: None | str | Unset = UNSET + fiscal_year_start_month: int | Unset = 1 + auto_seed_schedules: bool | Unset = False + earliest_data_period: None | str | Unset = UNSET + note: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + closed_through: None | str | Unset + if isinstance(self.closed_through, Unset): + closed_through = UNSET + else: + closed_through = self.closed_through + + fiscal_year_start_month = self.fiscal_year_start_month + + auto_seed_schedules = self.auto_seed_schedules + + earliest_data_period: None | str | Unset + if isinstance(self.earliest_data_period, Unset): + earliest_data_period = UNSET + else: + earliest_data_period = self.earliest_data_period + + note: None | str | Unset + if isinstance(self.note, Unset): + note = UNSET + else: + note = self.note + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if closed_through is not UNSET: + field_dict["closed_through"] = closed_through + if fiscal_year_start_month is not UNSET: + field_dict["fiscal_year_start_month"] = fiscal_year_start_month + if auto_seed_schedules is not UNSET: + field_dict["auto_seed_schedules"] = auto_seed_schedules + if earliest_data_period is not UNSET: + field_dict["earliest_data_period"] = earliest_data_period + if note is not UNSET: + field_dict["note"] = note + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_closed_through(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + closed_through = _parse_closed_through(d.pop("closed_through", UNSET)) + + fiscal_year_start_month = d.pop("fiscal_year_start_month", UNSET) + + auto_seed_schedules = d.pop("auto_seed_schedules", UNSET) + + def _parse_earliest_data_period(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + earliest_data_period = _parse_earliest_data_period( + d.pop("earliest_data_period", UNSET) + ) + + def _parse_note(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + note = _parse_note(d.pop("note", UNSET)) + + initialize_ledger_request = cls( + closed_through=closed_through, + fiscal_year_start_month=fiscal_year_start_month, + auto_seed_schedules=auto_seed_schedules, + earliest_data_period=earliest_data_period, + note=note, + ) + + initialize_ledger_request.additional_properties = d + return initialize_ledger_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/initialize_ledger_response.py b/robosystems_client/models/initialize_ledger_response.py new file mode 100644 index 0000000..4726d75 --- /dev/null +++ b/robosystems_client/models/initialize_ledger_response.py @@ -0,0 +1,89 @@ +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.fiscal_calendar_response import FiscalCalendarResponse + + +T = TypeVar("T", bound="InitializeLedgerResponse") + + +@_attrs_define +class InitializeLedgerResponse: + """ + Attributes: + fiscal_calendar (FiscalCalendarResponse): Current fiscal calendar state for a graph. + periods_created (int | Unset): Number of FiscalPeriod rows created by initialization Default: 0. + warnings (list[str] | Unset): Non-fatal warnings (e.g., auto_seed_schedules not implemented) + """ + + fiscal_calendar: FiscalCalendarResponse + periods_created: 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]: + fiscal_calendar = self.fiscal_calendar.to_dict() + + periods_created = self.periods_created + + 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( + { + "fiscal_calendar": fiscal_calendar, + } + ) + if periods_created is not UNSET: + field_dict["periods_created"] = periods_created + if warnings is not UNSET: + field_dict["warnings"] = warnings + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.fiscal_calendar_response import FiscalCalendarResponse + + d = dict(src_dict) + fiscal_calendar = FiscalCalendarResponse.from_dict(d.pop("fiscal_calendar")) + + periods_created = d.pop("periods_created", UNSET) + + warnings = cast(list[str], d.pop("warnings", UNSET)) + + initialize_ledger_response = cls( + fiscal_calendar=fiscal_calendar, + periods_created=periods_created, + warnings=warnings, + ) + + initialize_ledger_response.additional_properties = d + return initialize_ledger_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/manual_line_item_request.py b/robosystems_client/models/manual_line_item_request.py new file mode 100644 index 0000000..2c5e920 --- /dev/null +++ b/robosystems_client/models/manual_line_item_request.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ManualLineItemRequest") + + +@_attrs_define +class ManualLineItemRequest: + """ + Attributes: + element_id (str): Element ID (chart of accounts) + debit_amount (int | Unset): Debit in cents Default: 0. + credit_amount (int | Unset): Credit in cents Default: 0. + description (None | str | Unset): + """ + + element_id: str + debit_amount: int | Unset = 0 + credit_amount: int | Unset = 0 + description: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + element_id = self.element_id + + debit_amount = self.debit_amount + + credit_amount = self.credit_amount + + description: None | str | Unset + if isinstance(self.description, Unset): + description = UNSET + else: + description = self.description + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "element_id": element_id, + } + ) + if debit_amount is not UNSET: + field_dict["debit_amount"] = debit_amount + if credit_amount is not UNSET: + field_dict["credit_amount"] = credit_amount + if description is not UNSET: + field_dict["description"] = description + + 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") + + debit_amount = d.pop("debit_amount", UNSET) + + credit_amount = d.pop("credit_amount", UNSET) + + def _parse_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + description = _parse_description(d.pop("description", UNSET)) + + manual_line_item_request = cls( + element_id=element_id, + debit_amount=debit_amount, + credit_amount=credit_amount, + description=description, + ) + + manual_line_item_request.additional_properties = d + return manual_line_item_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/period_drafts_response.py b/robosystems_client/models/period_drafts_response.py new file mode 100644 index 0000000..2f98e3b --- /dev/null +++ b/robosystems_client/models/period_drafts_response.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field +from dateutil.parser import isoparse + +if TYPE_CHECKING: + from ..models.draft_entry_response import DraftEntryResponse + + +T = TypeVar("T", bound="PeriodDraftsResponse") + + +@_attrs_define +class PeriodDraftsResponse: + """All draft entries for a fiscal period, ready for review before close. + + Attributes: + period (str): YYYY-MM period name + period_start (datetime.date): + period_end (datetime.date): + draft_count (int): + total_debit (int): Sum across all drafts, in cents + total_credit (int): Sum across all drafts, in cents + all_balanced (bool): True if every draft entry has debit == credit + drafts (list[DraftEntryResponse]): + """ + + period: str + period_start: datetime.date + period_end: datetime.date + draft_count: int + total_debit: int + total_credit: int + all_balanced: bool + drafts: list[DraftEntryResponse] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + period = self.period + + period_start = self.period_start.isoformat() + + period_end = self.period_end.isoformat() + + draft_count = self.draft_count + + total_debit = self.total_debit + + total_credit = self.total_credit + + all_balanced = self.all_balanced + + drafts = [] + for drafts_item_data in self.drafts: + drafts_item = drafts_item_data.to_dict() + drafts.append(drafts_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "period": period, + "period_start": period_start, + "period_end": period_end, + "draft_count": draft_count, + "total_debit": total_debit, + "total_credit": total_credit, + "all_balanced": all_balanced, + "drafts": drafts, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.draft_entry_response import DraftEntryResponse + + d = dict(src_dict) + period = d.pop("period") + + period_start = isoparse(d.pop("period_start")).date() + + period_end = isoparse(d.pop("period_end")).date() + + draft_count = d.pop("draft_count") + + total_debit = d.pop("total_debit") + + total_credit = d.pop("total_credit") + + all_balanced = d.pop("all_balanced") + + drafts = [] + _drafts = d.pop("drafts") + for drafts_item_data in _drafts: + drafts_item = DraftEntryResponse.from_dict(drafts_item_data) + + drafts.append(drafts_item) + + period_drafts_response = cls( + period=period, + period_start=period_start, + period_end=period_end, + draft_count=draft_count, + total_debit=total_debit, + total_credit=total_credit, + all_balanced=all_balanced, + drafts=drafts, + ) + + period_drafts_response.additional_properties = d + return period_drafts_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/reopen_period_request.py b/robosystems_client/models/reopen_period_request.py new file mode 100644 index 0000000..f8e6e46 --- /dev/null +++ b/robosystems_client/models/reopen_period_request.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ReopenPeriodRequest") + + +@_attrs_define +class ReopenPeriodRequest: + """ + Attributes: + reason (str): Required reason for the reopen (captured in audit log) + note (None | str | Unset): Additional free-form note + """ + + reason: str + note: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + reason = self.reason + + note: None | str | Unset + if isinstance(self.note, Unset): + note = UNSET + else: + note = self.note + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "reason": reason, + } + ) + if note is not UNSET: + field_dict["note"] = note + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + reason = d.pop("reason") + + def _parse_note(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + note = _parse_note(d.pop("note", UNSET)) + + reopen_period_request = cls( + reason=reason, + note=note, + ) + + reopen_period_request.additional_properties = d + return reopen_period_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/set_close_target_request.py b/robosystems_client/models/set_close_target_request.py new file mode 100644 index 0000000..64a428b --- /dev/null +++ b/robosystems_client/models/set_close_target_request.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="SetCloseTargetRequest") + + +@_attrs_define +class SetCloseTargetRequest: + """ + Attributes: + period (str): Target period in YYYY-MM format + note (None | str | Unset): Free-form note attached to the audit event + """ + + period: str + note: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + period = self.period + + note: None | str | Unset + if isinstance(self.note, Unset): + note = UNSET + else: + note = self.note + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "period": period, + } + ) + if note is not UNSET: + field_dict["note"] = note + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + period = d.pop("period") + + def _parse_note(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + note = _parse_note(d.pop("note", UNSET)) + + set_close_target_request = cls( + period=period, + note=note, + ) + + set_close_target_request.additional_properties = d + return set_close_target_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/truncate_schedule_request.py b/robosystems_client/models/truncate_schedule_request.py new file mode 100644 index 0000000..94b221d --- /dev/null +++ b/robosystems_client/models/truncate_schedule_request.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import datetime +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 dateutil.parser import isoparse + +T = TypeVar("T", bound="TruncateScheduleRequest") + + +@_attrs_define +class TruncateScheduleRequest: + """ + Attributes: + new_end_date (datetime.date): New last-covered date for the schedule. Facts with period_start > this date are + deleted (along with any stale draft entries they produced). Historical facts (already posted) are preserved. + reason (str): Required reason for the truncation (captured in audit log). + """ + + new_end_date: datetime.date + reason: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + new_end_date = self.new_end_date.isoformat() + + reason = self.reason + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "new_end_date": new_end_date, + "reason": reason, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + new_end_date = isoparse(d.pop("new_end_date")).date() + + reason = d.pop("reason") + + truncate_schedule_request = cls( + new_end_date=new_end_date, + reason=reason, + ) + + truncate_schedule_request.additional_properties = d + return truncate_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/truncate_schedule_response.py b/robosystems_client/models/truncate_schedule_response.py new file mode 100644 index 0000000..74203e5 --- /dev/null +++ b/robosystems_client/models/truncate_schedule_response.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import datetime +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 dateutil.parser import isoparse + +T = TypeVar("T", bound="TruncateScheduleResponse") + + +@_attrs_define +class TruncateScheduleResponse: + """ + Attributes: + structure_id (str): + new_end_date (datetime.date): + facts_deleted (int): + reason (str): + """ + + structure_id: str + new_end_date: datetime.date + facts_deleted: int + reason: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + structure_id = self.structure_id + + new_end_date = self.new_end_date.isoformat() + + facts_deleted = self.facts_deleted + + reason = self.reason + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "structure_id": structure_id, + "new_end_date": new_end_date, + "facts_deleted": facts_deleted, + "reason": reason, + } + ) + + 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") + + new_end_date = isoparse(d.pop("new_end_date")).date() + + facts_deleted = d.pop("facts_deleted") + + reason = d.pop("reason") + + truncate_schedule_response = cls( + structure_id=structure_id, + new_end_date=new_end_date, + facts_deleted=facts_deleted, + reason=reason, + ) + + truncate_schedule_response.additional_properties = d + return truncate_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/tests/test_ledger_client.py b/tests/test_ledger_client.py index 7e075dd..9ad8302 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -531,3 +531,449 @@ def test_get_account_rollups(self, mock_rollups, mock_config, graph_id): ) assert len(result.rollups) == 1 + + +@pytest.mark.unit +class TestLedgerFiscalCalendar: + """Test suite for fiscal calendar / period-close operations.""" + + @patch("robosystems_client.extensions.ledger_client.initialize_ledger") + def test_initialize_ledger(self, mock_init, mock_config, graph_id): + """Test initializing the fiscal calendar — 201 on success.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.CREATED + mock_resp.parsed = Mock(initialized=True, closed_through="2024-12-31") + mock_init.return_value = mock_resp + + client = LedgerClient(mock_config) + result = client.initialize_ledger( + graph_id, + closed_through="2024-12-31", + fiscal_year_start_month=1, + note="Initial setup", + ) + + assert result.initialized is True + mock_init.assert_called_once() + body = mock_init.call_args[1]["body"] + assert body.closed_through == "2024-12-31" + assert body.fiscal_year_start_month == 1 + assert body.note == "Initial setup" + + @patch("robosystems_client.extensions.ledger_client.initialize_ledger") + def test_initialize_ledger_omits_optional_as_unset( + self, mock_init, mock_config, graph_id + ): + """Omitted optional args should serialize as UNSET, not None.""" + from robosystems_client.types import UNSET + + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.CREATED + mock_resp.parsed = Mock() + mock_init.return_value = mock_resp + + client = LedgerClient(mock_config) + client.initialize_ledger(graph_id) + + body = mock_init.call_args[1]["body"] + assert body.closed_through is UNSET + assert body.fiscal_year_start_month is UNSET + assert body.earliest_data_period is UNSET + assert body.auto_seed_schedules is UNSET + assert body.note is UNSET + + @patch("robosystems_client.extensions.ledger_client.initialize_ledger") + def test_initialize_ledger_200_is_error(self, mock_init, mock_config, graph_id): + """A 200 (instead of 201) should raise — guards against status typos.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.OK + mock_init.return_value = mock_resp + + client = LedgerClient(mock_config) + + with pytest.raises(RuntimeError, match="Initialize ledger failed"): + client.initialize_ledger(graph_id) + + @patch("robosystems_client.extensions.ledger_client.initialize_ledger") + def test_initialize_ledger_conflict(self, mock_init, mock_config, graph_id): + """409 already-initialized should raise.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.CONFLICT + mock_init.return_value = mock_resp + + client = LedgerClient(mock_config) + + with pytest.raises(RuntimeError, match="Initialize ledger failed"): + client.initialize_ledger(graph_id) + + @patch("robosystems_client.extensions.ledger_client.get_fiscal_calendar") + def test_get_fiscal_calendar(self, mock_get, mock_config, graph_id): + """Test reading fiscal calendar state.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.OK + mock_resp.parsed = Mock( + closed_through="2025-03-31", close_target="2025-06-30", closeable=True + ) + mock_get.return_value = mock_resp + + client = LedgerClient(mock_config) + result = client.get_fiscal_calendar(graph_id) + + assert result.closed_through == "2025-03-31" + assert result.closeable is True + + @patch("robosystems_client.extensions.ledger_client.get_fiscal_calendar") + def test_get_fiscal_calendar_error(self, mock_get, mock_config, graph_id): + """Test get fiscal calendar raises on error.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.NOT_FOUND + mock_get.return_value = mock_resp + + client = LedgerClient(mock_config) + + with pytest.raises(RuntimeError, match="Get fiscal calendar failed"): + client.get_fiscal_calendar(graph_id) + + @patch("robosystems_client.extensions.ledger_client.set_close_target") + def test_set_close_target(self, mock_set, mock_config, graph_id): + """Test setting the close target.""" + from robosystems_client.types import UNSET + + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.OK + mock_resp.parsed = Mock(close_target="2025-06-30") + mock_set.return_value = mock_resp + + client = LedgerClient(mock_config) + result = client.set_close_target(graph_id, period="2025-06-30") + + assert result.close_target == "2025-06-30" + body = mock_set.call_args[1]["body"] + assert body.period == "2025-06-30" + assert body.note is UNSET + + @patch("robosystems_client.extensions.ledger_client.set_close_target") + def test_set_close_target_with_note(self, mock_set, mock_config, graph_id): + """Note passes through when provided.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.OK + mock_resp.parsed = Mock() + mock_set.return_value = mock_resp + + client = LedgerClient(mock_config) + client.set_close_target(graph_id, period="2025-06-30", note="Quarterly target") + + body = mock_set.call_args[1]["body"] + assert body.note == "Quarterly target" + + @patch("robosystems_client.extensions.ledger_client.set_close_target") + def test_set_close_target_error(self, mock_set, mock_config, graph_id): + """Test set close target raises on validation failure.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.BAD_REQUEST + mock_set.return_value = mock_resp + + client = LedgerClient(mock_config) + + with pytest.raises(RuntimeError, match="Set close target failed"): + client.set_close_target(graph_id, period="2099-12-31") + + @patch("robosystems_client.extensions.ledger_client.close_fiscal_period") + def test_close_period(self, mock_close, mock_config, graph_id): + """Test closing a fiscal period.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.OK + mock_resp.parsed = Mock(closed_through="2025-03-31", entries_posted=12) + mock_close.return_value = mock_resp + + client = LedgerClient(mock_config) + result = client.close_period( + graph_id, + period="2025-03-31", + note="Q1 close", + allow_stale_sync=True, + ) + + assert result.entries_posted == 12 + mock_close.assert_called_once() + call_kwargs = mock_close.call_args[1] + assert call_kwargs["period"] == "2025-03-31" + body = call_kwargs["body"] + assert body.note == "Q1 close" + assert body.allow_stale_sync is True + + @patch("robosystems_client.extensions.ledger_client.close_fiscal_period") + def test_close_period_error(self, mock_close, mock_config, graph_id): + """Test close period raises when gates fail.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.CONFLICT + mock_close.return_value = mock_resp + + client = LedgerClient(mock_config) + + with pytest.raises(RuntimeError, match="Close period failed"): + client.close_period(graph_id, period="2025-03-31") + + @patch("robosystems_client.extensions.ledger_client.reopen_fiscal_period") + def test_reopen_period(self, mock_reopen, mock_config, graph_id): + """Test reopening a closed period with a required reason.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.OK + mock_resp.parsed = Mock(status="closing") + mock_reopen.return_value = mock_resp + + client = LedgerClient(mock_config) + result = client.reopen_period( + graph_id, + period="2025-03-31", + reason="Audit adjustment needed", + ) + + assert result.status == "closing" + call_kwargs = mock_reopen.call_args[1] + assert call_kwargs["period"] == "2025-03-31" + body = call_kwargs["body"] + assert body.reason == "Audit adjustment needed" + + @patch("robosystems_client.extensions.ledger_client.reopen_fiscal_period") + def test_reopen_period_error(self, mock_reopen, mock_config, graph_id): + """Test reopen period raises on error.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.FORBIDDEN + mock_reopen.return_value = mock_resp + + client = LedgerClient(mock_config) + + with pytest.raises(RuntimeError, match="Reopen period failed"): + client.reopen_period(graph_id, period="2025-03-31", reason="test") + + @patch("robosystems_client.extensions.ledger_client.list_period_drafts") + def test_list_period_drafts(self, mock_list, mock_config, graph_id): + """Test listing drafts in a period.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.OK + mock_resp.parsed = Mock(drafts=[Mock(), Mock(), Mock()]) + mock_list.return_value = mock_resp + + client = LedgerClient(mock_config) + result = client.list_period_drafts(graph_id, period="2025-03-31") + + assert len(result.drafts) == 3 + call_kwargs = mock_list.call_args[1] + assert call_kwargs["period"] == "2025-03-31" + + @patch("robosystems_client.extensions.ledger_client.list_period_drafts") + def test_list_period_drafts_error(self, mock_list, mock_config, graph_id): + """Test list period drafts raises on error.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.NOT_FOUND + mock_list.return_value = mock_resp + + client = LedgerClient(mock_config) + + with pytest.raises(RuntimeError, match="List period drafts failed"): + client.list_period_drafts(graph_id, period="2025-03-31") + + +@pytest.mark.unit +class TestLedgerScheduleMutations: + """Test suite for schedule mutation and manual closing entries.""" + + @patch("robosystems_client.extensions.ledger_client.truncate_schedule") + def test_truncate_schedule(self, mock_trunc, mock_config, graph_id): + """Test truncating a schedule — string date should be converted.""" + import datetime + + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.OK + mock_resp.parsed = Mock(deleted_fact_count=3, deleted_draft_count=1) + mock_trunc.return_value = mock_resp + + client = LedgerClient(mock_config) + result = client.truncate_schedule( + graph_id, + structure_id="struct-1", + new_end_date="2025-06-30", + reason="Contract ended early", + ) + + assert result.deleted_fact_count == 3 + call_kwargs = mock_trunc.call_args[1] + assert call_kwargs["structure_id"] == "struct-1" + body = call_kwargs["body"] + # Critical: the wrapper converts string → date + assert body.new_end_date == datetime.date(2025, 6, 30) + assert body.reason == "Contract ended early" + + @patch("robosystems_client.extensions.ledger_client.truncate_schedule") + def test_truncate_schedule_invalid_date_string( + self, mock_trunc, mock_config, graph_id + ): + """Invalid ISO date should raise from fromisoformat before the HTTP call.""" + client = LedgerClient(mock_config) + + with pytest.raises(ValueError): + client.truncate_schedule( + graph_id, + structure_id="struct-1", + new_end_date="not-a-date", + reason="test", + ) + mock_trunc.assert_not_called() + + @patch("robosystems_client.extensions.ledger_client.truncate_schedule") + def test_truncate_schedule_error(self, mock_trunc, mock_config, graph_id): + """Test truncate schedule raises on error response.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.BAD_REQUEST + mock_trunc.return_value = mock_resp + + client = LedgerClient(mock_config) + + with pytest.raises(RuntimeError, match="Truncate schedule failed"): + client.truncate_schedule( + graph_id, + structure_id="struct-1", + new_end_date="2025-06-30", + reason="test", + ) + + @patch("robosystems_client.extensions.ledger_client.create_manual_closing_entry") + def test_create_manual_closing_entry(self, mock_create, mock_config, graph_id): + """Test creating a manual closing entry — full field transform.""" + import datetime + from robosystems_client.models.create_manual_closing_entry_request_entry_type import ( + CreateManualClosingEntryRequestEntryType, + ) + + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.CREATED + mock_resp.parsed = Mock(entry_id="entry-1") + mock_create.return_value = mock_resp + + client = LedgerClient(mock_config) + result = client.create_manual_closing_entry( + graph_id, + posting_date="2025-03-31", + memo="Disposal of asset", + entry_type="adjusting", + line_items=[ + { + "element_id": "elem-a", + "debit_amount": 10000, + "credit_amount": 0, + "description": "Loss on disposal", + }, + { + "element_id": "elem-b", + "debit_amount": 0, + "credit_amount": 10000, + "description": "Asset removal", + }, + ], + ) + + assert result.entry_id == "entry-1" + body = mock_create.call_args[1]["body"] + assert body.posting_date == datetime.date(2025, 3, 31) + assert body.memo == "Disposal of asset" + assert body.entry_type == CreateManualClosingEntryRequestEntryType.ADJUSTING + assert len(body.line_items) == 2 + assert body.line_items[0].element_id == "elem-a" + assert body.line_items[0].debit_amount == 10000 + assert body.line_items[0].credit_amount == 0 + assert body.line_items[0].description == "Loss on disposal" + assert body.line_items[1].credit_amount == 10000 + + @patch("robosystems_client.extensions.ledger_client.create_manual_closing_entry") + def test_create_manual_closing_entry_omits_optional_fields( + self, mock_create, mock_config, graph_id + ): + """Line items without description and no entry_type should produce UNSET.""" + from robosystems_client.types import UNSET + + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.CREATED + mock_resp.parsed = Mock() + mock_create.return_value = mock_resp + + client = LedgerClient(mock_config) + client.create_manual_closing_entry( + graph_id, + posting_date="2025-03-31", + memo="Adjustment", + line_items=[ + {"element_id": "elem-a", "debit_amount": 500, "credit_amount": 0}, + {"element_id": "elem-b", "debit_amount": 0, "credit_amount": 500}, + ], + ) + + body = mock_create.call_args[1]["body"] + assert body.entry_type is UNSET + assert body.line_items[0].description is UNSET + assert body.line_items[1].description is UNSET + + @patch("robosystems_client.extensions.ledger_client.create_manual_closing_entry") + def test_create_manual_closing_entry_defaults_missing_amounts( + self, mock_create, mock_config, graph_id + ): + """Line items missing debit/credit should default to 0.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.CREATED + mock_resp.parsed = Mock() + mock_create.return_value = mock_resp + + client = LedgerClient(mock_config) + client.create_manual_closing_entry( + graph_id, + posting_date="2025-03-31", + memo="Sparse items", + line_items=[ + {"element_id": "elem-a", "debit_amount": 100}, + {"element_id": "elem-b", "credit_amount": 100}, + ], + ) + + body = mock_create.call_args[1]["body"] + assert body.line_items[0].debit_amount == 100 + assert body.line_items[0].credit_amount == 0 + assert body.line_items[1].debit_amount == 0 + assert body.line_items[1].credit_amount == 100 + + @patch("robosystems_client.extensions.ledger_client.create_manual_closing_entry") + def test_create_manual_closing_entry_200_is_error( + self, mock_create, mock_config, graph_id + ): + """A 200 (instead of 201) should raise — success is specifically CREATED.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.OK + mock_create.return_value = mock_resp + + client = LedgerClient(mock_config) + + with pytest.raises(RuntimeError, match="Create manual closing entry failed"): + client.create_manual_closing_entry( + graph_id, + posting_date="2025-03-31", + memo="test", + line_items=[{"element_id": "elem-a", "debit_amount": 1, "credit_amount": 1}], + ) + + @patch("robosystems_client.extensions.ledger_client.create_manual_closing_entry") + def test_create_manual_closing_entry_rejects_closed_period( + self, mock_create, mock_config, graph_id + ): + """409 (closed period) should raise.""" + mock_resp = Mock() + mock_resp.status_code = HTTPStatus.CONFLICT + mock_create.return_value = mock_resp + + client = LedgerClient(mock_config) + + with pytest.raises(RuntimeError, match="Create manual closing entry failed"): + client.create_manual_closing_entry( + graph_id, + posting_date="2025-03-31", + memo="test", + line_items=[{"element_id": "elem-a", "debit_amount": 1, "credit_amount": 1}], + )