File Coverage

blib/lib/Business/Tax/ID/PPH21.pm
Criterion Covered Total %
statement 50 97 51.5
branch 18 60 30.0
condition 5 40 12.5
subroutine 9 10 90.0
pod 4 4 100.0
total 86 211 40.7


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