Implement capital loss carryover on Schedule D.
[ustaxlib.git] / src / fed2019 / ScheduleD.ts
1 // Copyright 2020 Blue Static <https://www.bluestatic.org>
2 // This program is free software licensed under the GNU General Public License,
3 // version 3.0. The full text of the license can be found in LICENSE.txt.
4 // SPDX-License-Identifier: GPL-3.0-only
5
6 import { Form, Person, TaxReturn } from '../core';
7 import { Line, AccumulatorLine, ComputedLine, InputLine, ReferenceLine, SymbolicLine, UnsupportedLine, sumFormLines, sumLineOfForms } from '../core/Line';
8 import { Literal, clampToZero } from '../core/Math';
9 import { NotFoundError, UnsupportedFeatureError } from '../core/Errors';
10
11 import Form8949, { Form8949Box } from './Form8949';
12 import Form1099DIV from './Form1099DIV';
13 import Form1040, { FilingStatus, computeTax } from './Form1040';
14
15 export default class ScheduleD extends Form {
16 readonly name = 'Schedule D';
17
18 readonly lines = {
19 // 1a not supported (Totals for all short-term transactions reported on Form 1099-B for which basis was reported to the IRS and for which you have no adjustments)
20 '4': new UnsupportedLine(), // Short-term gain from Form 6252 and short-term gain or (loss) from Forms 4684, 6781, and 8824
21 '5': new UnsupportedLine(), // Net short-term gain or (loss) from partnerships, S corporations, estates, and trusts from Schedule(s) K-1
22 '6': new ComputedLine((tr): number => {
23 const ws = tr.findForm(CapitalLossCarryoverWorksheet);
24 if (!ws)
25 return 0;
26 return -ws.getValue(tr, '6');
27 }, 'Short-term capital loss carryover'),
28
29 '7': new ComputedLine((tr): number => {
30 // 1-3 are computed by Form8949.
31 let value = sumFormLines(tr, this, ['4', '5', '6']);
32 const f8949 = tr.getForm(Form8949);
33 value += f8949.getValue(tr, 'boxA').gainOrLoss;
34 value += f8949.getValue(tr, 'boxB').gainOrLoss;
35 value += f8949.getValue(tr, 'boxC').gainOrLoss;
36 return value;
37 }, 'Net short-term capital gain or loss'),
38
39 // 8a is not supported.
40
41 '11': new UnsupportedLine(), // Gain from Form 4797, Part I; long-term gain from Forms 2439 and 6252; and long-term gain or (loss) from Forms 4684, 6781, and 8824
42 '12': new UnsupportedLine(), // Net long-term gain or (loss) from partnerships, S corporations, estates, and trusts from Schedule(s) K-1
43 '13': new AccumulatorLine(Form1099DIV, '2a', 'Capital gain distributions'),
44 '14': new ComputedLine((tr): number => {
45 const ws = tr.findForm(CapitalLossCarryoverWorksheet);
46 if (!ws)
47 return 0;
48 return -ws.getValue(tr, '13');
49 }, 'Long-term capital loss carryover'),
50
51 '15': new ComputedLine((tr): number => {
52 let value = sumFormLines(tr, this, ['11', '12', '13', '14']);
53 const f8949 = tr.getForm(Form8949);
54 value += f8949.getValue(tr, 'boxD').gainOrLoss;
55 value += f8949.getValue(tr, 'boxE').gainOrLoss;
56 value += f8949.getValue(tr, 'boxF').gainOrLoss;
57 return value;
58 }, 'Net long-term capital gain or loss'),
59
60 '16': new ComputedLine((tr): number => {
61 return this.getValue(tr, '7') + this.getValue(tr, '15');
62 // If value is a gain, enter on 1040/6 and goto 17.
63 // If value is a loss, goto 21 and 22.
64 // If value is zero, enter 0 on 1040/6 and goto 22.
65 }, 'Total capital gain or loss'),
66
67 '17': new ComputedLine((tr): boolean => {
68 return this.getValue(tr, '15') > 0 && this.getValue(tr, '16') > 0;
69 // If yes, goto 18.
70 // If no, goto 22.
71 }, 'Both ST and LT are gains'),
72
73 '18': new UnsupportedLine('28% Rate Gain Worksheet Value (Qualified Small Business Stock or collectibles.)'),
74
75 '19': new UnsupportedLine('Unrecaptured Section 1250 Gain Worksheet'),
76
77 '20': new ComputedLine((tr): boolean | undefined => {
78 const l18 = this.getValue(tr, '18');
79 const l19 = this.getValue(tr, '19');
80 return (l18 === 0 || l18 === undefined) || (l19 === 0 || l19 === undefined);
81 }, 'Line 18 and 19 both 0 or blank?'),
82
83 '21': new ComputedLine((tr): number | undefined => {
84 const l16 = this.getValue(tr, '16');
85 if (l16 >= 0)
86 return 0;
87 const filingStatus = tr.getForm(Form1040).filingStatus;
88 return Math.max(l16, tr.constants.capitalLossLimit[filingStatus]);
89 }, 'Net capital loss'),
90
91 '22': new ComputedLine((tr): boolean => {
92 return tr.getForm(Form1040).qualifiedDividends(tr) > 0;
93 }, 'Need QD/CG Tax Worksheet'),
94 };
95 };
96
97 export class ScheduleDTaxWorksheet extends Form {
98 readonly name = 'Schedule D Tax Worksheet';
99
100 readonly lines = {
101 '1': new SymbolicLine(Form1040, 'taxableIncome', 'Taxable income'),
102 '2': new SymbolicLine(Form1040, 'qualifiedDividends', 'Qualified dividends'),
103 '3': new UnsupportedLine('Form 4952@4g'),
104 '4': new UnsupportedLine('Form 4952@4e'),
105 '5': new ComputedLine((tr): number => 0),
106 '6': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '2') - this.getValue(tr, '5'))),
107 '7': new ComputedLine((tr): number => {
108 const schedD = tr.getForm(ScheduleD);
109 return Math.min(schedD.getValue(tr, '15'), schedD.getValue(tr, '16'));
110 }, 'Capital loss'),
111 '8': new ComputedLine((tr): number => {
112 return Math.min(this.getValue(tr, '3'), this.getValue(tr, '4'));
113 }),
114 '9': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '7') - this.getValue(tr, '8'))),
115 '10': new ComputedLine((tr): number => this.getValue(tr, '6') + this.getValue(tr, '9')),
116 '11': new ComputedLine((tr): number => {
117 const schedD = tr.getForm(ScheduleD);
118 return schedD.getValue(tr, '18') + schedD.getValue(tr, '19');
119 }, '28% gains and unrecaptured gains'),
120 '12': new ComputedLine((tr): number => Math.min(this.getValue(tr, '9'), this.getValue(tr, '11'))),
121 '13': new ComputedLine((tr): number => this.getValue(tr, '10') - this.getValue(tr, '12')),
122 '14': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '1') - this.getValue(tr, '13'))),
123 '15': new ComputedLine((tr): number => {
124 const fs = tr.getForm(Form1040).filingStatus;
125 return tr.constants.capitalGains.rate0MaxIncome[fs];
126 }),
127 '16': new ComputedLine((tr): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '15'))),
128 '17': new ComputedLine((tr): number => Math.min(this.getValue(tr, '14'), this.getValue(tr, '16'))),
129 '18': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '1') - this.getValue(tr, '10'))),
130 '19': new ComputedLine((tr): number => {
131 const fs = tr.getForm(Form1040).filingStatus;
132 const threshold = tr.constants.qualifiedBusinessIncomeDeductionThreshold[fs];
133 return Math.min(this.getValue(tr, '1'), threshold);
134 }),
135 '20': new ComputedLine((tr): number => Math.min(this.getValue(tr, '14'), this.getValue(tr, '19'))),
136 '21': new ComputedLine((tr): number => Math.max(this.getValue(tr, '18'), this.getValue(tr, '20'))),
137 '22': new ComputedLine((tr): number => this.getValue(tr, '16') - this.getValue(tr, '17'), 'Amount taxed at 0%'),
138 '23': new ComputedLine((tr): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '13'))),
139 '24': new ReferenceLine(ScheduleDTaxWorksheet as any, '22'),
140 '25': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '23') - this.getValue(tr, '24'))),
141 '26': new ComputedLine((tr): number => {
142 const fs = tr.getForm(Form1040).filingStatus;
143 return tr.constants.capitalGains.rate15MaxIncome[fs];
144 }),
145 '27': new ComputedLine((tr): number => Math.min(this.getValue(tr, '1'), this.getValue(tr, '26'))),
146 '28': new ComputedLine((tr): number => this.getValue(tr, '21') + this.getValue(tr, '22')),
147 '29': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '27') - this.getValue(tr, '28'))),
148 '30': new ComputedLine((tr): number => Math.min(this.getValue(tr, '25'), this.getValue(tr, '29')), 'Amount taxed at 15%'),
149 '31': new ComputedLine((tr): number => this.getValue(tr, '30') * Literal(0.15), '15% Tax'),
150 '32': new ComputedLine((tr): number => this.getValue(tr, '24') + this.getValue(tr, '30')),
151 '33': new ComputedLine((tr): number => this.getValue(tr, '23') - this.getValue(tr, '32'), 'Amount taxed at 20%'),
152 '34': new ComputedLine((tr): number => this.getValue(tr, '33') * Literal(0.20), '20% Tax'),
153 '35': new ComputedLine((tr): number => {
154 const schedD = tr.getForm(ScheduleD);
155 return Math.min(this.getValue(tr, '9'), schedD.getValue(tr, '19'));
156 }),
157 '36': new ComputedLine((tr): number => this.getValue(tr, '10') + this.getValue(tr, '21')),
158 '37': new ReferenceLine(ScheduleDTaxWorksheet as any, '1'),
159 '38': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '36') - this.getValue(tr, '37'))),
160 '39': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '35') - this.getValue(tr, '38'))),
161 '40': new ComputedLine((tr): number => this.getValue(tr, '39') * Literal(0.25), 'Tax on unrecaptured gains'),
162 '41': new ComputedLine((tr): number => {
163 const schedD = tr.getForm(ScheduleD);
164 if (schedD.getValue(tr, '18'))
165 throw new UnsupportedFeatureError('28% Gain unsupported');
166 return 0;
167 }),
168 '42': new ComputedLine((tr): number => {
169 if (!tr.getForm(ScheduleD).getValue(tr, '18'))
170 return 0;
171 return this.getValue(tr, '1') - this.getValue(tr, '41');
172 }),
173 '43': new ComputedLine((tr): number => {
174 if (!tr.getForm(ScheduleD).getValue(tr, '18'))
175 return 0;
176 return this.getValue(tr, '42') * Literal(0.28);
177 }, '28% gain tax'),
178 '44': new ComputedLine((tr): number => {
179 const income = this.getValue(tr, '21');
180 return computeTax(income, tr);
181 }, 'Nominal rate tax'),
182 '45': new ComputedLine((tr): number => {
183 return sumFormLines(tr, this, ['31', '34', '40', '43', '44']);
184 }, 'Schedule D tax'),
185 '46': new ComputedLine((tr): number => {
186 const income = this.getValue(tr, '1');
187 return computeTax(income, tr);
188 }, 'Income tax'),
189 '47': new ComputedLine((tr): number => Math.min(this.getValue(tr, '45'), this.getValue(tr, '46')), 'Tax on all taxable income'),
190 };
191 };
192
193 export interface CapitalLossCarryoverWorksheetInput {
194 priorYearTaxableIncome: number;
195 priorYearSchedDGainOrLoss: number; // This is the value reported on F1040.
196 priorYearNetShortTermGainOrLoss: number;
197 priorYearNetLongTermGainOrLoss: number;
198 };
199
200 export class CapitalLossCarryoverWorksheet extends Form<CapitalLossCarryoverWorksheetInput> {
201 readonly name = 'Capital Loss Carryover Worksheet';
202
203 readonly lines = {
204 '1': new InputLine<CapitalLossCarryoverWorksheetInput>('priorYearTaxableIncome'),
205 '2': new ComputedLine((tr): number => Math.abs(this.getInput('priorYearSchedDGainOrLoss')), 'Prior year net loss as a positive amount'),
206 '3': new ComputedLine((tr): number => clampToZero(sumFormLines(tr, this, ['1', '2']))),
207 '4': new ComputedLine((tr): number => Math.min(this.getValue(tr, '2'), this.getValue(tr, '3'))),
208 '5': new ComputedLine((tr): number => {
209 const stgl = this.getInput('priorYearNetShortTermGainOrLoss');
210 if (stgl > 0)
211 return 0;
212 return Math.abs(stgl);
213 }, 'Prior year short-term capital loss as a positive amount'),
214 '6': new ComputedLine((tr): number => clampToZero(this.getInput('priorYearNetLongTermGainOrLoss'))),
215 '7': new ComputedLine((tr): number => sumFormLines(tr, this, ['4', '6'])),
216 '8': new ComputedLine((tr): number => {
217 const l5 = this.getValue(tr, '5');
218 if (l5 <= 0)
219 return 0;
220 return clampToZero(l5 - this.getValue(tr, '7'))
221 }, 'Short-term capital loss carryover'),
222 '9': new ComputedLine((tr): number => {
223 const ltgl = this.getInput('priorYearNetLongTermGainOrLoss');
224 if (ltgl > 0)
225 return 0;
226 return Math.abs(ltgl);
227 }, 'Prior year long-term capital loss as a positive amount'),
228 '10': new ComputedLine((tr): number => clampToZero(this.getInput('priorYearNetShortTermGainOrLoss'))),
229 '11': new ComputedLine((tr): number => clampToZero(this.getValue(tr, '4') - this.getValue(tr, '5'))),
230 '12': new ComputedLine((tr): number => sumFormLines(tr, this, ['10', '11'])),
231 '13': new ComputedLine((tr): number => {
232 const l9 = this.getValue(tr, '9');
233 if (l9 <= 0)
234 return 0;
235 return clampToZero(l9 - this.getValue(tr, '12'));
236 }, 'Long-term capital loss carryover'),
237 };
238 };