diff --git a/robosystems_client/clients/ledger_client.py b/robosystems_client/clients/ledger_client.py index 21817fe..99359ef 100644 --- a/robosystems_client/clients/ledger_client.py +++ b/robosystems_client/clients/ledger_client.py @@ -351,6 +351,12 @@ ListLedgerReports, ListLedgerReportsReportsReports, ) +from ..graphql.generated.list_ledger_journal_entries import ( + ListLedgerJournalEntries, +) +from ..graphql.generated.list_ledger_journal_entries import ( + ListLedgerJournalEntriesJournalEntries as LedgerJournalEntriesPage, +) from ..graphql.generated.list_ledger_structures import ( ListLedgerStructures, ListLedgerStructuresStructuresStructures, @@ -403,6 +409,7 @@ LIST_LEDGER_ELEMENTS_GQL, LIST_LEDGER_ENTITIES_GQL, LIST_LEDGER_EVENT_BLOCKS_GQL, + LIST_LEDGER_JOURNAL_ENTRIES_GQL, LIST_LEDGER_MAPPINGS_GQL, LIST_LEDGER_PUBLISH_LISTS_GQL, LIST_LEDGER_REPORTS_GQL, @@ -871,6 +878,49 @@ def get_transaction( ) return GetLedgerTransaction.model_validate(data).transaction + # ── Journal entries ───────────────────────────────────────────────── + + def list_journal_entries( + self, + graph_id: str, + start_date: str | None = None, + end_date: str | None = None, + status: str | None = None, + type: str | None = None, # noqa: A002 — matches backend arg name + provenance: str | None = None, + transaction_id: str | None = None, + limit: int = 100, + offset: int = 0, + ) -> LedgerJournalEntriesPage | None: + """List journal entries with their line items, newest first. + + The entry-centric read. Unlike :meth:`list_transactions`, which walks + transactions and hangs entries off them, this returns an entry + whether or not it has a parent transaction — and entries created by + the schedule engine and the event handlers have none, so they appear + in no transaction-based listing. ``transaction_id`` on each result is + ``None`` for such a standalone entry rather than absent data. + + Filter by ``provenance`` (``schedule_derived`` for what a period + close posted), ``type`` (``adjusting`` / ``closing``), ``status``, or + a parent ``transaction_id``. + """ + data = self._query( + graph_id, + LIST_LEDGER_JOURNAL_ENTRIES_GQL, + { + "startDate": start_date, + "endDate": end_date, + "status": status, + "type": type, + "provenance": provenance, + "transactionId": transaction_id, + "limit": limit, + "offset": offset, + }, + ) + return ListLedgerJournalEntries.model_validate(data).journal_entries + # ── Event blocks (inbox surface) ─────────────────────────────────── def list_event_blocks( diff --git a/robosystems_client/graphql/generated/__init__.py b/robosystems_client/graphql/generated/__init__.py index c71471b..0930fea 100644 --- a/robosystems_client/graphql/generated/__init__.py +++ b/robosystems_client/graphql/generated/__init__.py @@ -250,6 +250,13 @@ ListLedgerEventBlocks, ListLedgerEventBlocksEventBlocks, ) +from .list_ledger_journal_entries import ( + ListLedgerJournalEntries, + ListLedgerJournalEntriesJournalEntries, + ListLedgerJournalEntriesJournalEntriesEntries, + ListLedgerJournalEntriesJournalEntriesEntriesLineItems, + ListLedgerJournalEntriesJournalEntriesPagination, +) from .list_ledger_mappings import ( ListLedgerMappings, ListLedgerMappingsMappings, @@ -350,6 +357,7 @@ LIST_LEDGER_ELEMENTS_GQL, LIST_LEDGER_ENTITIES_GQL, LIST_LEDGER_EVENT_BLOCKS_GQL, + LIST_LEDGER_JOURNAL_ENTRIES_GQL, LIST_LEDGER_MAPPINGS_GQL, LIST_LEDGER_PUBLISH_LISTS_GQL, LIST_LEDGER_REPORTS_GQL, @@ -558,6 +566,7 @@ "LIST_LEDGER_ELEMENTS_GQL", "LIST_LEDGER_ENTITIES_GQL", "LIST_LEDGER_EVENT_BLOCKS_GQL", + "LIST_LEDGER_JOURNAL_ENTRIES_GQL", "LIST_LEDGER_MAPPINGS_GQL", "LIST_LEDGER_PUBLISH_LISTS_GQL", "LIST_LEDGER_REPORTS_GQL", @@ -618,6 +627,11 @@ "ListLedgerEntitiesEntities", "ListLedgerEventBlocks", "ListLedgerEventBlocksEventBlocks", + "ListLedgerJournalEntries", + "ListLedgerJournalEntriesJournalEntries", + "ListLedgerJournalEntriesJournalEntriesEntries", + "ListLedgerJournalEntriesJournalEntriesEntriesLineItems", + "ListLedgerJournalEntriesJournalEntriesPagination", "ListLedgerMappings", "ListLedgerMappingsMappings", "ListLedgerMappingsMappingsStructures", diff --git a/robosystems_client/graphql/generated/client.py b/robosystems_client/graphql/generated/client.py index 1251576..7970dc2 100644 --- a/robosystems_client/graphql/generated/client.py +++ b/robosystems_client/graphql/generated/client.py @@ -44,6 +44,7 @@ from .list_ledger_elements import ListLedgerElements from .list_ledger_entities import ListLedgerEntities from .list_ledger_event_blocks import ListLedgerEventBlocks +from .list_ledger_journal_entries import ListLedgerJournalEntries from .list_ledger_mappings import ListLedgerMappings from .list_ledger_publish_lists import ListLedgerPublishLists from .list_ledger_reports import ListLedgerReports @@ -98,6 +99,7 @@ LIST_LEDGER_ELEMENTS_GQL, LIST_LEDGER_ENTITIES_GQL, LIST_LEDGER_EVENT_BLOCKS_GQL, + LIST_LEDGER_JOURNAL_ENTRIES_GQL, LIST_LEDGER_MAPPINGS_GQL, LIST_LEDGER_PUBLISH_LISTS_GQL, LIST_LEDGER_REPORTS_GQL, @@ -698,6 +700,37 @@ def list_ledger_event_blocks( data = self.get_data(response) return ListLedgerEventBlocks.model_validate(data) + def list_ledger_journal_entries( + self, + limit: int, + offset: int, + start_date: Union[Optional[str], UnsetType] = UNSET, + end_date: Union[Optional[str], UnsetType] = UNSET, + status: Union[Optional[str], UnsetType] = UNSET, + type_: Union[Optional[str], UnsetType] = UNSET, + provenance: Union[Optional[str], UnsetType] = UNSET, + transaction_id: Union[Optional[str], UnsetType] = UNSET, + **kwargs: Any, + ) -> ListLedgerJournalEntries: + variables: dict[str, object] = { + "startDate": start_date, + "endDate": end_date, + "status": status, + "type": type_, + "provenance": provenance, + "transactionId": transaction_id, + "limit": limit, + "offset": offset, + } + response = self.execute( + query=LIST_LEDGER_JOURNAL_ENTRIES_GQL, + operation_name="ListLedgerJournalEntries", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return ListLedgerJournalEntries.model_validate(data) + def list_ledger_mappings(self, **kwargs: Any) -> ListLedgerMappings: variables: dict[str, object] = {} response = self.execute( diff --git a/robosystems_client/graphql/generated/list_ledger_journal_entries.py b/robosystems_client/graphql/generated/list_ledger_journal_entries.py new file mode 100644 index 0000000..a3ea05d --- /dev/null +++ b/robosystems_client/graphql/generated/list_ledger_journal_entries.py @@ -0,0 +1,61 @@ +from typing import Optional + +from pydantic import Field + +from .base_model import BaseModel + + +class ListLedgerJournalEntries(BaseModel): + journal_entries: Optional["ListLedgerJournalEntriesJournalEntries"] = Field( + alias="journalEntries" + ) + + +class ListLedgerJournalEntriesJournalEntries(BaseModel): + entries: list["ListLedgerJournalEntriesJournalEntriesEntries"] + pagination: "ListLedgerJournalEntriesJournalEntriesPagination" + + +class ListLedgerJournalEntriesJournalEntriesEntries(BaseModel): + id: str + number: Optional[str] + transaction_id: Optional[str] = Field(alias="transactionId") + type_: str = Field(alias="type") + status: str + posting_date: str = Field(alias="postingDate") + memo: Optional[str] + provenance: Optional[str] + source_structure_id: Optional[str] = Field(alias="sourceStructureId") + source_structure_name: Optional[str] = Field(alias="sourceStructureName") + triggered_by_event_id: Optional[str] = Field(alias="triggeredByEventId") + reversal_of: Optional[str] = Field(alias="reversalOf") + posted_at: Optional[str] = Field(alias="postedAt") + total_debit: float = Field(alias="totalDebit") + total_credit: float = Field(alias="totalCredit") + balanced: bool + line_items: list["ListLedgerJournalEntriesJournalEntriesEntriesLineItems"] = Field( + alias="lineItems" + ) + + +class ListLedgerJournalEntriesJournalEntriesEntriesLineItems(BaseModel): + id: str + account_id: str = Field(alias="accountId") + account_name: Optional[str] = Field(alias="accountName") + account_code: Optional[str] = Field(alias="accountCode") + debit_amount: float = Field(alias="debitAmount") + credit_amount: float = Field(alias="creditAmount") + description: Optional[str] + line_order: int = Field(alias="lineOrder") + + +class ListLedgerJournalEntriesJournalEntriesPagination(BaseModel): + total: int + limit: int + offset: int + has_more: bool = Field(alias="hasMore") + + +ListLedgerJournalEntries.model_rebuild() +ListLedgerJournalEntriesJournalEntries.model_rebuild() +ListLedgerJournalEntriesJournalEntriesEntries.model_rebuild() diff --git a/robosystems_client/graphql/generated/operations.py b/robosystems_client/graphql/generated/operations.py index 2e83b0d..378d0cf 100644 --- a/robosystems_client/graphql/generated/operations.py +++ b/robosystems_client/graphql/generated/operations.py @@ -40,6 +40,7 @@ "LIST_LEDGER_ELEMENTS_GQL", "LIST_LEDGER_ENTITIES_GQL", "LIST_LEDGER_EVENT_BLOCKS_GQL", + "LIST_LEDGER_JOURNAL_ENTRIES_GQL", "LIST_LEDGER_MAPPINGS_GQL", "LIST_LEDGER_PUBLISH_LISTS_GQL", "LIST_LEDGER_REPORTS_GQL", @@ -1491,6 +1492,56 @@ } """ +LIST_LEDGER_JOURNAL_ENTRIES_GQL = """ +query ListLedgerJournalEntries($startDate: Date, $endDate: Date, $status: String, $type: String, $provenance: String, $transactionId: String, $limit: Int! = 100, $offset: Int! = 0) { + journalEntries( + startDate: $startDate + endDate: $endDate + status: $status + type: $type + provenance: $provenance + transactionId: $transactionId + limit: $limit + offset: $offset + ) { + entries { + id + number + transactionId + type + status + postingDate + memo + provenance + sourceStructureId + sourceStructureName + triggeredByEventId + reversalOf + postedAt + totalDebit + totalCredit + balanced + lineItems { + id + accountId + accountName + accountCode + debitAmount + creditAmount + description + lineOrder + } + } + pagination { + total + limit + offset + hasMore + } + } +} +""" + LIST_LEDGER_MAPPINGS_GQL = """ query ListLedgerMappings { mappings { diff --git a/robosystems_client/graphql/operations/ledger/ListLedgerJournalEntries.graphql b/robosystems_client/graphql/operations/ledger/ListLedgerJournalEntries.graphql new file mode 100644 index 0000000..0504f45 --- /dev/null +++ b/robosystems_client/graphql/operations/ledger/ListLedgerJournalEntries.graphql @@ -0,0 +1,33 @@ +query ListLedgerJournalEntries( + $startDate: Date + $endDate: Date + $status: String + $type: String + $provenance: String + $transactionId: String + $limit: Int! = 100 + $offset: Int! = 0 +) { + journalEntries( + startDate: $startDate + endDate: $endDate + status: $status + type: $type + provenance: $provenance + transactionId: $transactionId + limit: $limit + offset: $offset + ) { + entries { + id number transactionId type status postingDate memo + provenance sourceStructureId sourceStructureName + triggeredByEventId reversalOf postedAt + totalDebit totalCredit balanced + lineItems { + id accountId accountName accountCode + debitAmount creditAmount description lineOrder + } + } + pagination { total limit offset hasMore } + } +} diff --git a/robosystems_client/graphql/schema.graphql b/robosystems_client/graphql/schema.graphql index f29d262..8124a42 100644 --- a/robosystems_client/graphql/schema.graphql +++ b/robosystems_client/graphql/schema.graphql @@ -23,6 +23,7 @@ type Query { trialBalance(startDate: Date = null, endDate: Date = null): TrialBalance transactions(type: String = null, startDate: Date = null, endDate: Date = null, limit: Int = null, offset: Int = null): LedgerTransactionList transaction(transactionId: String!): LedgerTransactionDetail + journalEntries(startDate: Date = null, endDate: Date = null, status: String = null, type: String = null, provenance: String = null, transactionId: String = null, limit: Int = null, offset: Int = null): LedgerJournalEntryList taxonomies(taxonomyType: String = null): TaxonomyList reportingTaxonomy: Taxonomy elements(taxonomyId: String = null, source: String = null, classification: String = null, isAbstract: Boolean = null, limit: Int = null, offset: Int = null): ElementList @@ -874,6 +875,70 @@ type LedgerLineItem { lineOrder: Int! } +""" +Paginated journal listing — entries with their line items expanded. + +Pagination counts *entries*, not line-item rows. +""" +type LedgerJournalEntryList { + entries: [LedgerJournalEntry!]! + pagination: PaginationInfo! +} + +""" +A posted or draft journal entry, read on its own terms. + +Distinct from `LedgerEntryResponse`, which is an entry *underneath* a +transaction in the detail view. An entry does not need a parent: +`Entry.transaction_id` is nullable by design (see the model comment on +`Entry.triggered_by_event_id`), and the schedule engine and event +handlers create entries with no transaction at all. Those entries are +invisible to the transaction-centric reads, which is what this shape +exists to fix — so `transaction_id` is projected explicitly, and a +`None` here means a standalone entry rather than missing data. +""" +type LedgerJournalEntry { + id: String! + number: String + + """ + Parent transaction, or null for a standalone entry (schedule-derived closing entries and event-handler entries have no parent) + """ + transactionId: String + + """'standard' | 'adjusting' | 'closing' | 'reversing'""" + type: String! + + """'draft' | 'posted' | 'reversed'""" + status: String! + postingDate: Date! + memo: String + + """ + Where the entry came from (ENTRY_PROVENANCE_VALUES): source_sync, ai_generated, manual_entry, schedule_derived, system_computed, event_handler + """ + provenance: String + + """Schedule structure that generated this entry (if any)""" + sourceStructureId: String + + """Human-readable name of the source schedule""" + sourceStructureName: String + + """Business event that caused this entry (if any)""" + triggeredByEventId: String + + """The entry this one reverses (if any)""" + reversalOf: String + postedAt: DateTime + lineItems: [LedgerLineItem!]! + totalDebit: Float! + totalCredit: Float! + + """True if total_debit == total_credit""" + balanced: Boolean! +} + """Flat list of taxonomy headers. Used by the catalog/picker UIs.""" type TaxonomyList { taxonomies: [Taxonomy!]! diff --git a/robosystems_client/models/document_section.py b/robosystems_client/models/document_section.py index 7e933d7..cae9e60 100644 --- a/robosystems_client/models/document_section.py +++ b/robosystems_client/models/document_section.py @@ -13,36 +13,45 @@ @_attrs_define class DocumentSection: - """Full document section retrieved by ID. - - Attributes: - document_id (str): - graph_id (str): - source_type (str): - content (str): - entity_ticker (None | str | Unset): - entity_name (None | str | Unset): - entity_cik (None | str | Unset): - section_label (None | str | Unset): - section_id (None | str | Unset): - element_qname (None | str | Unset): - filing_date (None | str | Unset): - fiscal_year (int | None | Unset): - fiscal_period (None | str | Unset): - form_type (None | str | Unset): - accession_number (None | str | Unset): - xbrl_elements (list[str] | None | Unset): - content_url (None | str | Unset): - content_length (int | Unset): Default: 0. - document_title (None | str | Unset): - tags (list[str] | None | Unset): - folder (None | str | Unset): + """Full document section retrieved by ID — one part of it when the section + is long; ``next_document_id`` continues it. + + Attributes: + document_id (str): + graph_id (str): + source_type (str): + content (str): + parent_document_id (None | str | Unset): + part (int | Unset): Default: 1. + part_count (int | Unset): Default: 1. + next_document_id (None | str | Unset): + entity_ticker (None | str | Unset): + entity_name (None | str | Unset): + entity_cik (None | str | Unset): + section_label (None | str | Unset): + section_id (None | str | Unset): + element_qname (None | str | Unset): + filing_date (None | str | Unset): + fiscal_year (int | None | Unset): + fiscal_period (None | str | Unset): + form_type (None | str | Unset): + accession_number (None | str | Unset): + xbrl_elements (list[str] | None | Unset): + content_url (None | str | Unset): + content_length (int | Unset): Default: 0. + document_title (None | str | Unset): + tags (list[str] | None | Unset): + folder (None | str | Unset): """ document_id: str graph_id: str source_type: str content: str + parent_document_id: None | str | Unset = UNSET + part: int | Unset = 1 + part_count: int | Unset = 1 + next_document_id: None | str | Unset = UNSET entity_ticker: None | str | Unset = UNSET entity_name: None | str | Unset = UNSET entity_cik: None | str | Unset = UNSET @@ -71,6 +80,22 @@ def to_dict(self) -> dict[str, Any]: content = self.content + parent_document_id: None | str | Unset + if isinstance(self.parent_document_id, Unset): + parent_document_id = UNSET + else: + parent_document_id = self.parent_document_id + + part = self.part + + part_count = self.part_count + + next_document_id: None | str | Unset + if isinstance(self.next_document_id, Unset): + next_document_id = UNSET + else: + next_document_id = self.next_document_id + entity_ticker: None | str | Unset if isinstance(self.entity_ticker, Unset): entity_ticker = UNSET @@ -185,6 +210,14 @@ def to_dict(self) -> dict[str, Any]: "content": content, } ) + if parent_document_id is not UNSET: + field_dict["parent_document_id"] = parent_document_id + if part is not UNSET: + field_dict["part"] = part + if part_count is not UNSET: + field_dict["part_count"] = part_count + if next_document_id is not UNSET: + field_dict["next_document_id"] = next_document_id if entity_ticker is not UNSET: field_dict["entity_ticker"] = entity_ticker if entity_name is not UNSET: @@ -233,6 +266,28 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: content = d.pop("content") + def _parse_parent_document_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + parent_document_id = _parse_parent_document_id(d.pop("parent_document_id", UNSET)) + + part = d.pop("part", UNSET) + + part_count = d.pop("part_count", UNSET) + + def _parse_next_document_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + next_document_id = _parse_next_document_id(d.pop("next_document_id", UNSET)) + def _parse_entity_ticker(data: object) -> None | str | Unset: if data is None: return data @@ -400,6 +455,10 @@ def _parse_folder(data: object) -> None | str | Unset: graph_id=graph_id, source_type=source_type, content=content, + parent_document_id=parent_document_id, + part=part, + part_count=part_count, + next_document_id=next_document_id, entity_ticker=entity_ticker, entity_name=entity_name, entity_cik=entity_cik, diff --git a/robosystems_client/models/search_hit.py b/robosystems_client/models/search_hit.py index 4776ffc..c89e1a7 100644 --- a/robosystems_client/models/search_hit.py +++ b/robosystems_client/models/search_hit.py @@ -15,26 +15,33 @@ class SearchHit: """A single search result with snippet. - Attributes: - document_id (str): - score (float): - source_type (str): - snippet (str): - parent_document_id (None | str | Unset): - entity_ticker (None | str | Unset): - entity_name (None | str | Unset): - section_label (None | str | Unset): - section_id (None | str | Unset): - element_qname (None | str | Unset): - filing_date (None | str | Unset): - fiscal_year (int | None | Unset): - form_type (None | str | Unset): - xbrl_elements (list[str] | None | Unset): - content_length (int | Unset): Default: 0. - content_url (None | str | Unset): - document_title (None | str | Unset): - tags (list[str] | None | Unset): - folder (None | str | Unset): + A long SEC section (an MD&A, a commitments note) is indexed in parts, each + a document of its own: ``part`` of ``part_count``, ``parent_document_id`` + shared by the section's parts, ``next_document_id`` to read on. + + Attributes: + document_id (str): + score (float): + source_type (str): + snippet (str): + parent_document_id (None | str | Unset): + part (int | Unset): Default: 1. + part_count (int | Unset): Default: 1. + next_document_id (None | str | Unset): + entity_ticker (None | str | Unset): + entity_name (None | str | Unset): + section_label (None | str | Unset): + section_id (None | str | Unset): + element_qname (None | str | Unset): + filing_date (None | str | Unset): + fiscal_year (int | None | Unset): + form_type (None | str | Unset): + xbrl_elements (list[str] | None | Unset): + content_length (int | Unset): Default: 0. + content_url (None | str | Unset): + document_title (None | str | Unset): + tags (list[str] | None | Unset): + folder (None | str | Unset): """ document_id: str @@ -42,6 +49,9 @@ class SearchHit: source_type: str snippet: str parent_document_id: None | str | Unset = UNSET + part: int | Unset = 1 + part_count: int | Unset = 1 + next_document_id: None | str | Unset = UNSET entity_ticker: None | str | Unset = UNSET entity_name: None | str | Unset = UNSET section_label: None | str | Unset = UNSET @@ -73,6 +83,16 @@ def to_dict(self) -> dict[str, Any]: else: parent_document_id = self.parent_document_id + part = self.part + + part_count = self.part_count + + next_document_id: None | str | Unset + if isinstance(self.next_document_id, Unset): + next_document_id = UNSET + else: + next_document_id = self.next_document_id + entity_ticker: None | str | Unset if isinstance(self.entity_ticker, Unset): entity_ticker = UNSET @@ -171,6 +191,12 @@ def to_dict(self) -> dict[str, Any]: ) if parent_document_id is not UNSET: field_dict["parent_document_id"] = parent_document_id + if part is not UNSET: + field_dict["part"] = part + if part_count is not UNSET: + field_dict["part_count"] = part_count + if next_document_id is not UNSET: + field_dict["next_document_id"] = next_document_id if entity_ticker is not UNSET: field_dict["entity_ticker"] = entity_ticker if entity_name is not UNSET: @@ -222,6 +248,19 @@ def _parse_parent_document_id(data: object) -> None | str | Unset: parent_document_id = _parse_parent_document_id(d.pop("parent_document_id", UNSET)) + part = d.pop("part", UNSET) + + part_count = d.pop("part_count", UNSET) + + def _parse_next_document_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + next_document_id = _parse_next_document_id(d.pop("next_document_id", UNSET)) + def _parse_entity_ticker(data: object) -> None | str | Unset: if data is None: return data @@ -363,6 +402,9 @@ def _parse_folder(data: object) -> None | str | Unset: source_type=source_type, snippet=snippet, parent_document_id=parent_document_id, + part=part, + part_count=part_count, + next_document_id=next_document_id, entity_ticker=entity_ticker, entity_name=entity_name, section_label=section_label, diff --git a/robosystems_client/models/search_request.py b/robosystems_client/models/search_request.py index 66e469a..87a939d 100644 --- a/robosystems_client/models/search_request.py +++ b/robosystems_client/models/search_request.py @@ -19,7 +19,8 @@ class SearchRequest: query (str): Search query entity (None | str | Unset): Filter by ticker, CIK, or entity name form_type (None | str | Unset): Filter by SEC form type (10-K, 10-Q) - section (None | str | Unset): Filter by section ID (item_1, item_1a, item_7, etc.) + section (None | str | Unset): Filter by section ID: an Item (item_1, item_1a, item_7, ...) or, for iXBRL + disclosures, the element qname (us-gaap:GoodwillDisclosureTextBlock) element (None | str | Unset): Filter by XBRL element qname (e.g., us-gaap:Goodwill) source_type (None | str | Unset): Filter by source type (xbrl_textblock, narrative_section, ixbrl_disclosure, uploaded_doc, connection_doc) diff --git a/tests/test_ledger_client.py b/tests/test_ledger_client.py index 5cde524..9bf0501 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -809,6 +809,97 @@ def test_list_transactions(self, mock_execute, mock_config, graph_id): assert variables["type"] == "invoice" assert variables["startDate"] == "2026-01-01" + @patch("robosystems_client.graphql.client.GraphQLClient.execute") + def test_list_journal_entries_returns_standalone_entries( + self, mock_execute, mock_config, graph_id + ): + """An entry with no parent transaction round-trips through the facade. + + That shape is why the read exists: schedule-derived closing entries + carry no `transaction_id`, so they appear in no transaction listing. + """ + mock_execute.return_value = { + "journalEntries": { + "entries": [ + { + "id": "je_1", + "number": None, + "transactionId": None, + "type": "adjusting", + "status": "posted", + "postingDate": "2026-07-31", + "memo": "MacBook depreciation", + "provenance": "schedule_derived", + "sourceStructureId": "struct_1", + "sourceStructureName": "MacBook Pro Depreciation", + "triggeredByEventId": None, + "reversalOf": None, + "postedAt": "2026-08-01T12:00:00", + "totalDebit": 42.41, + "totalCredit": 42.41, + "balanced": True, + "lineItems": [ + { + "id": "li_1", + "accountId": "el_1", + "accountName": "Depreciation Expense", + "accountCode": "6100", + "debitAmount": 42.41, + "creditAmount": 0.0, + "description": None, + "lineOrder": 1, + } + ], + } + ], + "pagination": {"total": 1, "limit": 100, "offset": 0, "hasMore": False}, + } + } + client = LedgerClient(mock_config) + result = client.list_journal_entries( + graph_id, start_date="2026-07-01", end_date="2026-07-31", status="posted" + ) + assert result is not None + assert len(result.entries) == 1 + entry = result.entries[0] + assert entry.id == "je_1" + # None means standalone, not missing data. + assert entry.transaction_id is None + assert entry.source_structure_name == "MacBook Pro Depreciation" + assert entry.balanced is True + variables = mock_execute.call_args[0][2] + assert variables["startDate"] == "2026-07-01" + assert variables["status"] == "posted" + + @patch("robosystems_client.graphql.client.GraphQLClient.execute") + def test_list_journal_entries_binds_every_filter( + self, mock_execute, mock_config, graph_id + ): + mock_execute.return_value = { + "journalEntries": { + "entries": [], + "pagination": {"total": 0, "limit": 25, "offset": 50, "hasMore": False}, + } + } + client = LedgerClient(mock_config) + client.list_journal_entries( + graph_id, + start_date="2026-07-01", + end_date="2026-07-31", + status="posted", + type="closing", + provenance="schedule_derived", + transaction_id="txn_1", + limit=25, + offset=50, + ) + variables = mock_execute.call_args[0][2] + assert variables["type"] == "closing" + assert variables["provenance"] == "schedule_derived" + assert variables["transactionId"] == "txn_1" + assert variables["limit"] == 25 + assert variables["offset"] == 50 + @patch("robosystems_client.graphql.client.GraphQLClient.execute") def test_get_transaction(self, mock_execute, mock_config, graph_id): mock_execute.return_value = {