lodgment-calendar 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.4
2
+ Name: lodgment-calendar
3
+ Version: 0.1.0
4
+ Summary: ATO lodgment program due dates for BAS, IAS and income tax returns, with cited rules and UNKNOWN for missing facts. Preparation aid, not a determination.
5
+ Author: Ryan Duguid
6
+ License-Expression: FSL-1.1-ALv2
7
+ Project-URL: Homepage, https://github.com/ryanduguid/australian-accounting/tree/main/packages/lodgment-calendar
8
+ Project-URL: Repository, https://github.com/ryanduguid/australian-accounting/tree/main/packages/lodgment-calendar
9
+ Project-URL: Issues, https://github.com/ryanduguid/australian-accounting/issues
10
+ Keywords: australia,ato,lodgment-program,bas,tax-agent,bas-agent,due-dates
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Financial and Insurance Industry
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
18
+ Requires-Python: >=3.14
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ License-File: NOTICE
22
+ Provides-Extra: dev
23
+ Requires-Dist: ruff==0.17.0; extra == "dev"
24
+ Requires-Dist: pyright[nodejs]==1.1.414; extra == "dev"
25
+ Requires-Dist: nodejs-wheel-binaries==24.19.0; extra == "dev"
26
+ Requires-Dist: pytest==9.1.1; extra == "dev"
27
+ Requires-Dist: coverage==7.16.2; extra == "dev"
28
+ Requires-Dist: pytest-cov==7.1.0; extra == "dev"
29
+ Requires-Dist: build==1.6.1; extra == "dev"
30
+ Requires-Dist: setuptools==84.2.1; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # Lodgment calendar
34
+
35
+ `lodgment-calendar` works out lodgement and payment due dates for BAS, IAS and income tax
36
+ returns under the ATO's 2026–27 lodgment program, for self-lodgers and for clients of
37
+ registered tax and BAS agents. Each date carries its rule, the ATO page it comes from and
38
+ the date that page was checked. A date that depends on a fact you did not supply, or that
39
+ the ATO pages leave open, comes back `UNKNOWN` with the missing condition named.
40
+
41
+ Distribution `lodgment-calendar`, import package `lodgmentcalendar`. Library only: this
42
+ component ships no command.
43
+
44
+ **Package lifecycle:** published. Install `lodgment-calendar` from PyPI with
45
+ `pip install lodgment-calendar`; see [release notes](RELEASE_NOTES.md) and
46
+ [DISCLAIMER.md](DISCLAIMER.md).
47
+
48
+ It is a preparation aid. It reads the facts you pass, contacts no service, calls no ATO
49
+ API and lodges nothing. Ryan Duguid is not a registered tax agent or BAS agent. Project
50
+ support is limited to software issues reproduced with fabricated data. Do not send
51
+ taxpayer information.
52
+
53
+ ## Example
54
+
55
+ ```python
56
+ from lodgmentcalendar import Facts, due_dates
57
+
58
+ result = due_dates(
59
+ "bas", "company", "2026-27", "bas_agent", reporting_cycle="quarterly",
60
+ facts=Facts(consolidated_head_company=False, lodges_electronically=True,
61
+ previous_statement_lodged_electronically=True,
62
+ includes_monthly_payg_obligation=False),
63
+ )
64
+ for row in result["obligations"]:
65
+ print(row["period"], row["status"], row["nominal_due_date"], row["due_date"], row["rule_id"])
66
+ ```
67
+
68
+ ```
69
+ 2026-27 Q1 DETERMINED 2026-11-25 2026-11-25 AS-Q-AGENT
70
+ 2026-27 Q2 DETERMINED 2027-02-28 2027-03-02 AS-Q-28
71
+ 2026-27 Q3 DETERMINED 2027-05-26 2027-05-26 AS-Q-AGENT
72
+ 2026-27 Q4 UNKNOWN 2027-08-25 None AS-Q-AGENT
73
+ ```
74
+
75
+ Quarter 2 is due on Sunday 28 February 2027. Monday 1 March is Labour Day in Western
76
+ Australia, which moves the date for every state, so the statement is due on 2 March.
77
+ Quarter 4 is `UNKNOWN` because the ATO marks 25 August 2027 'to be confirmed'.
78
+
79
+ ## Inputs
80
+
81
+ `due_dates(obligation, entity_type, financial_year, channel, *, reporting_cycle=None, facts=None, extra_holidays=None, holidays_complete_until=None)`
82
+
83
+ | Input | Values |
84
+ | --- | --- |
85
+ | `obligation` | `bas`, `ias` or `income_tax_return` |
86
+ | `entity_type` | `individual`, `trust`, `partnership`, `company` or `smsf` |
87
+ | `financial_year` | The year of the statement periods, or the income year a return covers, as `2026-27` |
88
+ | `channel` | `self`, `tax_agent` or `bas_agent` (a BAS agent cannot lodge an income tax return) |
89
+ | `reporting_cycle` | `monthly` or `quarterly`, for `bas` and `ias` only. A monthly `bas` reports GST monthly; other monthly activity statements are `ias`. For a consolidated head company, a quarterly `ias` is its PAYG instalment activity statement |
90
+ | `facts` | A `Facts` with the conditions below; each is `True`, `False` or `None` (not supplied) |
91
+ | `extra_holidays` | Up to 50 `date: name` holidays the ATO table does not list (see below) |
92
+ | `holidays_complete_until` | A `date` to which the ATO table and `extra_holidays` hold every holiday (see below) |
93
+
94
+ This release records statement periods in 2026-27 and income tax returns for the 2025-26
95
+ income year, which are the returns the 2026–27 program covers. Any other year returns one
96
+ `UNKNOWN` row.
97
+
98
+ The `Facts` fields are the conditions the ATO pages make the dates depend on:
99
+ `prior_returns_outstanding`, `overdue_returns_lodged_by_31_october`,
100
+ `on_agent_client_list_by_cutoff`, `large_or_medium`, `member_large_or_medium`,
101
+ `new_registrant`, `prior_year_return_taxable`, `current_year_return_taxable`,
102
+ `latest_tax_liability_20k_or_more`, `total_income_over_2m`,
103
+ `prior_year_non_taxable_or_refund`, `current_year_non_taxable_or_refund`,
104
+ `consolidated_head_company`, `first_consolidated_return`, `lodges_electronically`,
105
+ `previous_statement_lodged_electronically`, `includes_monthly_payg_obligation`,
106
+ `annual_turnover_up_to_10m` and
107
+ `deferred_gst_registered`. `FACT_MEANINGS` in `lodgmentcalendar.due_dates` gives the
108
+ ATO's wording for each. Only the facts on a date's decision path are read; a `None` on
109
+ that path makes the date `UNKNOWN`.
110
+
111
+ Invalid inputs raise `ValueError`: an unknown value, a malformed year, a cycle on an
112
+ income tax return, a BAS agent lodging an income tax return or a fact that is not
113
+ `True`, `False` or `None`.
114
+
115
+ ## Output
116
+
117
+ The result holds `obligations`, `counts` by status, `request`, `facts_used`,
118
+ `holiday_calendar`, `not_decided`, `warnings` and `source_checked`. Each obligation row has:
119
+
120
+ | Field | Meaning |
121
+ | --- | --- |
122
+ | `kind` | `lodgment`, `payment` (only where the payment date differs) or `concession` |
123
+ | `status` | `DETERMINED` or `UNKNOWN` |
124
+ | `due_date` | The date after the next-business-day roll; `null` when `UNKNOWN` |
125
+ | `nominal_due_date` | The date the rule names, before the roll, where the rule names one |
126
+ | `rule_id`, `summary` | The rule applied, in plain English |
127
+ | `source_url`, `source_provision`, `source_checked` | The ATO page, the table or heading on it, and the review date |
128
+ | `holiday_roll` | The weekend days and holidays passed over, with the holiday table's URL |
129
+ | `missing` | Each fact not supplied or question the ATO pages leave open |
130
+ | `payment` | On lodgement rows: `with_lodgment`, `separate`, `none` or `unknown` |
131
+
132
+ ## Weekends and public holidays
133
+
134
+ The holiday calendar is the ATO's own: Tables 1 and 2 of
135
+ [Lodgment and payment dates on weekends or public holidays](https://www.ato.gov.au/tax-and-super-professionals/for-tax-professionals/prepare-and-lodge/registered-agent-lodgment-program/lodgment-and-payment-dates-on-weekends-or-public-holidays),
136
+ checked on 20 October 2026 and again on 28 October 2026. Under that page, a date that
137
+ falls on a Saturday, a Sunday or a public holiday for the whole of any state or territory
138
+ moves to the next business day for taxpayers in every state. The tables cover 1 July 2026 to 30 June 2027. A later date
139
+ that is not a weekend cannot be shown to be a business day, so it is `UNKNOWN` with its
140
+ nominal date kept. A holiday missing from the tables can only move a date later.
141
+
142
+ To settle those dates, pass the missing holidays from an official source as
143
+ `extra_holidays` and the last date your list is complete to as
144
+ `holidays_complete_until`. Holidays alone never extend coverage, because a list holding
145
+ one 2027 holiday is not a list that has 2027 covered. Each one must be a weekday the ATO
146
+ table does not already list, and shows in `holiday_roll` and `holiday_calendar` marked
147
+ '(caller-supplied)'.
148
+
149
+ ```python
150
+ from datetime import date
151
+
152
+ result = due_dates("bas", "trust", "2026-27", "self", reporting_cycle="quarterly",
153
+ facts=Facts(lodges_electronically=True),
154
+ holidays_complete_until=date(2027, 8, 31))
155
+ print(result["obligations"][3]["due_date"])
156
+ ```
157
+
158
+ ```
159
+ 2027-08-11
160
+ ```
161
+
162
+ This assumes no whole-of-state holiday falls between 1 July and 31 August 2027 beyond
163
+ those you pass; the calendar takes that statement as given.
164
+
165
+ ## Scope
166
+
167
+ Modelled rules, open questions and every source are listed in the
168
+ [primary-source review](docs/primary-source-review-2026-10-20.md). The calendar does not
169
+ model substituted accounting periods, companies that are not full self-assessment
170
+ taxpayers, APRA-regulated funds, annual GST returns, instalment notices, FBT or other
171
+ obligation types. It does not check whether the facts you pass are true, apply a
172
+ client-specific date the ATO has set, or decide whether anything was lodged or paid on
173
+ time.
174
+
175
+ ## Licence
176
+
177
+ Licensed under the Functional Source License, Version 1.1, ALv2 Future License (FSL-1.1-ALv2). You may use, modify and redistribute it for any purpose except a Competing Use as defined in LICENSE (making it available to others in a competing commercial product or service), and each version becomes available under Apache-2.0 two years after it is made available. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
@@ -0,0 +1,14 @@
1
+ lodgment_calendar-0.1.0.dist-info/licenses/LICENSE,sha256=6AxWPlVU_Ce-vmqUDCqY10KJKh33AR3QTazuNEN7uVw,3743
2
+ lodgment_calendar-0.1.0.dist-info/licenses/NOTICE,sha256=ptxdTJjsS5yPxA8ju2a_0ST7Nxa5m_rf2dxfLhAa6Hw,866
3
+ lodgmentcalendar/__init__.py,sha256=3bTglzXSYKD0PQWJotU1ODC9zqwlATVIpyk_TsaXA4Q,326
4
+ lodgmentcalendar/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
5
+ lodgmentcalendar/due_dates.py,sha256=JoveKeNFkTDNt8F8IEi0gGZae4ft_4439l02DXLJ0rY,12023
6
+ lodgmentcalendar/holidays.py,sha256=69zzh314Q1dMO-1UzQ1QhfHTHinHNbauN6TNZRtRNU8,6389
7
+ lodgmentcalendar/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ lodgmentcalendar/returns.py,sha256=HVt6oHTp_k6e5Uduhpwhsg8LsbK_kDtFa-yVtUfIQ7o,17439
9
+ lodgmentcalendar/rules.py,sha256=XUfbMZkDFo8bmlYiiGs2IDKUqnsdaAFtPJK1LbJAD8M,10633
10
+ lodgmentcalendar/statements.py,sha256=qeHB9hVlcAnTWtAAKPPCXw3wwEaz0mOMzquNxIizCIE,7517
11
+ lodgment_calendar-0.1.0.dist-info/METADATA,sha256=5GUGGxkx-M486srDOGUQjuhr2ElQjHEHWlod2gtkgv8,9476
12
+ lodgment_calendar-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
13
+ lodgment_calendar-0.1.0.dist-info/top_level.txt,sha256=YCVyBC-sGD3tftQbTV78AUZbDbrrUYtQJ3w1urQPg_0,17
14
+ lodgment_calendar-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,105 @@
1
+ # Functional Source License, Version 1.1, ALv2 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-ALv2
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Ryan Duguid
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the Apache License, Version 2.0 that is effective on the second anniversary of
91
+ the date we make the Software available. On or after that date, you may use the
92
+ Software under the Apache License, Version 2.0, in which case the following
93
+ will apply:
94
+
95
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
96
+ this file except in compliance with the License.
97
+
98
+ You may obtain a copy of the License at
99
+
100
+ http://www.apache.org/licenses/LICENSE-2.0
101
+
102
+ Unless required by applicable law or agreed to in writing, software distributed
103
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
+ specific language governing permissions and limitations under the License.
@@ -0,0 +1,20 @@
1
+ lodgment-calendar
2
+ Copyright 2026 Ryan Duguid (https://github.com/ryanduguid)
3
+
4
+ This software is licensed under the Functional Source License,
5
+ Version 1.1, ALv2 Future License (FSL-1.1-ALv2); see LICENSE.
6
+
7
+ For each version licensed under FSL-1.1-ALv2, an additional licence under the
8
+ Apache License, Version 2.0 becomes effective on the second anniversary of the
9
+ date that version was made available.
10
+
11
+ Versions made available before this change remain available under the
12
+ licences they were released with.
13
+
14
+ Redistribution under FSL-1.1-ALv2 requires including its terms or a link to
15
+ them and keeping the copyright notices supplied with the software.
16
+ Redistribution under another applicable licence follows that licence's
17
+ conditions.
18
+
19
+ This product includes work by Ryan Duguid
20
+ (https://github.com/ryanduguid/australian-accounting/tree/main/packages/lodgment-calendar).
@@ -0,0 +1 @@
1
+ lodgmentcalendar
@@ -0,0 +1,11 @@
1
+ """Lodgment and payment due dates under the ATO's 2026–27 lodgment program.
2
+
3
+ A preparation aid, not a determination: it lodges nothing and contacts no service.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from ._version import __version__
9
+ from .due_dates import Facts, due_dates
10
+
11
+ __all__ = ["Facts", "__version__", "due_dates"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,259 @@
1
+ """Lodgment and payment due dates under the ATO's 2026–27 lodgment program.
2
+
3
+ The calendar reads the facts the caller supplies, applies the dates the ATO
4
+ publishes for self-lodgers and for registered tax and BAS agents, and moves each
5
+ date to the next business day under the program's public holiday rule. A date
6
+ that depends on a fact the caller did not supply, or that the ATO pages leave
7
+ open, is UNKNOWN and names what is missing. It contacts no service and lodges
8
+ nothing.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from collections.abc import Mapping
15
+ from dataclasses import fields
16
+ from datetime import date
17
+ from typing import Any
18
+
19
+ from ._version import __version__
20
+ from .holidays import COVERS_FROM, HOLIDAY_PROVISION, HOLIDAY_SOURCE, calendar, roll
21
+ from .returns import return_rule
22
+ from .rules import FACT_MEANINGS, SOURCES, Facts, Rule
23
+ from .statements import statement_rules
24
+
25
+ SOURCE_CHECKED = "2026-10-28"
26
+ PROGRAM_YEAR = "2026-27"
27
+ # The program dates cover activity statements for periods in 2026–27 and income
28
+ # tax returns for the 2025–26 income year (the 2026 return).
29
+ ACTIVITY_YEAR = "2026-27"
30
+ RETURN_YEAR = "2025-26"
31
+
32
+ DETERMINED, UNKNOWN = "DETERMINED", "UNKNOWN"
33
+ OBLIGATIONS = ("bas", "ias", "income_tax_return")
34
+ ENTITY_TYPES = ("individual", "trust", "partnership", "company", "smsf")
35
+ CHANNELS = ("self", "tax_agent", "bas_agent")
36
+ CYCLES = ("monthly", "quarterly")
37
+
38
+ NOT_DECIDED = [
39
+ "Dates the ATO sets for one client override these: a notice requiring earlier "
40
+ "lodgement, a prosecution letter, a deferral, an advised first-year date for a new "
41
+ "SMSF, or the due date shown on the activity statement or in Online services for "
42
+ "agents.",
43
+ "Whether a fact supplied is true. Large or medium status, taxable status, income and "
44
+ "liability thresholds, registration and lodgement history are taken as given.",
45
+ "Substituted accounting periods, companies that are not full self-assessment "
46
+ "taxpayers, APRA-regulated funds, members exiting a consolidated group, annual GST "
47
+ "returns, instalment notices (forms R, S, T and N), PAYG instalments paid in 2 "
48
+ "instalments or annually, deferred BAS payers (who pay monthly PAYG instalments on the "
49
+ "28th) and every other obligation type.",
50
+ "Whether a lodgement or payment was on time, and any penalty or general interest charge.",
51
+ "Disaster or other deferrals, and holidays proclaimed after the ATO published its "
52
+ "2026–27 table.",
53
+ ]
54
+
55
+ WARNINGS = [
56
+ "The ATO says its list of lodgment program due dates is a guide only. Confirm each "
57
+ "client's dates in Online services for agents or the practitioner lodgment service.",
58
+ "Retain the status and missing conditions of every UNKNOWN row; do not replace one "
59
+ "with a nominal date.",
60
+ ]
61
+
62
+ _YEAR = re.compile(r"([0-9]{4})-([0-9]{2})")
63
+
64
+
65
+ def due_dates(
66
+ obligation: str,
67
+ entity_type: str,
68
+ financial_year: str,
69
+ channel: str,
70
+ *,
71
+ reporting_cycle: str | None = None,
72
+ facts: Facts | None = None,
73
+ extra_holidays: Mapping[date, str] | None = None,
74
+ holidays_complete_until: date | None = None,
75
+ ) -> dict[str, Any]:
76
+ """Return each lodgement, payment and concession date with its rule and status.
77
+
78
+ ``financial_year`` is the year of the periods (activity statements) or the income
79
+ year the return covers (income tax returns), in the form 2026-27.
80
+
81
+ ``extra_holidays`` adds public holidays for the whole of a state or territory that
82
+ the ATO table does not list, by date and name. ``holidays_complete_until`` states
83
+ that the ATO table and those holidays hold every such holiday to that date, which
84
+ lets dates after 30 June 2027 be settled.
85
+ """
86
+ facts = Facts() if facts is None else facts
87
+ _check(obligation, entity_type, financial_year, channel, reporting_cycle, facts)
88
+ cal = calendar(extra_holidays, holidays_complete_until)
89
+ if obligation == "income_tax_return":
90
+ rows = _return_rows(entity_type, financial_year, channel, facts, cal)
91
+ else:
92
+ rows = _statement_rows(obligation, entity_type, financial_year, channel,
93
+ reporting_cycle, facts, cal)
94
+ return {
95
+ "ok": True,
96
+ "engine": "lodgment-calendar",
97
+ "engine_version": __version__,
98
+ "calendar": "lodgment_due_dates",
99
+ "program_year": PROGRAM_YEAR,
100
+ "request": {"obligation": obligation, "entity_type": entity_type,
101
+ "financial_year": financial_year, "channel": channel,
102
+ "reporting_cycle": reporting_cycle},
103
+ "facts_used": {field.name: getattr(facts, field.name) for field in fields(facts)},
104
+ "obligations": rows,
105
+ "counts": {status: sum(1 for row in rows if row["status"] == status)
106
+ for status in (DETERMINED, UNKNOWN)},
107
+ "holiday_calendar": _holiday_calendar(cal, holidays_complete_until),
108
+ "not_decided": NOT_DECIDED,
109
+ "warnings": WARNINGS,
110
+ "source_checked": SOURCE_CHECKED,
111
+ }
112
+
113
+
114
+ def _return_rows(entity_type: str, financial_year: str, channel: str, facts: Facts,
115
+ cal: Calendar) -> list[dict[str, Any]]:
116
+ label = f"Income tax return {financial_year}"
117
+ if financial_year != RETURN_YEAR:
118
+ return _rows(label, financial_year, _not_recorded(financial_year, RETURN_YEAR), [],
119
+ cal=cal)
120
+ rule, missing, concessions = return_rule(entity_type, channel, facts)
121
+ # A partnership pays no income tax, whatever its lodgement date turns out to be.
122
+ rows = _rows(label, financial_year, rule, missing, family=_family(entity_type, channel),
123
+ pending_payment="none" if entity_type == "partnership" else "unknown",
124
+ cal=cal)
125
+ for concession, needed in concessions:
126
+ rows.append(_row(label, financial_year, "concession", concession, needed, cal=cal))
127
+ return rows
128
+
129
+
130
+ def _statement_rows(obligation: str, entity_type: str, financial_year: str, channel: str,
131
+ reporting_cycle: str | None, facts: Facts,
132
+ cal: Calendar) -> list[dict[str, Any]]:
133
+ if financial_year != ACTIVITY_YEAR:
134
+ return _rows(f"{obligation.upper()} {financial_year}", financial_year,
135
+ _not_recorded(financial_year, ACTIVITY_YEAR), [], cal=cal)
136
+ rows: list[dict[str, Any]] = []
137
+ for period, label, rule, missing in statement_rules(
138
+ obligation, entity_type, channel, reporting_cycle, facts):
139
+ rows += _rows(label, period, rule, missing, family=_family(obligation, channel),
140
+ cal=cal)
141
+ return rows
142
+
143
+
144
+ def _holiday_calendar(cal: Calendar, complete_until: date | None) -> dict[str, Any]:
145
+ return {
146
+ "definition": "Saturdays, Sundays and days that are a public holiday for the whole "
147
+ "of any state or territory move a due date to the next business day, for taxpayers "
148
+ "in every state.",
149
+ "source_url": HOLIDAY_SOURCE,
150
+ "source_provision": HOLIDAY_PROVISION,
151
+ "covers_from": COVERS_FROM.isoformat(),
152
+ "covers_to": cal[1].isoformat(),
153
+ "caller_holidays": {day.isoformat(): name for day, name in sorted(cal[0].items())
154
+ if name.endswith(" (caller-supplied)")},
155
+ "caller_complete_until": None if complete_until is None else complete_until.isoformat(),
156
+ }
157
+
158
+
159
+ def _check(obligation: str, entity_type: str, financial_year: object, channel: str,
160
+ reporting_cycle: str | None, facts: object) -> None:
161
+ for name, value, allowed in (("obligation", obligation, OBLIGATIONS),
162
+ ("entity_type", entity_type, ENTITY_TYPES),
163
+ ("channel", channel, CHANNELS)):
164
+ if value not in allowed:
165
+ raise ValueError(f"{name} must be one of {', '.join(allowed)}; got {value!r}")
166
+ match = _YEAR.fullmatch(financial_year) if isinstance(financial_year, str) else None
167
+ if match is None or int(match[2]) != (int(match[1]) + 1) % 100:
168
+ raise ValueError(f"financial_year must look like 2026-27; got {financial_year!r}")
169
+ if obligation == "income_tax_return":
170
+ if reporting_cycle is not None:
171
+ raise ValueError("reporting_cycle applies only to bas and ias")
172
+ if channel == "bas_agent":
173
+ raise ValueError("a BAS agent cannot lodge an income tax return; use tax_agent")
174
+ elif reporting_cycle not in CYCLES:
175
+ raise ValueError(f"reporting_cycle must be one of {', '.join(CYCLES)} for "
176
+ f"{obligation}; got {reporting_cycle!r}")
177
+ if not isinstance(facts, Facts):
178
+ raise ValueError("facts must be a Facts instance")
179
+ for field in fields(facts):
180
+ value = getattr(facts, field.name)
181
+ # Identity, not isinstance: an object can claim bool as its class.
182
+ if value is not None and value is not True and value is not False:
183
+ raise ValueError(f"facts.{field.name} must be true, false or null; got {value!r}")
184
+
185
+
186
+ def _family(kind: str, channel: str) -> str:
187
+ return f"{kind.upper()}-{'SELF' if channel == 'self' else 'AGENT'}"
188
+
189
+
190
+ def _not_recorded(year: str, recorded: str) -> Rule:
191
+ return Rule(
192
+ "YEAR-NOT-RECORDED",
193
+ f"This release records the {PROGRAM_YEAR} lodgment program, which covers {recorded} "
194
+ f"for this obligation; {year} is not recorded.",
195
+ "about",
196
+ unresolved=f"rules for {year}: this release records only {recorded} for this "
197
+ "obligation",
198
+ payment="unknown",
199
+ )
200
+
201
+
202
+ Calendar = tuple[dict[date, str], date]
203
+
204
+
205
+ def _rows(label: str, period: str, rule: Rule | None, missing: list[str], *, cal: Calendar,
206
+ family: str = "", pending_payment: str = "unknown") -> list[dict[str, Any]]:
207
+ if rule is None or missing:
208
+ pending = Rule(family, "The date depends on facts not supplied: "
209
+ + "; ".join(FACT_MEANINGS[name] for name in missing) + ".", "about")
210
+ return [_row(label, period, "lodgment", pending, missing, payment=pending_payment,
211
+ cal=cal)]
212
+ payment = rule.payment
213
+ rows = [_row(label, period, "lodgment", rule, [],
214
+ payment=payment if isinstance(payment, str) else "separate", cal=cal)]
215
+ if isinstance(payment, Rule):
216
+ rows.append(_row(label, period, "payment", payment, [], cal=cal))
217
+ return rows
218
+
219
+
220
+ def _row(label: str, period: str, kind: str, rule: Rule, missing: list[str], *,
221
+ cal: Calendar, payment: str | None = None) -> dict[str, Any]:
222
+ url, provision = SOURCES[rule.source]
223
+ needed = [f"{name}: {FACT_MEANINGS[name]}" for name in missing]
224
+ due: date | None = None
225
+ holiday_roll = None
226
+ if rule.unresolved is not None:
227
+ needed.append(rule.unresolved)
228
+ elif rule.nominal is not None and not needed:
229
+ due, skipped, open_question = roll(rule.nominal, *cal)
230
+ if skipped or due is None:
231
+ holiday_roll = {"skipped": skipped, "source_url": HOLIDAY_SOURCE}
232
+ if open_question is not None:
233
+ needed.append(open_question)
234
+ elif kind == "concession" and skipped:
235
+ needed.append(
236
+ f"whether the next-business-day rule extends this concession: "
237
+ f"{rule.nominal.isoformat()} is not a business day, and the ATO pages call "
238
+ "it both a concessional due date and not a lodgment due date")
239
+ due = None
240
+ if needed:
241
+ due = None
242
+ row: dict[str, Any] = {
243
+ "obligation": label,
244
+ "period": period,
245
+ "kind": kind,
246
+ "status": UNKNOWN if needed else DETERMINED,
247
+ "due_date": None if due is None else due.isoformat(),
248
+ "nominal_due_date": None if rule.nominal is None else rule.nominal.isoformat(),
249
+ "rule_id": rule.id,
250
+ "summary": rule.summary,
251
+ "source_url": url,
252
+ "source_provision": provision,
253
+ "source_checked": SOURCE_CHECKED,
254
+ "holiday_roll": holiday_roll,
255
+ "missing": needed,
256
+ }
257
+ if kind == "lodgment":
258
+ row["payment"] = payment
259
+ return row