diff --git a/robosystems_client/clients/ledger_client.py b/robosystems_client/clients/ledger_client.py index 9f071a0..24c159a 100644 --- a/robosystems_client/clients/ledger_client.py +++ b/robosystems_client/clients/ledger_client.py @@ -2694,6 +2694,7 @@ def close_period( allow_reconciling_items: bool | None = None, allow_unposted_source_events: bool | None = None, entity_id: str | None = None, + allow_unexplained_residuals: bool | None = None, ) -> ClosePeriodResponse: """Close a fiscal period — the final commit action — on the group parent, or on the entity ``entity_id`` names. Each entity closes on its own. @@ -2709,6 +2710,9 @@ def close_period( changed afterwards (`resolve_reconciling_item`). - ``allow_unposted_source_events``: source events dated in the period that were never committed, and cannot post into it once it closes. + - ``allow_unexplained_residuals``: on the consolidation entity, whose + close is the group's, a residual or intercompany disagreement left + unexplained; its difference stays in the consolidated statements. """ def flag(value: bool | None) -> bool | Unset: @@ -2723,6 +2727,7 @@ def flag(value: bool | None) -> bool | Unset: allow_stranded_obligations=flag(allow_stranded_obligations), allow_reconciling_items=flag(allow_reconciling_items), allow_unposted_source_events=flag(allow_unposted_source_events), + allow_unexplained_residuals=flag(allow_unexplained_residuals), ) response = op_close_period(graph_id=graph_id, body=body, client=self._get_client()) envelope = self._call_op("Close period", response) diff --git a/robosystems_client/graphql/generated/get_ledger_fiscal_calendar.py b/robosystems_client/graphql/generated/get_ledger_fiscal_calendar.py index 6784a79..2bb427c 100644 --- a/robosystems_client/graphql/generated/get_ledger_fiscal_calendar.py +++ b/robosystems_client/graphql/generated/get_ledger_fiscal_calendar.py @@ -36,6 +36,10 @@ class GetLedgerFiscalCalendarFiscalCalendar(BaseModel): unposted_source_event_sample: list[str] = Field(alias="unpostedSourceEventSample") unreconciled_account_count: int = Field(alias="unreconciledAccountCount") unreconciled_account_sample: list[str] = Field(alias="unreconciledAccountSample") + group_open_entities: list[str] = Field(alias="groupOpenEntities") + group_pending_adjustments: list[str] = Field(alias="groupPendingAdjustments") + group_unexplained: list[str] = Field(alias="groupUnexplained") + group_not_footing: list[str] = Field(alias="groupNotFooting") sync_stale_days: Optional[int] = Field(alias="syncStaleDays") last_close_at: Optional[str] = Field(alias="lastCloseAt") initialized_at: Optional[str] = Field(alias="initializedAt") diff --git a/robosystems_client/graphql/generated/operations.py b/robosystems_client/graphql/generated/operations.py index a2a3c4a..966aa46 100644 --- a/robosystems_client/graphql/generated/operations.py +++ b/robosystems_client/graphql/generated/operations.py @@ -723,6 +723,10 @@ unpostedSourceEventSample unreconciledAccountCount unreconciledAccountSample + groupOpenEntities + groupPendingAdjustments + groupUnexplained + groupNotFooting syncStaleDays lastCloseAt initializedAt diff --git a/robosystems_client/graphql/operations/ledger/GetLedgerFiscalCalendar.graphql b/robosystems_client/graphql/operations/ledger/GetLedgerFiscalCalendar.graphql index a80309a..2a3a889 100644 --- a/robosystems_client/graphql/operations/ledger/GetLedgerFiscalCalendar.graphql +++ b/robosystems_client/graphql/operations/ledger/GetLedgerFiscalCalendar.graphql @@ -10,6 +10,7 @@ query GetLedgerFiscalCalendar($entityId: String) { reconcilingItemCount reconcilingItemSample unpostedSourceEventCount unpostedSourceEventSample unreconciledAccountCount unreconciledAccountSample + groupOpenEntities groupPendingAdjustments groupUnexplained groupNotFooting syncStaleDays lastCloseAt initializedAt lastSyncAt periods { name startDate endDate status closedAt } diff --git a/robosystems_client/graphql/schema.graphql b/robosystems_client/graphql/schema.graphql index 09c00ad..44ff599 100644 --- a/robosystems_client/graphql/schema.graphql +++ b/robosystems_client/graphql/schema.graphql @@ -2821,7 +2821,7 @@ type FiscalCalendar { closeableNow: Boolean! """ - Structured blocker codes when closeable_now is False: 'sequence_violation', 'period_incomplete', 'sync_stale', 'calendar_not_initialized', 'period_already_closed', 'pending_obligations', 'stranded_obligations', 'reconciling_items', 'unposted_source_events', 'unreconciled_accounts' + Structured blocker codes when closeable_now is False: 'sequence_violation', 'period_incomplete', 'sync_stale', 'calendar_not_initialized', 'period_already_closed', 'pending_obligations', 'stranded_obligations', 'reconciling_items', 'unposted_source_events', 'unreconciled_accounts'; on the consolidation entity, whose close is the group's, also 'entity_open', 'adjustments_unposted', 'residual_unexplained', 'consolidated_does_not_foot' """ blockers: [String!]! @@ -2884,6 +2884,26 @@ type FiscalCalendar { Name and status of up to 5 of those reconciliations, so the blocker names what is holding the close. """ unreconciledAccountSample: [String!]! + + """ + The group close: the entities the month consolidates, the parent and its equity-method members included, that have not closed it. Close each first. + """ + groupOpenEntities: [String!]! + + """ + The group close: the consolidating entries the consolidation entity neither holds nor has drafted, by kind and member, or what the engine left out and why. Run propose-consolidation-entries. + """ + groupPendingAdjustments: [String!]! + + """ + The group close: residuals and intercompany disagreements left unexplained. Correct the books or post the explaining entry, or close over them knowingly with allow_unexplained_residuals. + """ + groupUnexplained: [String!]! + + """ + The group close: the consolidated balance sheet's and equity statement's validation failures for the month. + """ + groupNotFooting: [String!]! lastCloseAt: DateTime initializedAt: DateTime diff --git a/robosystems_client/models/backfill_plan_history_operation.py b/robosystems_client/models/backfill_plan_history_operation.py index e03843e..dbb5c52 100644 --- a/robosystems_client/models/backfill_plan_history_operation.py +++ b/robosystems_client/models/backfill_plan_history_operation.py @@ -36,6 +36,8 @@ class BackfillPlanHistoryOperation: allow_unreconciled_accounts (bool | Unset): Override the reconciliation gate on each reclose. Needed when a month inside the backfill window has a reconciliation the close waits on that was never compared for it, which is every month closed before the reconciliation existed. Default: False. + allow_unexplained_residuals (bool | Unset): Override the group close's residual gate on each reclose of the + consolidation entity's months. Default: False. restamp (bool | Unset): Also re-derive months that ALREADY have canonical statement sets (default: skip them). Use after an engine improvement changes what a stamp produces — each month reruns the full reopen → reclose cycle and replaces its sets. A restamp run is not self-resuming (every month in range stays a candidate); @@ -51,6 +53,7 @@ class BackfillPlanHistoryOperation: allow_reconciling_items: bool | Unset = False allow_unposted_source_events: bool | Unset = False allow_unreconciled_accounts: bool | Unset = False + allow_unexplained_residuals: bool | Unset = False restamp: bool | Unset = False note: None | str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -80,6 +83,8 @@ def to_dict(self) -> dict[str, Any]: allow_unreconciled_accounts = self.allow_unreconciled_accounts + allow_unexplained_residuals = self.allow_unexplained_residuals + restamp = self.restamp note: None | str | Unset @@ -107,6 +112,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["allow_unposted_source_events"] = allow_unposted_source_events if allow_unreconciled_accounts is not UNSET: field_dict["allow_unreconciled_accounts"] = allow_unreconciled_accounts + if allow_unexplained_residuals is not UNSET: + field_dict["allow_unexplained_residuals"] = allow_unexplained_residuals if restamp is not UNSET: field_dict["restamp"] = restamp if note is not UNSET: @@ -148,6 +155,8 @@ def _parse_start_period(data: object) -> None | str | Unset: allow_unreconciled_accounts = d.pop("allow_unreconciled_accounts", UNSET) + allow_unexplained_residuals = d.pop("allow_unexplained_residuals", UNSET) + restamp = d.pop("restamp", UNSET) def _parse_note(data: object) -> None | str | Unset: @@ -168,6 +177,7 @@ def _parse_note(data: object) -> None | str | Unset: allow_reconciling_items=allow_reconciling_items, allow_unposted_source_events=allow_unposted_source_events, allow_unreconciled_accounts=allow_unreconciled_accounts, + allow_unexplained_residuals=allow_unexplained_residuals, restamp=restamp, note=note, ) diff --git a/robosystems_client/models/close_period_operation.py b/robosystems_client/models/close_period_operation.py index 8f24aeb..d81f69f 100644 --- a/robosystems_client/models/close_period_operation.py +++ b/robosystems_client/models/close_period_operation.py @@ -41,6 +41,10 @@ class ClosePeriodOperation: reconciliation the close waits on is not reconciled for the period, or was never compared for it. The books are then closed without that check. Prefer refresh-reconciliations and clearing what it reports first. The override is recorded in the close audit note. Default: False. + allow_unexplained_residuals (bool | Unset): On the consolidation entity, the group close: close even though a + residual or an intercompany disagreement is left unexplained. Its difference stays in the consolidated + statements. Prefer correcting the books or posting the explaining entry with its basis. The override, and each + item it closed over, is recorded in the close audit note. Default: False. """ period: str @@ -51,6 +55,7 @@ class ClosePeriodOperation: allow_reconciling_items: bool | Unset = False allow_unposted_source_events: bool | Unset = False allow_unreconciled_accounts: bool | Unset = False + allow_unexplained_residuals: bool | Unset = False additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -78,6 +83,8 @@ def to_dict(self) -> dict[str, Any]: allow_unreconciled_accounts = self.allow_unreconciled_accounts + allow_unexplained_residuals = self.allow_unexplained_residuals + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -99,6 +106,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["allow_unposted_source_events"] = allow_unposted_source_events if allow_unreconciled_accounts is not UNSET: field_dict["allow_unreconciled_accounts"] = allow_unreconciled_accounts + if allow_unexplained_residuals is not UNSET: + field_dict["allow_unexplained_residuals"] = allow_unexplained_residuals return field_dict @@ -135,6 +144,8 @@ def _parse_note(data: object) -> None | str | Unset: allow_unreconciled_accounts = d.pop("allow_unreconciled_accounts", UNSET) + allow_unexplained_residuals = d.pop("allow_unexplained_residuals", UNSET) + close_period_operation = cls( period=period, entity_id=entity_id, @@ -144,6 +155,7 @@ def _parse_note(data: object) -> None | str | Unset: allow_reconciling_items=allow_reconciling_items, allow_unposted_source_events=allow_unposted_source_events, allow_unreconciled_accounts=allow_unreconciled_accounts, + allow_unexplained_residuals=allow_unexplained_residuals, ) close_period_operation.additional_properties = d diff --git a/robosystems_client/models/fiscal_calendar_response.py b/robosystems_client/models/fiscal_calendar_response.py index 2383dac..ba0b81d 100644 --- a/robosystems_client/models/fiscal_calendar_response.py +++ b/robosystems_client/models/fiscal_calendar_response.py @@ -36,7 +36,9 @@ class FiscalCalendarResponse: 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', 'pending_obligations', - 'stranded_obligations', 'reconciling_items', 'unposted_source_events', 'unreconciled_accounts' + 'stranded_obligations', 'reconciling_items', 'unposted_source_events', 'unreconciled_accounts'; on the + consolidation entity, whose close is the group's, also 'entity_open', 'adjustments_unposted', + 'residual_unexplained', 'consolidated_does_not_foot' pending_obligation_count (int | Unset): Number of pending schedule_entry_due events blocking close. Non-zero only when `pending_obligations` is in `blockers`. Default: 0. pending_obligation_sample (list[PendingObligationDetailResponse] | Unset): Sample of up to 5 pending obligations @@ -68,6 +70,16 @@ class FiscalCalendarResponse: with set-reconciliation-policy, or close over them knowingly with allow_unreconciled_accounts. Default: 0. unreconciled_account_sample (list[str] | Unset): Name and status of up to 5 of those reconciliations, so the blocker names what is holding the close. + group_open_entities (list[str] | Unset): The group close: the entities the month consolidates, the parent and + its equity-method members included, that have not closed it. Close each first. + group_pending_adjustments (list[str] | Unset): The group close: the consolidating entries the consolidation + entity neither holds nor has drafted, by kind and member, or what the engine left out and why. Run propose- + consolidation-entries. + group_unexplained (list[str] | Unset): The group close: residuals and intercompany disagreements left + unexplained. Correct the books or post the explaining entry, or close over them knowingly with + allow_unexplained_residuals. + group_not_footing (list[str] | Unset): The group close: the consolidated balance sheet's and equity statement's + validation failures for the month. 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) @@ -95,6 +107,10 @@ class FiscalCalendarResponse: unposted_source_event_sample: list[str] | Unset = UNSET unreconciled_account_count: int | Unset = 0 unreconciled_account_sample: list[str] | Unset = UNSET + group_open_entities: list[str] | Unset = UNSET + group_pending_adjustments: list[str] | Unset = UNSET + group_unexplained: list[str] | Unset = UNSET + group_not_footing: 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 @@ -184,6 +200,22 @@ def to_dict(self) -> dict[str, Any]: if not isinstance(self.unreconciled_account_sample, Unset): unreconciled_account_sample = self.unreconciled_account_sample + group_open_entities: list[str] | Unset = UNSET + if not isinstance(self.group_open_entities, Unset): + group_open_entities = self.group_open_entities + + group_pending_adjustments: list[str] | Unset = UNSET + if not isinstance(self.group_pending_adjustments, Unset): + group_pending_adjustments = self.group_pending_adjustments + + group_unexplained: list[str] | Unset = UNSET + if not isinstance(self.group_unexplained, Unset): + group_unexplained = self.group_unexplained + + group_not_footing: list[str] | Unset = UNSET + if not isinstance(self.group_not_footing, Unset): + group_not_footing = self.group_not_footing + last_close_at: None | str | Unset if isinstance(self.last_close_at, Unset): last_close_at = UNSET @@ -261,6 +293,14 @@ def to_dict(self) -> dict[str, Any]: field_dict["unreconciled_account_count"] = unreconciled_account_count if unreconciled_account_sample is not UNSET: field_dict["unreconciled_account_sample"] = unreconciled_account_sample + if group_open_entities is not UNSET: + field_dict["group_open_entities"] = group_open_entities + if group_pending_adjustments is not UNSET: + field_dict["group_pending_adjustments"] = group_pending_adjustments + if group_unexplained is not UNSET: + field_dict["group_unexplained"] = group_unexplained + if group_not_footing is not UNSET: + field_dict["group_not_footing"] = group_not_footing if last_close_at is not UNSET: field_dict["last_close_at"] = last_close_at if initialized_at is not UNSET: @@ -381,6 +421,16 @@ def _parse_sync_stale_days(data: object) -> int | None | Unset: list[str], d.pop("unreconciled_account_sample", UNSET) ) + group_open_entities = cast(list[str], d.pop("group_open_entities", UNSET)) + + group_pending_adjustments = cast( + list[str], d.pop("group_pending_adjustments", UNSET) + ) + + group_unexplained = cast(list[str], d.pop("group_unexplained", UNSET)) + + group_not_footing = cast(list[str], d.pop("group_not_footing", UNSET)) + def _parse_last_close_at(data: object) -> datetime.datetime | None | Unset: if data is None: return data @@ -463,6 +513,10 @@ def _parse_last_sync_at(data: object) -> datetime.datetime | None | Unset: unposted_source_event_sample=unposted_source_event_sample, unreconciled_account_count=unreconciled_account_count, unreconciled_account_sample=unreconciled_account_sample, + group_open_entities=group_open_entities, + group_pending_adjustments=group_pending_adjustments, + group_unexplained=group_unexplained, + group_not_footing=group_not_footing, last_close_at=last_close_at, initialized_at=initialized_at, last_sync_at=last_sync_at, diff --git a/robosystems_client/models/preview_consolidation_entries_response.py b/robosystems_client/models/preview_consolidation_entries_response.py index e07d059..87f31a5 100644 --- a/robosystems_client/models/preview_consolidation_entries_response.py +++ b/robosystems_client/models/preview_consolidation_entries_response.py @@ -27,8 +27,9 @@ class PreviewConsolidationEntriesResponse: period_start (datetime.date): The month's first day. period_end (datetime.date): The month's last day. consolidation_entity_id (str): The entity the entries post in. - up_to_date (bool): True when nothing is proposed, withheld or left out: the consolidation entity already holds - what the month needs. A closed month is up to date as posted; read `closed` first. + up_to_date (bool): True when nothing is withheld or left out and the consolidation entity holds what the month + needs, posted or in the month's drafts (then `proposals` lists what the drafts hold). A closed month is up to + date as posted; read `closed` first. proposals (list[ConsolidationProposal] | Unset): The entries to post; empty when the month is up to date. residuals (list[ConsolidationResidual] | Unset): Differences withheld from the proposals until explained. reconciling_items (list[ConsolidationReconcilingItem] | Unset): Intercompany figures the two sides do not agree diff --git a/tests/test_ledger_client.py b/tests/test_ledger_client.py index 1635783..d5f756d 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -306,6 +306,10 @@ def test_get_fiscal_calendar(self, mock_execute, mock_config, graph_id): "unpostedSourceEventSample": [], "unreconciledAccountCount": 0, "unreconciledAccountSample": [], + "groupOpenEntities": [], + "groupPendingAdjustments": [], + "groupUnexplained": [], + "groupNotFooting": [], "syncStaleDays": None, "lastCloseAt": None, "initializedAt": "2026-01-01T00:00:00Z", @@ -361,6 +365,10 @@ def test_get_fiscal_calendar_names_blocking_obligations( "unpostedSourceEventSample": ["evt_3", "INV-1042"], "unreconciledAccountCount": 1, "unreconciledAccountSample": ["Equipment Loan (statement): unreconciled"], + "groupOpenEntities": [], + "groupPendingAdjustments": [], + "groupUnexplained": [], + "groupNotFooting": [], "syncStaleDays": None, "lastCloseAt": None, "initializedAt": "2026-01-01T00:00:00Z", @@ -471,6 +479,20 @@ def test_close_period_can_override_the_reconciliation_gate( body = mock_op.call_args.kwargs["body"] assert body.to_dict()["allow_unreconciled_accounts"] is True + @patch("robosystems_client.clients.ledger_client.op_close_period") + def test_the_group_close_can_override_the_residual_gate( + self, mock_op, mock_config, graph_id + ): + envelope = _envelope("close-period", {"period": "2026-03", "fiscal_calendar": {}}) + mock_op.return_value = _mock_response(envelope) + client = LedgerClient(mock_config) + client.close_period( + graph_id, "2026-03", entity_id="ent_cons", allow_unexplained_residuals=True + ) + body = mock_op.call_args.kwargs["body"].to_dict() + assert body["allow_unexplained_residuals"] is True + assert body["entity_id"] == "ent_cons" + @patch("robosystems_client.clients.ledger_client.op_close_period") def test_close_period_sends_each_override_it_is_given( self, mock_op, mock_config, graph_id