macrs 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.
macrs/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ """macrs — IRS Publication 946 MACRS depreciation schedules.
2
+
3
+ Deterministic, decimal-precise depreciation schedules for US tax depreciation
4
+ under the Modified Accelerated Cost Recovery System. Rates come from IRS
5
+ Publication 946 Appendix A tables.
6
+
7
+ Not tax advice. Verify recovery period, convention, section 179, and bonus
8
+ depreciation eligibility against current law and the current Publication 946
9
+ before filing.
10
+ """
11
+
12
+ from .models import AssetInput, DepreciationSchedule, ScheduleLine, money
13
+ from .schedule import (
14
+ asset_from_class,
15
+ build_schedule,
16
+ lookup_asset_class,
17
+ mid_quarter_required,
18
+ month_to_quarter,
19
+ schedule_for_class,
20
+ )
21
+ from .tables import ASSET_CLASSES, TABLE_SOURCE, TABLE_URL
22
+
23
+ __version__ = "0.1.0"
24
+
25
+ __all__ = [
26
+ "ASSET_CLASSES",
27
+ "AssetInput",
28
+ "DepreciationSchedule",
29
+ "ScheduleLine",
30
+ "TABLE_SOURCE",
31
+ "TABLE_URL",
32
+ "asset_from_class",
33
+ "build_schedule",
34
+ "lookup_asset_class",
35
+ "mid_quarter_required",
36
+ "money",
37
+ "month_to_quarter",
38
+ "schedule_for_class",
39
+ "__version__",
40
+ ]
macrs/cli.py ADDED
@@ -0,0 +1,284 @@
1
+ """Command-line interface for MACRS depreciation schedules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import csv
7
+ import json
8
+ import sys
9
+ from decimal import Decimal
10
+ from typing import Optional, Sequence, TextIO
11
+
12
+ from . import __version__
13
+ from .models import AssetInput, money
14
+ from .schedule import (
15
+ build_schedule,
16
+ lookup_asset_class,
17
+ mid_quarter_required,
18
+ schedule_for_class,
19
+ )
20
+ from .tables import ASSET_CLASSES, GDS_PERSONAL_PERIODS, TABLE_SOURCE, TABLE_URL
21
+
22
+
23
+ def _print_schedule(sched, out: Optional[TextIO] = None, *, verbose: bool = False) -> None:
24
+ if out is None:
25
+ out = sys.stdout
26
+ a = sched.asset
27
+ print(f"# MACRS depreciation schedule", file=out)
28
+ if a.label:
29
+ print(f"# Asset: {a.label}", file=out)
30
+ if a.description:
31
+ print(f"# Class: {a.description}", file=out)
32
+ print(f"# Cost: {a.cost}", file=out)
33
+ print(f"# Recovery: {a.recovery_years}-year Convention: {sched.convention}", file=out)
34
+ print(f"# Table: {sched.table_id}", file=out)
35
+ print(f"# Source: {sched.table_source}", file=out)
36
+ print(f"# §179: {sched.section_179_deduction} Bonus: {sched.bonus_deduction}", file=out)
37
+ print(f"# MACRS basis: {sched.macrs_basis}", file=out)
38
+ print(f"# Year-1 total (§179+bonus+MACRS): {sched.total_first_year}", file=out)
39
+ print(file=out)
40
+ headers = [
41
+ "year",
42
+ "calendar",
43
+ "rate_%",
44
+ "depreciation",
45
+ "cumulative",
46
+ "remaining",
47
+ ]
48
+ print("\t".join(headers), file=out)
49
+ for line in sched.lines:
50
+ print(
51
+ "\t".join(
52
+ [
53
+ str(line.recovery_year),
54
+ str(line.calendar_year or ""),
55
+ str(line.rate_percent),
56
+ str(line.depreciation),
57
+ str(line.cumulative_depreciation),
58
+ str(line.remaining_basis),
59
+ ]
60
+ ),
61
+ file=out,
62
+ )
63
+ print(file=out)
64
+ print(f"# Total MACRS depreciation: {sched.total_macrs_depreciation}", file=out)
65
+ if sched.warnings:
66
+ print("# Warnings:", file=out)
67
+ for w in sched.warnings:
68
+ print(f"# - {w}", file=out)
69
+ if verbose:
70
+ print("# Audit trail:", file=out)
71
+ for r in sched.reasons:
72
+ print(f"# - {r}", file=out)
73
+ print(
74
+ "# Disclaimer: Not tax advice. Verify against current IRS Publication 946.",
75
+ file=out,
76
+ )
77
+
78
+
79
+ def cmd_schedule(args: argparse.Namespace) -> int:
80
+ if args.asset_class:
81
+ sched = schedule_for_class(
82
+ args.cost,
83
+ args.asset_class,
84
+ convention=args.convention,
85
+ placed_in_service_month=args.month,
86
+ placed_in_service_year=args.year,
87
+ section_179=args.section_179,
88
+ bonus_percent=args.bonus,
89
+ business_use_percent=args.business_use,
90
+ label=args.label,
91
+ )
92
+ else:
93
+ asset = AssetInput(
94
+ cost=money(args.cost),
95
+ recovery_years=args.years,
96
+ convention=args.convention,
97
+ placed_in_service_month=args.month,
98
+ placed_in_service_year=args.year,
99
+ quarter=args.quarter,
100
+ section_179=money(args.section_179),
101
+ bonus_percent=Decimal(str(args.bonus)),
102
+ business_use_percent=Decimal(str(args.business_use)),
103
+ label=args.label,
104
+ )
105
+ sched = build_schedule(asset)
106
+
107
+ if args.format == "json":
108
+ payload = {
109
+ "cost": str(sched.asset.cost),
110
+ "recovery_years": sched.asset.recovery_years,
111
+ "convention": sched.convention,
112
+ "table_id": sched.table_id,
113
+ "table_source": sched.table_source,
114
+ "section_179": str(sched.section_179_deduction),
115
+ "bonus": str(sched.bonus_deduction),
116
+ "macrs_basis": str(sched.macrs_basis),
117
+ "total_first_year": str(sched.total_first_year),
118
+ "total_macrs": str(sched.total_macrs_depreciation),
119
+ "lines": sched.to_rows(),
120
+ "reasons": sched.reasons if args.verbose else [],
121
+ "warnings": sched.warnings,
122
+ "disclaimer": "Not tax advice. Verify against current IRS Publication 946.",
123
+ }
124
+ json.dump(payload, sys.stdout, indent=2)
125
+ print()
126
+ elif args.format == "csv":
127
+ w = csv.DictWriter(
128
+ sys.stdout,
129
+ fieldnames=[
130
+ "recovery_year",
131
+ "calendar_year",
132
+ "rate_percent",
133
+ "depreciation",
134
+ "cumulative_depreciation",
135
+ "remaining_basis",
136
+ "notes",
137
+ ],
138
+ )
139
+ w.writeheader()
140
+ for row in sched.to_rows():
141
+ w.writerow(row)
142
+ else:
143
+ _print_schedule(sched, verbose=args.verbose)
144
+ return 0
145
+
146
+
147
+ def cmd_classes(_: argparse.Namespace) -> int:
148
+ print(f"# Common asset classes (from {TABLE_SOURCE})")
149
+ print("key\tgds_years\tads_years\tkind\tdescription")
150
+ for key, meta in sorted(ASSET_CLASSES.items()):
151
+ print(
152
+ f"{key}\t{meta['gds_years']}\t{meta['ads_years']}\t"
153
+ f"{meta['kind']}\t{meta['description']}"
154
+ )
155
+ return 0
156
+
157
+
158
+ def cmd_mq_test(args: argparse.Namespace) -> int:
159
+ qs = [args.q1, args.q2, args.q3, args.q4]
160
+ required, ratio, reasons = mid_quarter_required(qs)
161
+ if args.format == "json":
162
+ json.dump(
163
+ {
164
+ "mid_quarter_required": required,
165
+ "q4_ratio": str(ratio),
166
+ "quarters": [str(money(q)) for q in qs],
167
+ "reasons": reasons,
168
+ },
169
+ sys.stdout,
170
+ indent=2,
171
+ )
172
+ print()
173
+ else:
174
+ print(f"Mid-quarter required: {required}")
175
+ print(f"Q4 ratio: {ratio * 100}%")
176
+ for r in reasons:
177
+ print(f" - {r}")
178
+ return 0
179
+
180
+
181
+ def cmd_rates(args: argparse.Namespace) -> int:
182
+ from .tables import personal_gds_rates
183
+
184
+ rates = personal_gds_rates(
185
+ args.years,
186
+ convention=args.convention,
187
+ quarter=args.quarter,
188
+ )
189
+ for i, r in enumerate(rates, 1):
190
+ print(f"{i}\t{r}")
191
+ return 0
192
+
193
+
194
+ def build_parser() -> argparse.ArgumentParser:
195
+ p = argparse.ArgumentParser(
196
+ prog="macrs",
197
+ description=(
198
+ "MACRS depreciation schedules from IRS Publication 946 percentage tables. "
199
+ "Not tax advice."
200
+ ),
201
+ )
202
+ p.add_argument("--version", action="version", version=f"macrs {__version__}")
203
+ sub = p.add_subparsers(dest="command", required=True)
204
+
205
+ s = sub.add_parser("schedule", help="Build a depreciation schedule")
206
+ s.add_argument("--cost", type=str, required=True, help="Asset cost / basis")
207
+ s.add_argument(
208
+ "--years",
209
+ type=int,
210
+ default=5,
211
+ help="Recovery period: 3,5,7,10,15,20 (personal); 27 residential; 39 nonresidential",
212
+ )
213
+ s.add_argument(
214
+ "--convention",
215
+ default="half_year",
216
+ choices=["half_year", "mid_quarter", "mid_month"],
217
+ help="Depreciation convention",
218
+ )
219
+ s.add_argument("--month", type=int, default=1, help="Month placed in service (1-12)")
220
+ s.add_argument("--year", type=int, default=None, help="Calendar year placed in service")
221
+ s.add_argument("--quarter", type=int, default=None, help="Quarter 1-4 (mid_quarter)")
222
+ s.add_argument("--section-179", type=str, default="0", dest="section_179")
223
+ s.add_argument("--bonus", type=str, default="0", help="Bonus depreciation percent 0-100")
224
+ s.add_argument(
225
+ "--business-use",
226
+ type=str,
227
+ default="100",
228
+ dest="business_use",
229
+ help="Business-use percent 0-100",
230
+ )
231
+ s.add_argument(
232
+ "--class",
233
+ dest="asset_class",
234
+ default=None,
235
+ help=f"Named asset class: {', '.join(sorted(ASSET_CLASSES))}",
236
+ )
237
+ s.add_argument("--label", default=None)
238
+ s.add_argument(
239
+ "--format",
240
+ choices=["text", "json", "csv"],
241
+ default="text",
242
+ )
243
+ s.add_argument("-v", "--verbose", action="store_true", help="Show audit trail")
244
+ s.set_defaults(func=cmd_schedule)
245
+
246
+ c = sub.add_parser("classes", help="List common asset class shortcuts")
247
+ c.set_defaults(func=cmd_classes)
248
+
249
+ m = sub.add_parser(
250
+ "mq-test",
251
+ help="Apply the >40%% fourth-quarter mid-quarter convention test",
252
+ )
253
+ m.add_argument("--q1", type=str, required=True)
254
+ m.add_argument("--q2", type=str, required=True)
255
+ m.add_argument("--q3", type=str, required=True)
256
+ m.add_argument("--q4", type=str, required=True)
257
+ m.add_argument("--format", choices=["text", "json"], default="text")
258
+ m.set_defaults(func=cmd_mq_test)
259
+
260
+ r = sub.add_parser("rates", help="Print raw IRS percentage rates for a class/convention")
261
+ r.add_argument("--years", type=int, required=True, choices=list(GDS_PERSONAL_PERIODS))
262
+ r.add_argument(
263
+ "--convention",
264
+ default="half_year",
265
+ choices=["half_year", "mid_quarter"],
266
+ )
267
+ r.add_argument("--quarter", type=int, default=None)
268
+ r.set_defaults(func=cmd_rates)
269
+
270
+ return p
271
+
272
+
273
+ def main(argv: Optional[Sequence[str]] = None) -> int:
274
+ parser = build_parser()
275
+ args = parser.parse_args(argv)
276
+ try:
277
+ return args.func(args)
278
+ except (ValueError, KeyError) as e:
279
+ print(f"error: {e}", file=sys.stderr)
280
+ return 2
281
+
282
+
283
+ if __name__ == "__main__":
284
+ raise SystemExit(main())
macrs/models.py ADDED
@@ -0,0 +1,105 @@
1
+ """Domain models for MACRS depreciation schedules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from decimal import Decimal
7
+ from typing import List, Optional
8
+
9
+
10
+ def money(value: Decimal | int | str | float) -> Decimal:
11
+ """Quantize to cents using banker's rounding default ROUND_HALF_EVEN via Decimal."""
12
+ d = value if isinstance(value, Decimal) else Decimal(str(value))
13
+ return d.quantize(Decimal("0.01"))
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class AssetInput:
18
+ """Inputs for a single depreciable asset schedule."""
19
+
20
+ cost: Decimal
21
+ recovery_years: int
22
+ """GDS recovery period: 3,5,7,10,15,20 for personal; 27 for residential; 39 for nonresidential."""
23
+
24
+ convention: str = "half_year"
25
+ """half_year | mid_quarter | mid_month"""
26
+
27
+ placed_in_service_month: int = 1
28
+ """Calendar month (1–12) property was placed in service. Required for mid_month / MQ quarter."""
29
+
30
+ placed_in_service_year: Optional[int] = None
31
+ """Optional calendar year for schedule labeling."""
32
+
33
+ quarter: Optional[int] = None
34
+ """1–4; if omitted and convention is mid_quarter, derived from placed_in_service_month."""
35
+
36
+ section_179: Decimal = Decimal("0")
37
+ """Section 179 expense deducted in year 1 (reduces depreciable basis)."""
38
+
39
+ bonus_percent: Decimal = Decimal("0")
40
+ """Special depreciation allowance % of remaining basis after §179 (0–100)."""
41
+
42
+ business_use_percent: Decimal = Decimal("100")
43
+ """Business/investment use percentage (0–100)."""
44
+
45
+ asset_class: Optional[str] = None
46
+ description: Optional[str] = None
47
+ label: Optional[str] = None
48
+
49
+ def __post_init__(self) -> None:
50
+ object.__setattr__(self, "cost", money(self.cost))
51
+ object.__setattr__(self, "section_179", money(self.section_179))
52
+ object.__setattr__(
53
+ self, "bonus_percent", Decimal(str(self.bonus_percent))
54
+ )
55
+ object.__setattr__(
56
+ self, "business_use_percent", Decimal(str(self.business_use_percent))
57
+ )
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class ScheduleLine:
62
+ """One recovery year of the depreciation schedule."""
63
+
64
+ recovery_year: int
65
+ calendar_year: Optional[int]
66
+ rate_percent: Decimal
67
+ depreciation: Decimal
68
+ cumulative_depreciation: Decimal
69
+ remaining_basis: Decimal
70
+ notes: str = ""
71
+
72
+
73
+ @dataclass
74
+ class DepreciationSchedule:
75
+ """Full MACRS schedule with audit metadata."""
76
+
77
+ asset: AssetInput
78
+ depreciable_basis: Decimal
79
+ section_179_deduction: Decimal
80
+ bonus_deduction: Decimal
81
+ macrs_basis: Decimal
82
+ convention: str
83
+ table_id: str
84
+ table_source: str
85
+ lines: List[ScheduleLine] = field(default_factory=list)
86
+ total_macrs_depreciation: Decimal = Decimal("0")
87
+ total_first_year: Decimal = Decimal("0")
88
+ warnings: List[str] = field(default_factory=list)
89
+ reasons: List[str] = field(default_factory=list)
90
+
91
+ def to_rows(self) -> List[dict]:
92
+ rows = []
93
+ for line in self.lines:
94
+ rows.append(
95
+ {
96
+ "recovery_year": line.recovery_year,
97
+ "calendar_year": line.calendar_year,
98
+ "rate_percent": str(line.rate_percent),
99
+ "depreciation": str(line.depreciation),
100
+ "cumulative_depreciation": str(line.cumulative_depreciation),
101
+ "remaining_basis": str(line.remaining_basis),
102
+ "notes": line.notes,
103
+ }
104
+ )
105
+ return rows