diff --git a/robosystems_client/clients/ledger_client.py b/robosystems_client/clients/ledger_client.py index 117cc17..1ab8137 100644 --- a/robosystems_client/clients/ledger_client.py +++ b/robosystems_client/clients/ledger_client.py @@ -1358,13 +1358,17 @@ def list_structures( page = ListLedgerStructures.model_validate(data).structures return page.structures if page else [] - def list_mappings(self, graph_id: str) -> list[ListLedgerMappingsMappingsStructures]: + def list_mappings( + self, graph_id: str, *, entity_id: str | None = None + ) -> list[ListLedgerMappingsMappingsStructures]: """List active CoA→reporting mapping structures, the book mapping first. Each carries ``framework``: the reporting framework it maps the chart - into. A chart holds one mapping per framework. + into (a chart holds one mapping per framework), and ``entity_id``: the + entity whose chart it maps from, so whose reports it can produce. + ``entity_id`` keeps that entity's mappings only. """ - data = self._query(graph_id, LIST_LEDGER_MAPPINGS_GQL) + data = self._query(graph_id, LIST_LEDGER_MAPPINGS_GQL, {"entityId": entity_id}) page = ListLedgerMappings.model_validate(data).mappings return page.structures if page else [] @@ -1383,16 +1387,19 @@ def get_mapping_coverage( return GetLedgerMappingCoverage.model_validate(data).mapping_coverage def get_mapping_candidates( - self, graph_id: str, classification: str + self, graph_id: str, classification: str, *, entity_id: str | None = None ) -> list[MappingCandidatesMappingCandidates]: """rs-gaap concepts a CoA element of the given EFS ``classification`` (asset / liability / equity / revenue / expense) may map to — limited - to concepts that render under the graph's active Reporting Style, - with statement-level subtotals excluded. Use this to populate the - mapping picker so it never offers an unreachable target. + to concepts that render under the Reporting Style of the entity whose + chart is being mapped (``entity_id``, default the group parent), with + statement-level subtotals excluded. Use this to populate the mapping + picker so it never offers an unreachable target. """ data = self._query( - graph_id, MAPPING_CANDIDATES_GQL, {"classification": classification} + graph_id, + MAPPING_CANDIDATES_GQL, + {"classification": classification, "entityId": entity_id}, ) return MappingCandidates.model_validate(data).mapping_candidates @@ -1454,6 +1461,7 @@ def get_information_block( series: bool | None = None, series_history: int | None = None, series_forecast: int | None = None, + entity_id: str | None = None, ) -> InformationBlock | None: """Fetch an Information Block envelope by id — the cross-block-type read. @@ -1474,6 +1482,12 @@ def get_information_block( ``series_history`` / ``series_forecast`` window the series to its seam-adjacent columns — the last N actual columns and the first N forecast columns; ``None`` = unbounded. + + ``entity_id`` picks whose books a block shared by the group reads + (statements, metrics, disclosures); ``None`` = the scenario's entity, + else the group parent. A schedule, reconciliation or forecast always + reads its own entity's. The envelope's ``entity_id`` names the entity + it read. """ data = self._query( graph_id, @@ -1484,6 +1498,7 @@ def get_information_block( "series": series, "seriesHistory": series_history, "seriesForecast": series_forecast, + "entityId": entity_id, }, ) return GetInformationBlock.model_validate(data).information_block @@ -1497,6 +1512,7 @@ def list_information_blocks( limit: int | None = None, offset: int | None = None, scenario_id: str | None = None, + entity_id: str | None = None, ) -> list[ListInformationBlocksInformationBlocks]: """List Information Block envelopes, optionally filtered. @@ -1504,6 +1520,11 @@ def list_information_blocks( ``block_type='schedule'`` for the same set of blocks. ``scenario_id`` (a forecast block's structure id) threads into each envelope's FactSet binding. + + ``entity_id`` (``None`` = the scenario's entity, else the group + parent) lists the blocks shared by the group plus that entity's own + schedules, reconciliations and forecasts, never another entity's; + shared blocks read its books. """ data = self._query( graph_id, @@ -1514,6 +1535,7 @@ def list_information_blocks( "limit": limit, "offset": offset, "scenarioId": scenario_id, + "entityId": entity_id, }, ) return ListInformationBlocks.model_validate(data).information_blocks @@ -1604,12 +1626,16 @@ def create_schedule( asset_element_id: str | None = None, auto_reverse: bool = False, booked_on: str | None = None, + entity_id: str | None = None, ) -> InformationBlockEnvelope: """Create a new schedule with pre-generated monthly facts. ``booked_on`` (``YYYY-MM-DD``) is the day the schedule's cost went on the books, when that is before its first period. The schedule reconciliation carries the cost from that day. + + ``entity_id`` is the entity whose books the schedule belongs to; + ``None`` = the group parent. """ payload_dict: dict[str, Any] = { "name": name, @@ -1627,6 +1653,8 @@ def create_schedule( } if taxonomy_id: payload_dict["taxonomy_id"] = taxonomy_id + if entity_id: + payload_dict["entity_id"] = entity_id schedule_metadata: dict[str, Any] = {} if method: schedule_metadata["method"] = method @@ -2636,6 +2664,7 @@ def create_report( period_type: str = "quarterly", comparative: bool = True, periods: list[PeriodSpec | dict[str, Any]] | None = None, + entity_id: str | None = None, ) -> ReportResponse: """Generate report facts from the ledger and publish a Report definition. Synchronous — returns the published report header. @@ -2647,6 +2676,10 @@ def create_report( ``{"start", "end", "label"}`` dicts — for multi-period layouts (YTD-by-quarter, multi-year). When given, the server ignores ``period_start`` / ``period_end`` / ``comparative``. + + ``entity_id`` is the entity the report is for; ``None`` = the entity + whose chart ``mapping_id`` maps from. Named alongside another entity's + mapping, the server refuses it. """ body = CreateReportRequest( name=name, @@ -2661,6 +2694,8 @@ def create_report( body.periods = [ p if isinstance(p, PeriodSpec) else PeriodSpec.from_dict(p) for p in periods ] + if entity_id: + body.entity_id = entity_id response = op_create_report(graph_id=graph_id, body=body, client=self._get_client()) envelope = self._call_op("Create report", response) return self._typed_result("Create report", envelope, ReportResponse) diff --git a/robosystems_client/graphql/generated/client.py b/robosystems_client/graphql/generated/client.py index 0c2ae17..af4085b 100644 --- a/robosystems_client/graphql/generated/client.py +++ b/robosystems_client/graphql/generated/client.py @@ -250,6 +250,7 @@ def get_information_block( scenario_id: Union[Optional[str], UnsetType] = UNSET, series_history: Union[Optional[int], UnsetType] = UNSET, series_forecast: Union[Optional[int], UnsetType] = UNSET, + entity_id: Union[Optional[str], UnsetType] = UNSET, **kwargs: Any, ) -> GetInformationBlock: variables: dict[str, object] = { @@ -258,6 +259,7 @@ def get_information_block( "series": series, "seriesHistory": series_history, "seriesForecast": series_forecast, + "entityId": entity_id, } response = self.execute( query=GET_INFORMATION_BLOCK_GQL, @@ -591,6 +593,7 @@ def list_information_blocks( limit: Union[Optional[int], UnsetType] = UNSET, offset: Union[Optional[int], UnsetType] = UNSET, scenario_id: Union[Optional[str], UnsetType] = UNSET, + entity_id: Union[Optional[str], UnsetType] = UNSET, **kwargs: Any, ) -> ListInformationBlocks: variables: dict[str, object] = { @@ -599,6 +602,7 @@ def list_information_blocks( "limit": limit, "offset": offset, "scenarioId": scenario_id, + "entityId": entity_id, } response = self.execute( query=LIST_INFORMATION_BLOCKS_GQL, @@ -791,8 +795,10 @@ def list_ledger_journal_entries( data = self.get_data(response) return ListLedgerJournalEntries.model_validate(data) - def list_ledger_mappings(self, **kwargs: Any) -> ListLedgerMappings: - variables: dict[str, object] = {} + def list_ledger_mappings( + self, entity_id: Union[Optional[str], UnsetType] = UNSET, **kwargs: Any + ) -> ListLedgerMappings: + variables: dict[str, object] = {"entityId": entity_id} response = self.execute( query=LIST_LEDGER_MAPPINGS_GQL, operation_name="ListLedgerMappings", @@ -916,8 +922,16 @@ def list_ledger_unmapped_elements( data = self.get_data(response) return ListLedgerUnmappedElements.model_validate(data) - def mapping_candidates(self, classification: str, **kwargs: Any) -> MappingCandidates: - variables: dict[str, object] = {"classification": classification} + def mapping_candidates( + self, + classification: str, + entity_id: Union[Optional[str], UnsetType] = UNSET, + **kwargs: Any, + ) -> MappingCandidates: + variables: dict[str, object] = { + "classification": classification, + "entityId": entity_id, + } response = self.execute( query=MAPPING_CANDIDATES_GQL, operation_name="MappingCandidates", diff --git a/robosystems_client/graphql/generated/get_information_block.py b/robosystems_client/graphql/generated/get_information_block.py index a2e57a2..a9dbdbf 100644 --- a/robosystems_client/graphql/generated/get_information_block.py +++ b/robosystems_client/graphql/generated/get_information_block.py @@ -19,6 +19,7 @@ class GetInformationBlockInformationBlock(BaseModel): category: str taxonomy_id: Optional[str] = Field(alias="taxonomyId") taxonomy_name: Optional[str] = Field(alias="taxonomyName") + entity_id: Optional[str] = Field(alias="entityId") information_model: "GetInformationBlockInformationBlockInformationModel" = Field( alias="informationModel" ) diff --git a/robosystems_client/graphql/generated/list_information_blocks.py b/robosystems_client/graphql/generated/list_information_blocks.py index 6091255..8224b64 100644 --- a/robosystems_client/graphql/generated/list_information_blocks.py +++ b/robosystems_client/graphql/generated/list_information_blocks.py @@ -19,6 +19,7 @@ class ListInformationBlocksInformationBlocks(BaseModel): category: str taxonomy_id: Optional[str] = Field(alias="taxonomyId") taxonomy_name: Optional[str] = Field(alias="taxonomyName") + entity_id: Optional[str] = Field(alias="entityId") information_model: "ListInformationBlocksInformationBlocksInformationModel" = Field( alias="informationModel" ) diff --git a/robosystems_client/graphql/generated/list_ledger_mappings.py b/robosystems_client/graphql/generated/list_ledger_mappings.py index dcaa20e..c0767bd 100644 --- a/robosystems_client/graphql/generated/list_ledger_mappings.py +++ b/robosystems_client/graphql/generated/list_ledger_mappings.py @@ -21,6 +21,7 @@ class ListLedgerMappingsMappingsStructures(BaseModel): taxonomy_id: str = Field(alias="taxonomyId") is_active: bool = Field(alias="isActive") framework: Optional[str] + entity_id: Optional[str] = Field(alias="entityId") ListLedgerMappings.model_rebuild() diff --git a/robosystems_client/graphql/generated/operations.py b/robosystems_client/graphql/generated/operations.py index 3ebd5d3..4c951c1 100644 --- a/robosystems_client/graphql/generated/operations.py +++ b/robosystems_client/graphql/generated/operations.py @@ -279,13 +279,14 @@ """ GET_INFORMATION_BLOCK_GQL = """ -query GetInformationBlock($id: ID!, $scenarioId: String, $series: Boolean! = false, $seriesHistory: Int, $seriesForecast: Int) { +query GetInformationBlock($id: ID!, $scenarioId: String, $series: Boolean! = false, $seriesHistory: Int, $seriesForecast: Int, $entityId: String) { informationBlock( id: $id scenarioId: $scenarioId series: $series seriesHistory: $seriesHistory seriesForecast: $seriesForecast + entityId: $entityId ) { id blockType @@ -294,6 +295,7 @@ category taxonomyId taxonomyName + entityId informationModel { conceptArrangement memberArrangement @@ -1192,13 +1194,14 @@ """ LIST_INFORMATION_BLOCKS_GQL = """ -query ListInformationBlocks($blockType: String, $category: String, $limit: Int, $offset: Int, $scenarioId: String) { +query ListInformationBlocks($blockType: String, $category: String, $limit: Int, $offset: Int, $scenarioId: String, $entityId: String) { informationBlocks( blockType: $blockType category: $category limit: $limit offset: $offset scenarioId: $scenarioId + entityId: $entityId ) { id blockType @@ -1207,6 +1210,7 @@ category taxonomyId taxonomyName + entityId informationModel { conceptArrangement memberArrangement @@ -1608,8 +1612,8 @@ """ LIST_LEDGER_MAPPINGS_GQL = """ -query ListLedgerMappings { - mappings { +query ListLedgerMappings($entityId: String) { + mappings(entityId: $entityId) { structures { id name @@ -1618,6 +1622,7 @@ taxonomyId isActive framework + entityId } } } @@ -1840,8 +1845,8 @@ """ MAPPING_CANDIDATES_GQL = """ -query MappingCandidates($classification: String!) { - mappingCandidates(classification: $classification) { +query MappingCandidates($classification: String!, $entityId: String) { + mappingCandidates(classification: $classification, entityId: $entityId) { id name qname diff --git a/robosystems_client/graphql/operations/ledger/GetInformationBlock.graphql b/robosystems_client/graphql/operations/ledger/GetInformationBlock.graphql index 701a9c8..4d5eff9 100644 --- a/robosystems_client/graphql/operations/ledger/GetInformationBlock.graphql +++ b/robosystems_client/graphql/operations/ledger/GetInformationBlock.graphql @@ -4,6 +4,7 @@ query GetInformationBlock( $series: Boolean! = false $seriesHistory: Int $seriesForecast: Int + $entityId: String ) { informationBlock( id: $id @@ -11,9 +12,10 @@ query GetInformationBlock( series: $series seriesHistory: $seriesHistory seriesForecast: $seriesForecast + entityId: $entityId ) { id blockType name displayName category - taxonomyId taxonomyName + taxonomyId taxonomyName entityId informationModel { conceptArrangement memberArrangement } artifact { topic rendererNote template mechanics } elements { diff --git a/robosystems_client/graphql/operations/ledger/ListInformationBlocks.graphql b/robosystems_client/graphql/operations/ledger/ListInformationBlocks.graphql index 7661753..63b7e0e 100644 --- a/robosystems_client/graphql/operations/ledger/ListInformationBlocks.graphql +++ b/robosystems_client/graphql/operations/ledger/ListInformationBlocks.graphql @@ -4,6 +4,7 @@ query ListInformationBlocks( $limit: Int $offset: Int $scenarioId: String + $entityId: String ) { informationBlocks( blockType: $blockType @@ -11,9 +12,10 @@ query ListInformationBlocks( limit: $limit offset: $offset scenarioId: $scenarioId + entityId: $entityId ) { id blockType name displayName category - taxonomyId taxonomyName + taxonomyId taxonomyName entityId informationModel { conceptArrangement memberArrangement } artifact { topic rendererNote template mechanics } elements { diff --git a/robosystems_client/graphql/operations/ledger/ListLedgerMappings.graphql b/robosystems_client/graphql/operations/ledger/ListLedgerMappings.graphql index 648bc38..7a0f318 100644 --- a/robosystems_client/graphql/operations/ledger/ListLedgerMappings.graphql +++ b/robosystems_client/graphql/operations/ledger/ListLedgerMappings.graphql @@ -1,7 +1,7 @@ -query ListLedgerMappings { - mappings { +query ListLedgerMappings($entityId: String) { + mappings(entityId: $entityId) { structures { - id name description blockType taxonomyId isActive framework + id name description blockType taxonomyId isActive framework entityId } } } diff --git a/robosystems_client/graphql/operations/ledger/MappingCandidates.graphql b/robosystems_client/graphql/operations/ledger/MappingCandidates.graphql index 1ac5fe1..148cfdf 100644 --- a/robosystems_client/graphql/operations/ledger/MappingCandidates.graphql +++ b/robosystems_client/graphql/operations/ledger/MappingCandidates.graphql @@ -1,5 +1,5 @@ -query MappingCandidates($classification: String!) { - mappingCandidates(classification: $classification) { +query MappingCandidates($classification: String!, $entityId: String) { + mappingCandidates(classification: $classification, entityId: $entityId) { id name qname diff --git a/robosystems_client/graphql/schema.graphql b/robosystems_client/graphql/schema.graphql index 30b5807..783664e 100644 --- a/robosystems_client/graphql/schema.graphql +++ b/robosystems_client/graphql/schema.graphql @@ -178,6 +178,11 @@ type Query { """Filter on the event's category label.""" eventCategory: String = null + """ + `economic` for business activity, `support` for evidence about it (balance readings, reconciliation sign-offs), `operational` for schedule obligations. A balance reading's amount is a balance, not a flow. + """ + eventClass: String = null + """ Filter by lifecycle state: `captured` is the unposted queue, `committed` the audit trail. """ @@ -439,17 +444,20 @@ type Query { """ The rs-gaap concepts a chart-of-accounts element may map to. - Limited to concepts that actually render under the active Reporting Style - — falling back to the rs-gaap-presentation set when no Style is seeded — - with statement-level subtotals excluded. It is the same candidate set the - mapping operator picks from, so a target that would land a fact on an - unreachable branch is never offered. + Limited to concepts that actually render under the entity's Reporting + Style — falling back to the rs-gaap-presentation set when no Style is + seeded — with statement-level subtotals excluded. It is the same candidate + set the mapping operator picks from, so a target that would land a fact on + an unreachable branch is never offered. """ mappingCandidates( """ The CoA element's EFS classification - asset, liability, equity, revenue, expense, gain or loss - whose eligible rs-gaap targets to return. """ classification: String! + + """The entity whose chart is being mapped. Omit for the group parent.""" + entityId: String = null ): [Element!]! """CoA elements not yet mapped to the reporting taxonomy.""" @@ -467,8 +475,14 @@ type Query { blockType: String = null ): StructureList - """List all active mapping structures.""" - mappings: StructureList + """ + List the active mapping structures, each naming the entity whose chart + it maps from. + """ + mappings( + """Keep only this entity's mappings. Omit for every entity's.""" + entityId: String = null + ): StructureList """Single mapping structure with all associations.""" mapping( @@ -669,6 +683,12 @@ type Query { seam-adjacent columns — the last N actual columns and the first N forecast columns; omitted = unbounded. Pass the visible window so the envelope scales with the screen, not the ledger's age. + + `entityId` picks whose books a block shared by the group reads + (statements, metrics, disclosures); omitted = the scenario's entity, + else the group parent. A schedule, reconciliation or forecast is one + entity's own and always reads its owner's. `entityId` on the envelope + names the entity it read. """ informationBlock( """The block's structure id.""" @@ -687,6 +707,11 @@ type Query { """Cap the series at the first N forecast columns. Omit for unbounded.""" seriesForecast: Int = null + + """ + The entity whose books a shared block reads. Omit for the group parent. + """ + entityId: String = null ): InformationBlock """ @@ -697,6 +722,11 @@ type Query { category label ('Close', 'Reporting', …). Both combine as AND. `scenarioId` threads into each envelope's FactSet binding (the Structure list itself is scenario-independent). + + `entityId` (omitted = the scenario's entity, else the group parent) + lists the blocks shared by the group plus that entity's own schedules, + reconciliations and forecasts, never another entity's; shared blocks + read its books. """ informationBlocks( """Filter to one registered block type, e.g. `schedule`.""" @@ -721,6 +751,9 @@ type Query { A forecast block's structure id, threaded into each envelope's FactSet binding. """ scenarioId: String = null + + """The entity whose blocks and books to list. Omit for the group parent.""" + entityId: String = null ): [InformationBlock!]! """Fetch a single Taxonomy Block envelope by id.""" @@ -2254,6 +2287,11 @@ type Structure { `coa_mapping` only: the reporting framework the mapping maps the chart into (e.g. `rs-gaap`). A chart holds one mapping per framework. """ framework: String + + """ + `coa_mapping` only, in the mappings list: the entity whose chart the mapping maps from, so whose reports it can produce. + """ + entityId: String } """A mapping structure with all its associations.""" @@ -2939,6 +2977,11 @@ type InformationBlock { taxonomyId: String taxonomyName: String disclosureId: String + + """ + The entity whose books the envelope reads: a schedule, reconciliation or forecast reads its owner's; a shared block (statements, metrics) the entity asked for, else the group parent. + """ + entityId: String informationModel: InformationModel! artifact: Artifact! elements: [InformationBlockElement!]! diff --git a/robosystems_client/models/assert_metrics_request.py b/robosystems_client/models/assert_metrics_request.py index 58cc67b..2e9dc69 100644 --- a/robosystems_client/models/assert_metrics_request.py +++ b/robosystems_client/models/assert_metrics_request.py @@ -41,8 +41,7 @@ class AssertMetricsRequest: observations (list[MetricObservation]): Observed values, one per metric concept — duplicates rejected. period_start (datetime.date | None | Unset): Window start for duration concepts and the standing FactSet's period_start. Instant concepts ignore it. - entity_id (None | str | Unset): Entity to assert for. Defaults to the graph's earliest-created entity (the - primary entity for single-entity graphs). + entity_id (None | str | Unset): The entity the asserted values belong to, by id. Omit for the group parent. basis_note (None | str | Unset): Free-text basis / source reference for the observations. """ diff --git a/robosystems_client/models/bind_text_block_request.py b/robosystems_client/models/bind_text_block_request.py index 8e97251..66906c4 100644 --- a/robosystems_client/models/bind_text_block_request.py +++ b/robosystems_client/models/bind_text_block_request.py @@ -26,7 +26,7 @@ class BindTextBlockRequest: element_id (None | str | Unset): Disclosure element to tag the text to (id form). element_qname (None | str | Unset): Disclosure element qname (e.g. 'acme:SignificantAccountingPoliciesTextBlock') — exactly one of element_id / element_qname. - entity_id (None | str | Unset): Entity the fact belongs to; defaults to the primary entity. + entity_id (None | str | Unset): Entity the fact belongs to, by id; omit for the group parent. """ document_id: str diff --git a/robosystems_client/models/change_reporting_style_request.py b/robosystems_client/models/change_reporting_style_request.py index c35f72c..74ddebb 100644 --- a/robosystems_client/models/change_reporting_style_request.py +++ b/robosystems_client/models/change_reporting_style_request.py @@ -23,8 +23,7 @@ class ChangeReportingStyleRequest: Attributes: reporting_style_id (str): Structure id of the target Reporting Style. Must exist in the tenant schema with a complete Network composition. - entity_id (None | str | Unset): Target entity. Omit to target the graph's primary (earliest-created) entity — - the single-entity default. + entity_id (None | str | Unset): The entity whose style to change, by id. Omit for the group parent. """ reporting_style_id: str diff --git a/robosystems_client/models/compute_metrics_request.py b/robosystems_client/models/compute_metrics_request.py index 2c39ddd..6132b3e 100644 --- a/robosystems_client/models/compute_metrics_request.py +++ b/robosystems_client/models/compute_metrics_request.py @@ -29,8 +29,7 @@ class ComputeMetricsRequest: exactly (instant balances as of this date; durations ending on it). period_start (datetime.date | None | Unset): Optional lower bound for duration-operand binding and the standing FactSet's period_start. - entity_id (None | str | Unset): Entity to compute for. Defaults to the graph's earliest-created entity (the - primary entity for single-entity graphs). + entity_id (None | str | Unset): The entity whose metrics to compute, by id. Omit for the group parent. scenario_id (None | str | Unset): Compute on a scenario slice: operands bind that scenario's facts (actuals as the fallback across the seam) and the standing metric set is stamped with the scenario. None (the default) computes actuals only. Pass a forecast block's structure id after compute-forecast to extend the metric series diff --git a/robosystems_client/models/create_forecast_request.py b/robosystems_client/models/create_forecast_request.py index 7b91186..fdf87b8 100644 --- a/robosystems_client/models/create_forecast_request.py +++ b/robosystems_client/models/create_forecast_request.py @@ -52,8 +52,8 @@ class CreateForecastRequest: line_growth (list[LineGrowthRequest] | Unset): Per-line growth trajectories. Each names an income-statement leaf and grows it month-over-month at the asserted rate — the generic form of the revenue growth lever, for lines the catalog doesn't drive (opex trajectories, cost-cut ramps). - entity_id (None | str | Unset): Entity the scenario belongs to. Defaults to the graph's earliest-created entity - (single-entity convention). + entity_id (None | str | Unset): The entity the scenario belongs to, by id: its history and its computed months + are that entity's books. Omit for the group parent. """ name: str diff --git a/robosystems_client/models/create_schedule_request.py b/robosystems_client/models/create_schedule_request.py index e6acec7..e2904b8 100644 --- a/robosystems_client/models/create_schedule_request.py +++ b/robosystems_client/models/create_schedule_request.py @@ -28,6 +28,9 @@ class CreateScheduleRequest: period_end (datetime.date): Last period end monthly_amount (int): Monthly amount in cents entry_template (EntryTemplateRequest): + entity_id (None | str | Unset): The entity whose books the schedule belongs to, by id. Omit for the group + parent. Its facts, obligations and closing entries land in that entity's books, against that entity's own + accounts. taxonomy_id (None | str | Unset): Taxonomy ID (auto-creates if omitted) schedule_metadata (None | ScheduleMetadataRequest | Unset): closed_through (datetime.date | None | Unset): Watermark for onboarding. Facts with period_end ≤ this date are @@ -46,6 +49,7 @@ class CreateScheduleRequest: period_end: datetime.date monthly_amount: int entry_template: EntryTemplateRequest + entity_id: None | str | Unset = UNSET taxonomy_id: None | str | Unset = UNSET schedule_metadata: None | ScheduleMetadataRequest | Unset = UNSET closed_through: datetime.date | None | Unset = UNSET @@ -67,6 +71,12 @@ def to_dict(self) -> dict[str, Any]: entry_template = self.entry_template.to_dict() + entity_id: None | str | Unset + if isinstance(self.entity_id, Unset): + entity_id = UNSET + else: + entity_id = self.entity_id + taxonomy_id: None | str | Unset if isinstance(self.taxonomy_id, Unset): taxonomy_id = UNSET @@ -107,6 +117,8 @@ def to_dict(self) -> dict[str, Any]: "entry_template": entry_template, } ) + if entity_id is not UNSET: + field_dict["entity_id"] = entity_id if taxonomy_id is not UNSET: field_dict["taxonomy_id"] = taxonomy_id if schedule_metadata is not UNSET: @@ -136,6 +148,15 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: entry_template = EntryTemplateRequest.from_dict(d.pop("entry_template")) + def _parse_entity_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + entity_id = _parse_entity_id(d.pop("entity_id", UNSET)) + def _parse_taxonomy_id(data: object) -> None | str | Unset: if data is None: return data @@ -199,6 +220,7 @@ def _parse_source_transaction_id(data: object) -> None | str | Unset: period_end=period_end, monthly_amount=monthly_amount, entry_template=entry_template, + entity_id=entity_id, taxonomy_id=taxonomy_id, schedule_metadata=schedule_metadata, closed_through=closed_through, diff --git a/robosystems_client/models/information_block_envelope.py b/robosystems_client/models/information_block_envelope.py index 15e1f2b..8c77efa 100644 --- a/robosystems_client/models/information_block_envelope.py +++ b/robosystems_client/models/information_block_envelope.py @@ -54,6 +54,9 @@ class InformationBlockEnvelope: disclosure_id (None | str | Unset): Qname of the named Disclosure this block corresponds to (e.g., 'disclosures:BalanceSheet'), when an inbound reportedDisclosure-requiresDisclosure arc identifies one. Null for tenant-authored blocks without a Disclosure mapping. + entity_id (None | str | Unset): The entity whose books the envelope reads. A schedule, reconciliation or + forecast is one entity's own and always reads its owner's; a block shared by the group (statements, metrics) + reads the entity asked for, else the group parent. Null on the library and on a graph with no entity yet. elements (list[ElementLite] | Unset): connections (list[ConnectionLite] | Unset): facts (list[FactLite] | Unset): @@ -92,6 +95,7 @@ class InformationBlockEnvelope: taxonomy_id: None | str | Unset = UNSET taxonomy_name: None | str | Unset = UNSET disclosure_id: None | str | Unset = UNSET + entity_id: None | str | Unset = UNSET elements: list[ElementLite] | Unset = UNSET connections: list[ConnectionLite] | Unset = UNSET facts: list[FactLite] | Unset = UNSET @@ -139,6 +143,12 @@ def to_dict(self) -> dict[str, Any]: else: disclosure_id = self.disclosure_id + entity_id: None | str | Unset + if isinstance(self.entity_id, Unset): + entity_id = UNSET + else: + entity_id = self.entity_id + elements: list[dict[str, Any]] | Unset = UNSET if not isinstance(self.elements, Unset): elements = [] @@ -220,6 +230,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["taxonomy_name"] = taxonomy_name if disclosure_id is not UNSET: field_dict["disclosure_id"] = disclosure_id + if entity_id is not UNSET: + field_dict["entity_id"] = entity_id if elements is not UNSET: field_dict["elements"] = elements if connections is not UNSET: @@ -299,6 +311,15 @@ def _parse_disclosure_id(data: object) -> None | str | Unset: disclosure_id = _parse_disclosure_id(d.pop("disclosure_id", UNSET)) + def _parse_entity_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + entity_id = _parse_entity_id(d.pop("entity_id", UNSET)) + _elements = d.pop("elements", UNSET) elements: list[ElementLite] | Unset = UNSET if _elements is not UNSET: @@ -411,6 +432,7 @@ def _parse_verification_summary(data: object) -> None | Unset | VerificationSumm taxonomy_id=taxonomy_id, taxonomy_name=taxonomy_name, disclosure_id=disclosure_id, + entity_id=entity_id, elements=elements, connections=connections, facts=facts, diff --git a/tests/test_facade_wire_shapes.py b/tests/test_facade_wire_shapes.py index b162acc..de5dcfd 100644 --- a/tests/test_facade_wire_shapes.py +++ b/tests/test_facade_wire_shapes.py @@ -686,6 +686,22 @@ def test_create_report_sends_periods_and_iso_dates(self, httpx_mock): {"start": "2025-01-01", "end": "2025-03-31", "label": "Q1"} ] + def test_create_report_sends_the_entity(self, httpx_mock): + httpx_mock.add_response( + method="POST", + url=f"{BASE}/extensions/roboledger/{GRAPH}/operations/create-report", + status_code=500, + json={"detail": "stop here"}, + ) + + with pytest.raises(RuntimeError): + LedgerClient(CONFIG).create_report( + GRAPH, "FY25", "map_sub", "2025-01-01", "2025-12-31", entity_id="ent_sub" + ) + + body = json.loads(httpx_mock.get_requests()[0].content) + assert body["entity_id"] == "ent_sub" + def test_list_information_blocks_sends_scenario_id(self, httpx_mock): httpx_mock.add_response( method="POST", diff --git a/tests/test_ledger_client.py b/tests/test_ledger_client.py index e296d6d..a09179c 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -261,15 +261,18 @@ def test_list_mappings_unwraps_structures(self, mock_execute, mock_config, graph "taxonomyId": "tax_map_gaap", "isActive": True, "framework": "rs-gaap", + "entityId": "ent_sub", } ] } } client = LedgerClient(mock_config) - mappings = client.list_mappings(graph_id) + mappings = client.list_mappings(graph_id, entity_id="ent_sub") assert len(mappings) == 1 assert mappings[0].block_type == "coa_mapping" assert mappings[0].framework == "rs-gaap" + assert mappings[0].entity_id == "ent_sub" + assert mock_execute.call_args[0][2] == {"entityId": "ent_sub"} @patch("robosystems_client.graphql.client.GraphQLClient.execute") def test_get_fiscal_calendar(self, mock_execute, mock_config, graph_id): @@ -597,6 +600,26 @@ def test_create_schedule_carries_the_day_its_cost_was_booked( ) payload = mock_op.call_args.kwargs["body"].payload.to_dict() assert payload["schedule_metadata"]["booked_on"] == "2026-01-15" + assert "entity_id" not in payload + + @patch("robosystems_client.clients.ledger_client.op_create_information_block") + def test_create_schedule_for_a_subsidiary(self, mock_op, mock_config, graph_id): + envelope = _envelope("create-information-block", {"structure_id": "str_1"}) + mock_op.return_value = _mock_response(envelope) + client = LedgerClient(mock_config) + client.create_schedule( + graph_id, + name="Café prepaid rent", + element_ids=["elem_1"], + period_start="2026-02-01", + period_end="2027-01-31", + monthly_amount=10000, + debit_element_id="elem_rent", + credit_element_id="elem_prepaid", + entity_id="ent_sub", + ) + payload = mock_op.call_args.kwargs["body"].payload.to_dict() + assert payload["entity_id"] == "ent_sub" @patch("robosystems_client.clients.ledger_client.op_create_information_block") def test_create_schedule(self, mock_op, mock_config, graph_id): @@ -1545,6 +1568,7 @@ def test_list_information_blocks(self, mock_execute, mock_config, graph_id): "category": "Close", "taxonomyId": "tax_01", "taxonomyName": "My CoA", + "entityId": "ent_parent", "informationModel": { "conceptArrangement": "roll_forward", "memberArrangement": None, @@ -1591,6 +1615,7 @@ def test_get_information_block(self, mock_execute, mock_config, graph_id): "category": "Close", "taxonomyId": None, "taxonomyName": None, + "entityId": "ent_parent", "informationModel": { "conceptArrangement": "roll_forward", "memberArrangement": None, @@ -2534,6 +2559,32 @@ def test_get_information_block_forwards_scenario_series_window( "seriesForecast": 12, } + @patch("robosystems_client.graphql.client.GraphQLClient.execute") + def test_block_reads_forward_the_entity(self, mock_execute, mock_config, graph_id): + mock_execute.return_value = {"informationBlock": None} + client = LedgerClient(mock_config) + client.get_information_block(graph_id, "blk_1", entity_id="ent_sub") + assert mock_execute.call_args[0][2] == {"id": "blk_1", "entityId": "ent_sub"} + + mock_execute.return_value = {"informationBlocks": []} + client.list_information_blocks(graph_id, block_type="forecast", entity_id="ent_sub") + assert mock_execute.call_args[0][2] == { + "blockType": "forecast", + "entityId": "ent_sub", + } + + @patch("robosystems_client.graphql.client.GraphQLClient.execute") + def test_mapping_candidates_forward_the_entity( + self, mock_execute, mock_config, graph_id + ): + mock_execute.return_value = {"mappingCandidates": []} + client = LedgerClient(mock_config) + client.get_mapping_candidates(graph_id, "asset", entity_id="ent_sub") + assert mock_execute.call_args[0][2] == { + "classification": "asset", + "entityId": "ent_sub", + } + # ── Compute metrics ────────────────────────────────────────────────────