line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
#--------------------------------------------------------------------- |
2
|
|
|
|
|
|
|
package Finance::PaycheckRecords; |
3
|
|
|
|
|
|
|
# |
4
|
|
|
|
|
|
|
# Copyright 2013 Christopher J. Madsen |
5
|
|
|
|
|
|
|
# |
6
|
|
|
|
|
|
|
# Author: Christopher J. Madsen |
7
|
|
|
|
|
|
|
# Created: 2 Feb 2013 |
8
|
|
|
|
|
|
|
# |
9
|
|
|
|
|
|
|
# This program is free software; you can redistribute it and/or modify |
10
|
|
|
|
|
|
|
# it under the same terms as Perl itself. |
11
|
|
|
|
|
|
|
# |
12
|
|
|
|
|
|
|
# This program is distributed in the hope that it will be useful, |
13
|
|
|
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
14
|
|
|
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either the |
15
|
|
|
|
|
|
|
# GNU General Public License or the Artistic License for more details. |
16
|
|
|
|
|
|
|
# |
17
|
|
|
|
|
|
|
# ABSTRACT: Parse data from PaycheckRecords.com |
18
|
|
|
|
|
|
|
#--------------------------------------------------------------------- |
19
|
|
|
|
|
|
|
|
20
|
3
|
|
|
3
|
|
63096
|
use 5.010; |
|
3
|
|
|
|
|
11
|
|
21
|
3
|
|
|
3
|
|
17
|
use strict; |
|
3
|
|
|
|
|
5
|
|
|
3
|
|
|
|
|
65
|
|
22
|
3
|
|
|
3
|
|
14
|
use warnings; |
|
3
|
|
|
|
|
5
|
|
|
3
|
|
|
|
|
145
|
|
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
our $VERSION = '1.001'; |
25
|
|
|
|
|
|
|
# This file is part of Finance-PaycheckRecords 1.001 (October 24, 2015) |
26
|
|
|
|
|
|
|
|
27
|
3
|
|
|
3
|
|
17
|
use Carp qw(croak); |
|
3
|
|
|
|
|
5
|
|
|
3
|
|
|
|
|
214
|
|
28
|
3
|
|
|
3
|
|
2921
|
use HTML::TableExtract 2.10; |
|
3
|
|
|
|
|
48815
|
|
|
3
|
|
|
|
|
21
|
|
29
|
3
|
|
|
3
|
|
125
|
use List::Util qw(sum); |
|
3
|
|
|
|
|
6
|
|
|
3
|
|
|
|
|
312
|
|
30
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
|
32
|
3
|
|
|
3
|
|
15
|
use Exporter 5.57 'import'; # exported import method |
|
3
|
|
|
|
|
41
|
|
|
3
|
|
|
|
|
603
|
|
33
|
|
|
|
|
|
|
our @EXPORT = qw(parse_paystub paystub_to_QIF); |
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
# This indicates which HTML::TableExtract methods to call based on |
36
|
|
|
|
|
|
|
# the keyword passed to parse_paystub. |
37
|
|
|
|
|
|
|
our %parse_method = qw( |
38
|
|
|
|
|
|
|
file parse_file |
39
|
|
|
|
|
|
|
string parse |
40
|
|
|
|
|
|
|
); |
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
our %eof_after_parse = (string => 1); |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
# When converting a paystub to QIF, this controls which column |
45
|
|
|
|
|
|
|
# contains the values that will be used in the transaction. |
46
|
|
|
|
|
|
|
our $current = 'Current'; |
47
|
|
|
|
|
|
|
|
48
|
|
|
|
|
|
|
#===================================================================== |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
sub parse_paystub |
52
|
|
|
|
|
|
|
{ |
53
|
6
|
|
|
6
|
1
|
1380
|
my ($input_type, $input) = @_; |
54
|
|
|
|
|
|
|
|
55
|
6
|
50
|
|
|
|
38
|
my $parse_method = $parse_method{$input_type} |
56
|
|
|
|
|
|
|
or croak("Don't know how to parse '$input_type'"); |
57
|
|
|
|
|
|
|
|
58
|
6
|
|
|
|
|
56
|
my $te = HTML::TableExtract->new; |
59
|
6
|
|
|
|
|
579905
|
$te->$parse_method($input); |
60
|
6
|
100
|
|
|
|
89909
|
$te->eof if $eof_after_parse{$input_type}; |
61
|
|
|
|
|
|
|
|
62
|
6
|
|
|
|
|
58
|
my %paystub; |
63
|
|
|
|
|
|
|
|
64
|
6
|
|
|
|
|
26
|
foreach my $ts ($te->tables) { |
65
|
60
|
|
|
|
|
205
|
my @coords = $ts->coords; |
66
|
60
|
|
|
|
|
535
|
my @rows = $ts->rows; |
67
|
|
|
|
|
|
|
|
68
|
3
|
|
|
3
|
|
17
|
no warnings 'uninitialized'; |
|
3
|
|
|
|
|
9
|
|
|
3
|
|
|
|
|
3880
|
|
69
|
60
|
100
|
66
|
|
|
8397
|
if ($coords[0] == 2) { |
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
70
|
12
|
100
|
|
|
|
158
|
$paystub{pay_period} = $1 |
71
|
|
|
|
|
|
|
if $rows[0][0] =~ /^\s*Pay stub for period:\s*(\S.+\S)\s*\z/s; |
72
|
|
|
|
|
|
|
} elsif ($coords[0] == 4 and $coords[1] == 0) { |
73
|
6
|
|
|
|
|
18
|
$paystub{company} = $rows[0][0]; |
74
|
6
|
|
|
|
|
14
|
$paystub{payee} = $rows[2][0]; |
75
|
6
|
100
|
|
|
|
36
|
$paystub{check_number} = $1 |
76
|
|
|
|
|
|
|
if $rows[0][2] =~ /\bCheck\s*#\s*(\d+)/; |
77
|
6
|
50
|
|
|
|
45
|
$paystub{date} = $1 |
78
|
|
|
|
|
|
|
if $rows[0][2] =~ /\bDate:\s*(\S.+\S)/; |
79
|
6
|
|
|
|
|
17
|
for (@paystub{qw(company payee)}) { |
80
|
12
|
50
|
|
|
|
32
|
next unless defined; |
81
|
12
|
|
|
|
|
47
|
s/^[\s\xA0]+//; |
82
|
12
|
|
|
|
|
99
|
s/[\s\xA0]+\z//; |
83
|
12
|
|
|
|
|
81
|
s/\n[ \t]+/\n/g; |
84
|
12
|
|
|
|
|
78
|
s/\n{2,}/\n/g; |
85
|
|
|
|
|
|
|
} |
86
|
|
|
|
|
|
|
} elsif ($coords[0] == 3) { |
87
|
30
|
100
|
|
|
|
150
|
if ($rows[0][-1] =~ /^\s*YTD\s*\z/ ) { |
|
|
100
|
|
|
|
|
|
88
|
18
|
|
|
|
|
26
|
my $headings = shift @rows; |
89
|
18
|
|
|
|
|
28
|
my %table; |
90
|
18
|
|
|
|
|
45
|
$paystub{split}{ shift @$headings } = \%table; |
91
|
18
|
|
|
|
|
40
|
for my $row (@rows) { |
92
|
48
|
|
|
|
|
81
|
for (@$row) { |
93
|
168
|
50
|
|
|
|
312
|
next unless defined; |
94
|
168
|
|
|
|
|
466
|
s/^[\s\xA0]+//; |
95
|
168
|
|
|
|
|
491
|
s/[\s\xA0]+\z//; |
96
|
|
|
|
|
|
|
} |
97
|
48
|
|
|
|
|
71
|
my $category = shift @$row; |
98
|
48
|
|
|
|
|
78
|
@{ $table{$category} }{@$headings} = @$row; |
|
48
|
|
|
|
|
264
|
|
99
|
|
|
|
|
|
|
} |
100
|
|
|
|
|
|
|
} # end if YTD |
101
|
|
|
|
|
|
|
elsif ($rows[0][0] =~ /^\s*Net\s+This\s+Check:/) { |
102
|
6
|
|
|
|
|
14
|
for my $row (@rows) { |
103
|
9
|
|
|
|
|
17
|
for (@$row) { |
104
|
18
|
50
|
|
|
|
37
|
next unless defined; |
105
|
18
|
|
|
|
|
47
|
s/^[\s\xA0]+//; |
106
|
18
|
|
|
|
|
70
|
s/[\s\xA0]+\z//; |
107
|
|
|
|
|
|
|
} |
108
|
9
|
|
|
|
|
29
|
$row->[0] =~ s/:\z//; |
109
|
9
|
|
|
|
|
37
|
$row->[1] =~ s/[\$,]//g; |
110
|
|
|
|
|
|
|
|
111
|
9
|
|
|
|
|
41
|
$paystub{totals}{$row->[0]} = $row->[1]; |
112
|
|
|
|
|
|
|
} |
113
|
|
|
|
|
|
|
} # end if Net This Check |
114
|
|
|
|
|
|
|
} |
115
|
|
|
|
|
|
|
} # end for each $ts in tables |
116
|
|
|
|
|
|
|
|
117
|
6
|
|
|
|
|
1320
|
\%paystub; |
118
|
|
|
|
|
|
|
} # end parse_paystub |
119
|
|
|
|
|
|
|
#--------------------------------------------------------------------- |
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
sub paystub_to_QIF |
123
|
|
|
|
|
|
|
{ |
124
|
4
|
|
|
4
|
1
|
6081
|
my ($paystub, $config) = @_; |
125
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
my $net_deposit = $paystub->{totals}{ $config->{net_deposit} |
127
|
4
|
|
50
|
|
|
29
|
// 'Net This Check'}; |
128
|
4
|
|
|
|
|
6
|
my @splits; |
129
|
|
|
|
|
|
|
|
130
|
4
|
|
|
|
|
16
|
_add_splits(\@splits, $paystub, $config->{income}, ''); |
131
|
4
|
|
|
|
|
12
|
_add_splits(\@splits, $paystub, $config->{expenses}, '-'); |
132
|
|
|
|
|
|
|
|
133
|
4
|
|
|
|
|
9
|
my $sum = sprintf "%.2f", sum( map { $_->[0] } @splits); |
|
18
|
|
|
|
|
111
|
|
134
|
4
|
50
|
|
|
|
23
|
croak("Sum of splits $sum != Net $net_deposit") unless $sum eq $net_deposit; |
135
|
|
|
|
|
|
|
|
136
|
4
|
|
|
|
|
12
|
my $qif = "D$paystub->{date}\n"; |
137
|
|
|
|
|
|
|
|
138
|
4
|
100
|
|
|
|
17
|
$qif .= "N$paystub->{check_number}\n" if length $paystub->{check_number}; |
139
|
|
|
|
|
|
|
|
140
|
4
|
|
|
|
|
8
|
my $company = $paystub->{company}; |
141
|
4
|
|
|
|
|
19
|
$company =~ s/\n/\nA/g; # Subsequent lines are address |
142
|
4
|
|
|
|
|
11
|
$qif .= "P$company\n"; |
143
|
|
|
|
|
|
|
|
144
|
4
|
|
66
|
|
|
20
|
my $memo = $config->{memo} // "Paycheck for $paystub->{pay_period}"; |
145
|
4
|
50
|
|
|
|
14
|
$qif .= "M$memo\n" if length $memo; |
146
|
|
|
|
|
|
|
|
147
|
4
|
|
50
|
|
|
19
|
$qif .= sprintf "T%s\nL%s\n", $net_deposit, $config->{category} // 'Income'; |
148
|
|
|
|
|
|
|
|
149
|
4
|
|
|
|
|
8
|
for my $split (@splits) { |
150
|
18
|
|
|
|
|
30
|
$qif .= "S$split->[1]\n"; |
151
|
18
|
100
|
|
|
|
47
|
$qif .= "E$split->[2]\n" if length $split->[2]; |
152
|
18
|
|
|
|
|
35
|
$qif .= "\$$split->[0]\n"; |
153
|
|
|
|
|
|
|
} |
154
|
|
|
|
|
|
|
|
155
|
4
|
|
|
|
|
34
|
$qif . "^\n"; |
156
|
|
|
|
|
|
|
} # end paystub_to_QIF |
157
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
#--------------------------------------------------------------------- |
159
|
|
|
|
|
|
|
sub _add_splits |
160
|
|
|
|
|
|
|
{ |
161
|
8
|
|
|
8
|
|
16
|
my ($all_splits, $paystub, $config, $sign) = @_; |
162
|
|
|
|
|
|
|
|
163
|
8
|
|
|
|
|
11
|
my @splits; |
164
|
|
|
|
|
|
|
|
165
|
8
|
|
|
|
|
31
|
while (my ($section, $fields) = each %$config) { |
166
|
8
|
|
|
|
|
11
|
while (my ($field, $values) = each %{ $paystub->{split}{$section} }) { |
|
28
|
|
|
|
|
110
|
|
167
|
|
|
|
|
|
|
|
168
|
20
|
100
|
50
|
|
|
84
|
next unless ($values->{$current} // 0) != 0; |
169
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
croak("Don't know what to do with $section: '$field'") |
171
|
18
|
50
|
|
|
|
42
|
unless $fields->{$field}; |
172
|
|
|
|
|
|
|
|
173
|
18
|
|
|
|
|
36
|
push @splits, [ $sign . $values->{$current}, @{ $fields->{$field} } ]; |
|
18
|
|
|
|
|
56
|
|
174
|
|
|
|
|
|
|
} |
175
|
|
|
|
|
|
|
} |
176
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
# Sort splits in ascending order by category name, and |
178
|
|
|
|
|
|
|
# descending order by absolute value within a category: |
179
|
8
|
50
|
|
|
|
24
|
push @$all_splits, sort { $a->[1] cmp $b->[1] or |
|
12
|
|
|
|
|
56
|
|
180
|
|
|
|
|
|
|
abs($b->[0]) <=> abs($a->[0]) } @splits; |
181
|
|
|
|
|
|
|
} # end _add_splits |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
#===================================================================== |
184
|
|
|
|
|
|
|
# Package Return Value: |
185
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
1; |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
__END__ |