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 +40 -0
- macrs/cli.py +284 -0
- macrs/models.py +105 -0
- macrs/schedule.py +279 -0
- macrs/tables.py +565 -0
- macrs-0.1.0.dist-info/METADATA +138 -0
- macrs-0.1.0.dist-info/RECORD +11 -0
- macrs-0.1.0.dist-info/WHEEL +5 -0
- macrs-0.1.0.dist-info/entry_points.txt +2 -0
- macrs-0.1.0.dist-info/licenses/LICENSE +21 -0
- macrs-0.1.0.dist-info/top_level.txt +1 -0
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
|