File Coverage

blib/lib/Business/Tax/ID/PPH21.pm
Criterion Covered Total %
statement 50 97 51.5
branch 14 50 28.0
condition 5 40 12.5
subroutine 9 10 90.0
pod 4 4 100.0
total 82 201 40.8


line stmt bran cond sub pod time code
1             package Business::Tax::ID::PPH21;
2              
3 1     1   71491 use 5.010001;
  1         14  
4 1     1   6 use strict;
  1         1  
  1         21  
5 1     1   5 use warnings;
  1         2  
  1         28  
6              
7 1     1   499 use Exporter::Rinci qw(import);
  1         433  
  1         6  
8              
9             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
10             our $DATE = '2021-12-14'; # DATE
11             our $DIST = 'Business-Tax-ID-PPH21'; # DIST
12             our $VERSION = '0.065'; # VERSION
13              
14             our %SPEC;
15              
16             my $latest_supported_year = 2022;
17              
18             our %arg_tp_status = (
19             tp_status => {
20             summary => 'Taxypayer status',
21             description => <<'_',
22              
23             Taypayer status reflects his/her marital status and affects the amount of
24             his/her non-taxable income.
25              
26             _
27             schema => ['str*', in=>[
28             'TK/0', 'TK/1', 'TK/2', 'TK/3',
29             'K/0' , 'K/1', 'K/2', 'K/3',
30             ]],
31             req => 1,
32             },
33             );
34              
35             our %arg_year = (
36             year => {
37             schema => ['int*', min=>1983],
38             req => 1,
39             pos => 0,
40             },
41             );
42              
43             our %arg_net_income = (
44             net_income => {
45             summary => 'Yearly net income',
46             schema => ['float*', min=>0],
47             req => 1,
48             },
49             );
50              
51             our %arg_pph21_op = (
52             pph21_op => {
53             summary => 'Amount of PPh 21 op paid',
54             schema => ['float*', min=>0],
55             req => 1,
56             pos => 1,
57             },
58             );
59              
60             $SPEC{':package'} = {
61             v => 1.1,
62             summary => 'Routines to help calculate Indonesian income tax article 21 (PPh pasal 21)',
63             description => <<'_',
64              
65             The law ("undang-undang") for income tax ("pajak penghasilan") in Indonesia is
66             UU 7/1983 (along with its several amendments, the latest two of which are UU
67             36/2008 and UU HPP 7/2021). This law is comprised of several articles ("pasal").
68             Article 21 ("pasal 21") regulates earned income, which is income generated by
69             individual/statutory bodies from performed work/services, including: employment
70             (salary and benefits, as well as severance pay), freelance work, business,
71             pension, and "jaminan hari tua" (life insurance paid to dependents when a worker
72             is deceased). Article 21 also regulates some types of passive income, which is
73             income generated from capital or other non-work sources, including: savings
74             interests, gifts/lotteries, royalties.
75              
76             Some other passive income like rent and dividends are regulated in article 23
77             ("pasal 23") instead of article 21. And some articles regulate other aspects or
78             special cases, e.g. income tax for sales to government agencies/import/specific
79             industries ("pasal 22"), rules regarding monthly tax payments ("pasal 25"), or
80             rules regarding work earned in Indonesia by non-citizens ("pasal 26").
81              
82             This module contains several routines to help calculate income tax article 21.
83              
84             _
85              
86             };
87              
88             $SPEC{get_pph21_op_rates} = {
89             v => 1.1,
90             summary => 'Get tax rates for PPh21 for individuals ("OP", "orang pribadi")',
91             description => <<'_',
92              
93             PPh21 differentiates rates between individuals ("OP", "orang pribadi") and
94             statutory bodies ("badan"). Both are progressive. This routine returns the tax
95             rates for individuals.
96              
97             Keywords: tax rates, tax brackets.
98              
99             _
100             'description.alt.lang.id_ID' => <<'_',
101              
102             Kata kunci: tarif pajak, lapisan pajak.
103              
104             _
105             args => {
106             %arg_year,
107             },
108             examples => [
109             {args=>{year=>2022}},
110             ],
111             };
112             sub get_pph21_op_rates {
113 2     2 1 6 my %args = @_;
114 2         4 my $year = $args{year};
115 2         11 my $resmeta = {
116             'table.fields' => [qw/xmin max rate/],
117             'table.field_formats' => [
118             undef, undef, ['percent', {sprintf=>'%3.0f%%'}]
119             ],
120             };
121 2 50 33     13 if ($year >= 2022 && $year <= $latest_supported_year) {
    50 33        
    0 0        
122 0         0 state $res = [
123             200, "OK",
124             [
125             { max=> 60_000_000, rate=>0.05},
126             {xmin=> 60_000_000, max=> 250_000_000, rate=>0.15},
127             {xmin=> 250_000_000, max=> 500_000_000, rate=>0.25},
128             {xmin=> 500_000_000, max=>5_000_000_000, rate=>0.30},
129             {xmin=>5_000_000_000, rate=>0.35},
130             ],
131             $resmeta,
132             ];
133 0         0 return $res;
134             } elsif ($year >= 2009 && $year <= 2022) {
135 2         12 state $res = [
136             200, "OK",
137             [
138             { max=> 50_000_000, rate=>0.05},
139             {xmin=> 50_000_000, max=>250_000_000, rate=>0.15},
140             {xmin=>250_000_000, max=>500_000_000, rate=>0.25},
141             {xmin=>500_000_000, rate=>0.30},
142             ],
143             $resmeta,
144             ];
145 2         5 return $res;
146             } elsif ($year >= 2000 && $year <= 2008) {
147 0         0 state $res = [
148             200, "OK",
149             [
150             { max=> 25_000_000, rate=>0.05},
151             {xmin=> 25_000_000, max=> 50_000_000, rate=>0.10},
152             {xmin=> 50_000_000, max=>100_000_000, rate=>0.15},
153             {xmin=>100_000_000, max=>200_000_000, rate=>0.25},
154             {xmin=>200_000_000, rate=>0.35},
155             ],
156             $resmeta,
157             ];
158 0         0 return $res;
159             } else {
160 0         0 return [412, "Year unknown or unsupported (latest supported year is ".
161             "$latest_supported_year)"];
162             }
163             }
164              
165             # TODO: get_pph21_badan_rates
166              
167             $SPEC{get_pph21_op_ptkp} = {
168             v => 1.1,
169             summary => 'Get PPh21 non-taxable income amount ("PTKP") for individuals',
170             description => <<'_',
171              
172             When calculating individual income tax, the net income is subtracted by this
173             amount first. This means that if a person has income below this amount, he/she
174             does not need to pay income tax.
175              
176             _
177             'description.alt.lang.id_ID' => <<'_',
178              
179             Kata kunci: penghasilan tidak kena pajak.
180              
181             _
182             args => {
183             %arg_year,
184             },
185             examples => [
186             {args=>{year=>2016}},
187             ],
188             };
189             sub get_pph21_op_ptkp {
190 3     3 1 7 my %args = @_;
191              
192 3         5 my $tp_status = $args{tp_status};
193 3         5 my $year = $args{year};
194              
195             my $code_make = sub {
196 1     1   3 my ($base, $add) = @_;
197             return {
198             map {
199 1         3 ("TK/$_" => $base + $add*$_,
  4         20  
200             "K/$_" => $base + $add + $add*$_)
201             } 0..3
202             };
203 3         27 };
204              
205 3 50 33     20 if ($year >= 2016 && $year <= $latest_supported_year) { # UU PMK: 101/PMK.010/2016
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
206 3         7 state $res = [200, "OK", $code_make->( 54_000_000, 4_500_000)];
207 3         17 return $res;
208             } elsif ($year >= 2015 && $year <= 2015) {
209 0         0 state $res = [200, "OK", $code_make->( 36_000_000, 3_000_000)];
210 0         0 return $res;
211             } elsif ($year >= 2013 && $year <= 2014) {
212 0         0 state $res = [200, "OK", $code_make->( 24_300_000, 2_025_000)];
213 0         0 return $res;
214             } elsif ($year >= 2009 && $year <= 2012) {
215 0         0 state $res = [200, "OK", $code_make->( 15_840_000, 1_320_000)];
216 0         0 return $res;
217             } elsif ($year >= 2006 && $year <= 2008) {
218 0         0 state $res = [200, "OK", $code_make->( 13_200_000, 1_200_000)];
219 0         0 return $res;
220             } elsif ($year >= 2005 && $year <= 2005) {
221 0         0 state $res = [200, "OK", $code_make->( 12_000_000, 1_200_000)];
222 0         0 return $res;
223             } elsif ($year >= 2001 && $year <= 2004) {
224 0         0 state $res = [200, "OK", $code_make->( 2_880_000, 1_440_000)];
225 0         0 return $res;
226             } elsif ($year >= 1994 && $year <= 2000) {
227 0         0 state $res = [200, "OK", $code_make->( 1_728_000, 864_000)];
228 0         0 return $res;
229             } elsif ($year >= 1983 && $year <= 1994) {
230 0         0 state $res = [200, "OK", $code_make->( 960_000, 480_000)];
231 0         0 return $res;
232             } else {
233 0         0 return [412, "Year unknown or unsupported (latest supported year is ".
234             "$latest_supported_year)"];
235             }
236             }
237              
238 6 100   6   23 sub _min { $_[0] < $_[1] ? $_[0] : $_[1] }
239              
240             $SPEC{calc_pph21_op} = {
241             v => 1.1,
242             summary => 'Calculate PPh 21 for individuals ("OP", "orang pribadi")',
243             args => {
244             %arg_year,
245             %arg_tp_status,
246             %arg_net_income,
247             },
248             examples => [
249             {
250             summary => 'Someone who earns below PTKP',
251             args => {year=>2015, tp_status=>'TK/0', net_income=>30_000_000},
252             },
253             {
254             args => {year=>2015, tp_status=>'K/2', net_income=>300_000_000},
255             },
256             ],
257             };
258             sub calc_pph21_op {
259 3     3 1 1127 my %args = @_;
260              
261 3         7 my $year = $args{year};
262 3         6 my $tp_status = $args{tp_status};
263 3         6 my $net_income = $args{net_income};
264              
265 3         6 my $res;
266              
267 3         7 $res = get_pph21_op_ptkp(year => $year);
268 3 50       10 return $res unless $res->[0] == 200;
269 3         5 my $ptkps = $res->[2];
270 3 50       9 my $ptkp = $ptkps->{$tp_status}
271             or die "BUG: Can't get PTKP for '$tp_status'";
272              
273 3         7 my $pkp = $net_income - $ptkp;
274 3 100       11 return [200, "OK", 0] if $pkp <= 0;
275              
276 2         7 $res = get_pph21_op_rates(year => $year);
277 2 50       7 return $res unless $res->[0] == 200;
278 2         4 my $brackets = $res->[2];
279              
280 2         3 my $tax = 0;
281 2         5 for my $bracket (@$brackets) {
282 7 100       14 if (defined $bracket->{max}) {
283             $tax += (_min($pkp, $bracket->{max}) -
284 6   100     12 ($bracket->{xmin} // 0)) * $bracket->{rate};
285 6 100       18 last if $pkp <= $bracket->{max};
286             } else {
287 1         3 $tax += ($pkp - $bracket->{xmin}) * $bracket->{rate};
288 1         2 last;
289             }
290             }
291              
292 2         10 [200, "OK", $tax];
293             }
294              
295             $SPEC{calc_net_income_from_pph21_op} = {
296             v => 1.1,
297             summary => 'Given that someone pays a certain amount of PPh 21 op, '.
298             'calculate her yearly net income',
299             description => <<'_',
300              
301             If pph21_op is 0, will return the PTKP amount. Actually one can earn between
302             zero and the full PTKP amount to pay zero PPh 21 op.
303              
304             _
305             args => {
306             %arg_year,
307             %arg_tp_status,
308             %arg_pph21_op,
309             monthly => {
310             summary => 'Instead of yearly, return monthly net income',
311             schema => ['bool*', is=>1],
312             },
313             },
314             examples => [
315             {
316             summary => "Someone who doesn't pay PPh 21 op earns at or below PTKP",
317             args => {year=>2016, tp_status=>'TK/0', pph21_op=>0},
318             },
319             {
320             args => {year=>2016, tp_status=>'K/2', pph21_op=>20_000_000},
321             },
322             ],
323             };
324             sub calc_net_income_from_pph21_op {
325 0     0 1   my %args = @_;
326              
327 0           my $year = $args{year};
328 0           my $tp_status = $args{tp_status};
329 0           my $pph21_op = $args{pph21_op};
330              
331 0           my $res;
332              
333 0           $res = get_pph21_op_ptkp(year => $year);
334 0 0         return $res unless $res->[0] == 200;
335 0           my $ptkps = $res->[2];
336 0 0         my $ptkp = $ptkps->{$tp_status}
337             or die "BUG: Can't get PTKP for '$tp_status'";
338              
339 0           $res = get_pph21_op_rates(year => $year);
340 0 0         return $res unless $res->[0] == 200;
341 0           my $brackets = $res->[2];
342              
343 0           my $net_income = $ptkp;
344 0           for my $bracket (@$brackets) {
345 0 0         if (defined $bracket->{max}) {
346 0   0       my $range = $bracket->{max} - ($bracket->{xmin} // 0);
347 0           my $bracket_tax = $range * $bracket->{rate};
348 0 0         if ($pph21_op <= $bracket_tax) {
349 0           $net_income += $pph21_op / $bracket->{rate};
350 0           last;
351             } else {
352 0           $pph21_op -= $bracket_tax;
353 0           $net_income += $range;
354             }
355             } else {
356 0           $net_income += $pph21_op/$bracket->{rate};
357 0           last;
358             }
359             }
360 0 0         [200, "OK", $args{monthly} ? $net_income / 12 : $net_income];
361             }
362              
363             1;
364             # ABSTRACT: Routines to help calculate Indonesian income tax article 21 (PPh pasal 21)
365              
366             __END__