Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions robosystems_client/clients/ledger_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 4 additions & 0 deletions robosystems_client/graphql/generated/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,10 @@
unpostedSourceEventSample
unreconciledAccountCount
unreconciledAccountSample
groupOpenEntities
groupPendingAdjustments
groupUnexplained
groupNotFooting
syncStaleDays
lastCloseAt
initializedAt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
22 changes: 21 additions & 1 deletion robosystems_client/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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!]!

Expand Down Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions robosystems_client/models/backfill_plan_history_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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,
)
Expand Down
12 changes: 12 additions & 0 deletions robosystems_client/models/close_period_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]:
Expand Down Expand Up @@ -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(
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
56 changes: 55 additions & 1 deletion robosystems_client/models/fiscal_calendar_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions tests/test_ledger_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
Loading