line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Finance::Bank::Postbank_de::Account; |
2
|
14
|
|
|
14
|
|
5508
|
use 5.006; |
|
14
|
|
|
|
|
54
|
|
3
|
14
|
|
|
14
|
|
79
|
use strict; |
|
14
|
|
|
|
|
26
|
|
|
14
|
|
|
|
|
295
|
|
4
|
14
|
|
|
14
|
|
73
|
use warnings; |
|
14
|
|
|
|
|
28
|
|
|
14
|
|
|
|
|
419
|
|
5
|
14
|
|
|
14
|
|
75
|
use Carp qw(croak); |
|
14
|
|
|
|
|
28
|
|
|
14
|
|
|
|
|
709
|
|
6
|
14
|
|
|
14
|
|
2515
|
use POSIX qw(strftime); |
|
14
|
|
|
|
|
32230
|
|
|
14
|
|
|
|
|
99
|
|
7
|
14
|
|
|
14
|
|
10494
|
use Moo 2; |
|
14
|
|
|
|
|
57169
|
|
|
14
|
|
|
|
|
87
|
|
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
our $VERSION = '0.55'; |
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
has [ |
12
|
|
|
|
|
|
|
'number', |
13
|
|
|
|
|
|
|
'balance', |
14
|
|
|
|
|
|
|
'balance_unavailable', |
15
|
|
|
|
|
|
|
'balance_prev', |
16
|
|
|
|
|
|
|
'transactions_future', |
17
|
|
|
|
|
|
|
'iban', |
18
|
|
|
|
|
|
|
'blz', |
19
|
|
|
|
|
|
|
'account_type', |
20
|
|
|
|
|
|
|
'name', |
21
|
|
|
|
|
|
|
] => ( is => 'rw' ); |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
around BUILDARGS => sub { |
24
|
|
|
|
|
|
|
my ($orig, $class,%args) = @_; |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
if( exists $args{ number } and exists $args{ kontonummer } |
27
|
|
|
|
|
|
|
and $args{ number } ne $args{ kontonummer } ) { |
28
|
|
|
|
|
|
|
croak "'kontonummer' is '$args{kontonummer}' and 'number' is '$args{ number }'"; |
29
|
|
|
|
|
|
|
}; |
30
|
|
|
|
|
|
|
my $num = delete $args{number} || delete $args{kontonummer}; |
31
|
|
|
|
|
|
|
$args{ number } = $num |
32
|
|
|
|
|
|
|
if defined $num; |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
$orig->( $class, %args ); |
35
|
|
|
|
|
|
|
}; |
36
|
|
|
|
|
|
|
|
37
|
14
|
|
|
14
|
|
12486
|
{ no warnings 'once'; |
|
14
|
|
|
|
|
34
|
|
|
14
|
|
|
|
|
33551
|
|
38
|
|
|
|
|
|
|
*kontonummer = *number; |
39
|
|
|
|
|
|
|
} |
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
our %safety_check = ( |
42
|
|
|
|
|
|
|
name => 1, |
43
|
|
|
|
|
|
|
kontonummer => 1, |
44
|
|
|
|
|
|
|
); |
45
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
our %tags = ( |
47
|
|
|
|
|
|
|
#Girokonto => [qw(Name BLZ Kontonummer IBAN)], |
48
|
|
|
|
|
|
|
"gebuchte Ums\x{00E4}tze" => [qw(Name BLZ Kontonummer IBAN)], |
49
|
|
|
|
|
|
|
Tagesgeldkonto => [qw(Name BLZ Kontonummer IBAN)], |
50
|
|
|
|
|
|
|
Sparcard => [qw(Name BLZ Kontonummer IBAN)], |
51
|
|
|
|
|
|
|
Sparkonto => [qw(Name BLZ Kontonummer IBAN)], |
52
|
|
|
|
|
|
|
Kreditkarte => [qw(Name BLZ Kontonummer IBAN)], |
53
|
|
|
|
|
|
|
'' => [qw(Name BLZ Kontonummer IBAN)], |
54
|
|
|
|
|
|
|
); |
55
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
our %totals = ( |
57
|
|
|
|
|
|
|
"gebuchte Ums\x{00E4}tze" => [ |
58
|
|
|
|
|
|
|
[qr'^Aktueller Kontostand' => 'balance'], |
59
|
|
|
|
|
|
|
[qr'^Summe vorgemerkter Ums.tze' => 'transactions_future'], |
60
|
|
|
|
|
|
|
[qr'^Davon noch nicht verf.gbar' => 'balance_unavailable'], |
61
|
|
|
|
|
|
|
], |
62
|
|
|
|
|
|
|
Sparcard => [[qr'Aktueller Kontostand' => 'balance'],], |
63
|
|
|
|
|
|
|
Sparkonto => [[qr'Aktueller Kontostand' => 'balance'],], |
64
|
|
|
|
|
|
|
Tagesgeldkonto => [[qr'Aktueller Kontostand' => 'balance'],], |
65
|
|
|
|
|
|
|
"" => [ |
66
|
|
|
|
|
|
|
[qr'^Aktueller Kontostand' => 'balance'], |
67
|
|
|
|
|
|
|
[qr'^Summe der Ums.tze in den n.chsten 14 Tagen' => 'transactions_future'], |
68
|
|
|
|
|
|
|
[qr'^Davon noch nicht verf.gbar' => 'balance_unavailable'], |
69
|
|
|
|
|
|
|
], |
70
|
|
|
|
|
|
|
); |
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
our %columns = ( |
73
|
|
|
|
|
|
|
qr'Buchungsdatum' => 'tradedate', |
74
|
|
|
|
|
|
|
qr'Wertstellung' => 'valuedate', |
75
|
|
|
|
|
|
|
qr'Umsatzart' => 'type', |
76
|
|
|
|
|
|
|
qr'Buchungsdetails' => 'comment', |
77
|
|
|
|
|
|
|
qr'Auftraggeber' => 'sender', |
78
|
|
|
|
|
|
|
qr'Empf.nger' => 'receiver', |
79
|
|
|
|
|
|
|
qr"Betrag \((?:\x{20AC}|\x{80})\)" => 'amount', |
80
|
|
|
|
|
|
|
qr"Saldo \((?:\x{20AC}|\x{80})\)" => 'running_total', |
81
|
|
|
|
|
|
|
); |
82
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
sub parse_date { |
84
|
367
|
|
|
367
|
0
|
4552
|
my ($self,$date) = @_; |
85
|
367
|
100
|
|
|
|
1321
|
$date =~ /^(\d{2})\.(\d{2})\.(\d{4})$/ |
86
|
|
|
|
|
|
|
or die "Unknown date format '$date'. A date must be in the format 'DD.MM.YYYY'\n"; |
87
|
364
|
|
|
|
|
1336
|
$3.$2.$1; |
88
|
|
|
|
|
|
|
}; |
89
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
sub parse_amount { |
91
|
404
|
|
|
404
|
0
|
7194
|
my ($self,$amount) = @_; |
92
|
|
|
|
|
|
|
# '¿ 5.314,05' |
93
|
404
|
100
|
|
|
|
2066
|
die "String '$amount' does not look like a number" |
94
|
|
|
|
|
|
|
unless $amount =~ /^(-?)(?:\s*\x{20AC}\s*|\s*\x{80}\s*|\s*\x{A4}\s*)?([0-9]{1,3}(?:\.\d{3})*,\d{2})(?:\s*\x{20AC}|\s*\x{80})?$/; |
95
|
401
|
|
100
|
|
|
1495
|
$amount = ($1||'') . $2; |
96
|
401
|
|
|
|
|
789
|
$amount =~ tr/.//d; |
97
|
401
|
|
|
|
|
1199
|
$amount =~ s/,/./; |
98
|
401
|
|
|
|
|
1257
|
$amount; |
99
|
|
|
|
|
|
|
}; |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
sub slurp_file { |
102
|
3
|
|
|
3
|
0
|
21
|
my ($self,$filename) = @_; |
103
|
3
|
|
|
|
|
14
|
local $/ = undef; |
104
|
3
|
50
|
|
|
|
146
|
open my $fh, "< $filename" |
105
|
|
|
|
|
|
|
or croak "Couldn't read from file '$filename' : $!"; |
106
|
3
|
|
|
|
|
43
|
binmode $fh, ':encoding(UTF-8)'; |
107
|
3
|
|
|
|
|
268
|
<$fh>; |
108
|
|
|
|
|
|
|
}; |
109
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
sub parse_statement { |
111
|
27
|
|
|
27
|
1
|
48438
|
my ($self,%args) = @_; |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
# If $self is just a string, we want to make a new class out of us |
114
|
27
|
100
|
|
|
|
606
|
$self = $self->new |
115
|
|
|
|
|
|
|
unless ref $self; |
116
|
27
|
|
|
|
|
631
|
my $filename = $args{file}; |
117
|
27
|
|
|
|
|
72
|
my $raw_statement = $args{content}; |
118
|
27
|
100
|
|
|
|
142
|
if ($filename) { |
|
|
100
|
|
|
|
|
|
119
|
4
|
|
|
|
|
12
|
$raw_statement = $self->slurp_file($filename); |
120
|
|
|
|
|
|
|
} elsif (! defined $raw_statement) { |
121
|
|
|
|
|
|
|
croak "Need an account number if I have to retrieve the statement online" |
122
|
4
|
100
|
|
|
|
172
|
unless $args{number}; |
123
|
|
|
|
|
|
|
croak "Need a password if I have to retrieve the statement online" |
124
|
3
|
100
|
|
|
|
115
|
unless exists $args{password}; |
125
|
2
|
|
66
|
|
|
9
|
my $login = $args{login} || $args{number}; |
126
|
|
|
|
|
|
|
|
127
|
2
|
|
|
|
|
11
|
require Finance::Bank::Postbank_de; |
128
|
2
|
|
|
|
|
12
|
return Finance::Bank::Postbank_de->new( login => $login, password => $args{password}, past_days => $args{past_days} )->get_account_statement; |
129
|
|
|
|
|
|
|
}; |
130
|
|
|
|
|
|
|
|
131
|
22
|
100
|
|
|
|
326
|
croak "Don't know what to do with empty content" |
132
|
|
|
|
|
|
|
unless $raw_statement; |
133
|
|
|
|
|
|
|
|
134
|
21
|
|
|
|
|
695
|
my @lines = split /\r?\n/, $raw_statement; |
135
|
21
|
100
|
|
|
|
213
|
croak "No valid account statement: '$lines[0]'" |
136
|
|
|
|
|
|
|
unless $lines[0] =~ /^Umsatzauskunft;$/; |
137
|
20
|
|
|
|
|
54
|
shift @lines; |
138
|
|
|
|
|
|
|
|
139
|
20
|
|
|
|
|
47
|
my $account_type = ''; |
140
|
|
|
|
|
|
|
#my $account_type = $1; |
141
|
|
|
|
|
|
|
#if( ! exists $tags{ $account_type }) { |
142
|
|
|
|
|
|
|
# $account_type =~ s!([^\x00-\x7f])!sprintf '%08x', ord($1)!ge; |
143
|
|
|
|
|
|
|
# croak "Unknown account type '$account_type' (" . (join ",",keys %tags) . ")" |
144
|
|
|
|
|
|
|
# unless exists $tags{$account_type}; |
145
|
|
|
|
|
|
|
#}; |
146
|
|
|
|
|
|
|
#$self->account_type($account_type); |
147
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
# Name: PETRA PFIFFIG |
149
|
|
|
|
|
|
|
#for my $tag (@{ $tags{ $self->account_type }||[] }) { |
150
|
20
|
|
|
|
|
47
|
my $sep = ";"; |
151
|
20
|
100
|
|
|
|
98
|
if( $lines[0] =~ /([\t;])/) { |
152
|
19
|
|
|
|
|
66
|
$sep = $1; |
153
|
|
|
|
|
|
|
}; |
154
|
20
|
50
|
|
|
|
46
|
for my $tag (@{ $tags{ $account_type }||[] }) { |
|
20
|
|
|
|
|
115
|
|
155
|
73
|
100
|
|
|
|
1954
|
$lines[0] =~ /^\Q$tag\E$sep(.*?)$sep?$/ |
156
|
|
|
|
|
|
|
or croak "Field '$tag' not found in account statement ($lines[0])"; |
157
|
69
|
|
|
|
|
204
|
my $method = lc($tag); |
158
|
69
|
|
|
|
|
150
|
my $value = $1; |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
# special check for special fields: |
161
|
|
|
|
|
|
|
croak "Wrong/mixed account $method: Got '$value', expected '" . $self->$method . "'" |
162
|
69
|
100
|
100
|
|
|
593
|
if (exists $safety_check{$method} and defined $self->$method and $self->$method ne $value); |
|
|
|
100
|
|
|
|
|
163
|
|
|
|
|
|
|
|
164
|
68
|
|
|
|
|
246
|
$self->$method($value); |
165
|
68
|
|
|
|
|
173
|
shift @lines; |
166
|
|
|
|
|
|
|
}; |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
|
169
|
15
|
|
|
|
|
95
|
while ($lines[0] !~ /^\s*$/) { |
170
|
30
|
|
|
|
|
82
|
my $line = shift @lines; |
171
|
30
|
|
|
|
|
72
|
my ($method,$balance); |
172
|
30
|
50
|
|
|
|
60
|
for my $total (@{ $totals{ $account_type }||[] }) { |
|
30
|
|
|
|
|
130
|
|
173
|
90
|
|
|
|
|
244
|
my ($re,$possible_method) = @$total; |
174
|
90
|
100
|
|
|
|
2943
|
if ($line =~ /$re$sep\s*(?:(?:(\S+)\s*(?:\x{20AC}|\x{80}))|(null))$sep$/) { |
175
|
30
|
|
|
|
|
87
|
$method = $possible_method; |
176
|
30
|
|
33
|
|
|
116
|
$balance = $1 || $2; |
177
|
30
|
50
|
|
|
|
158
|
if ($balance =~ /^(-?[0-9.,]+)\s*$/) { |
|
|
0
|
|
|
|
|
|
178
|
30
|
|
|
|
|
127
|
$self->$method( ['????????',$self->parse_amount($balance)]); |
179
|
|
|
|
|
|
|
} elsif ('null' eq $balance) { |
180
|
0
|
|
|
|
|
0
|
$self->$method( ['????????',$self->parse_amount("0,00")]); |
181
|
|
|
|
|
|
|
} else { |
182
|
0
|
|
|
|
|
0
|
die "Invalid number '$balance' found for $method in '$line'"; |
183
|
|
|
|
|
|
|
}; |
184
|
|
|
|
|
|
|
}; |
185
|
|
|
|
|
|
|
}; |
186
|
30
|
50
|
|
|
|
210
|
if (! $method) { |
187
|
0
|
|
|
|
|
0
|
$account_type =~ s!([^\x00-\x7f])!sprintf '%08x', ord($1)!ge; |
|
0
|
|
|
|
|
0
|
|
188
|
0
|
|
|
|
|
0
|
$line =~ s!([^\x00-\x7f])!sprintf '%08x', ord($1)!ge; |
|
0
|
|
|
|
|
0
|
|
189
|
0
|
|
|
|
|
0
|
croak "No summary found in account '$account_type' statement ($line)"; |
190
|
|
|
|
|
|
|
}; |
191
|
|
|
|
|
|
|
}; |
192
|
|
|
|
|
|
|
|
193
|
15
|
50
|
|
|
|
89
|
$lines[0] =~ m!^\s*$! |
194
|
|
|
|
|
|
|
or croak "Expected an empty line after the account balances, got '$lines[0]'"; |
195
|
15
|
|
|
|
|
38
|
shift @lines; |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
# Now skip over the transactions in the future |
198
|
15
|
|
|
|
|
74
|
while( $lines[0] !~ /^gebuchte Ums.tze/ ) { |
199
|
105
|
|
|
|
|
235
|
shift @lines; |
200
|
|
|
|
|
|
|
}; |
201
|
15
|
|
|
|
|
40
|
shift @lines; |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
# Now parse the lines for each cashflow : |
204
|
15
|
50
|
|
|
|
155
|
$lines[0] =~ /^Buchungsdatum${sep}Wertstellung${sep}Umsatzart/ |
205
|
|
|
|
|
|
|
or croak "Couldn't find start of transactions ($lines[0])"; |
206
|
|
|
|
|
|
|
|
207
|
15
|
|
|
|
|
40
|
my (@fields); |
208
|
|
|
|
|
|
|
COLUMN: |
209
|
15
|
|
|
|
|
139
|
for my $col (split /$sep/, $lines[0]) { |
210
|
120
|
|
|
|
|
435
|
for my $target (keys %columns) { |
211
|
540
|
100
|
|
|
|
10129
|
if ($col =~ m!^["']?$target["']?$!) { |
212
|
120
|
|
|
|
|
366
|
push @fields, $columns{$target}; |
213
|
120
|
|
|
|
|
366
|
next COLUMN; |
214
|
|
|
|
|
|
|
}; |
215
|
|
|
|
|
|
|
}; |
216
|
0
|
|
|
|
|
0
|
die "Unknown column '$col' in '$lines[0]'"; |
217
|
|
|
|
|
|
|
}; |
218
|
15
|
|
|
|
|
48
|
shift @lines; |
219
|
|
|
|
|
|
|
|
220
|
15
|
|
|
|
|
130
|
my (%convert) = ( |
221
|
|
|
|
|
|
|
tradedate => \&parse_date, |
222
|
|
|
|
|
|
|
valuedate => \&parse_date, |
223
|
|
|
|
|
|
|
amount => \&parse_amount, |
224
|
|
|
|
|
|
|
running_total => \&parse_amount, |
225
|
|
|
|
|
|
|
); |
226
|
|
|
|
|
|
|
|
227
|
15
|
|
|
|
|
46
|
my @transactions; |
228
|
|
|
|
|
|
|
my $line; |
229
|
15
|
|
|
|
|
46
|
for $line (@lines) { |
230
|
180
|
50
|
|
|
|
554
|
next if $line =~ /^\s*$/; |
231
|
180
|
|
|
|
|
896
|
my (@row) = split /$sep/, $line; |
232
|
180
|
50
|
|
|
|
451
|
scalar @row == scalar @fields |
233
|
|
|
|
|
|
|
or die "Malformed cashflow ($line): Expected ".scalar(@fields)." entries, got ".scalar(@row); |
234
|
|
|
|
|
|
|
|
235
|
180
|
|
|
|
|
346
|
for (@row) { |
236
|
1440
|
50
|
|
|
|
3533
|
$_ = $1 |
237
|
|
|
|
|
|
|
if /^\s*["']\s*(.*?)\s*["']\s*$/; |
238
|
|
|
|
|
|
|
}; |
239
|
|
|
|
|
|
|
|
240
|
180
|
|
|
|
|
287
|
my (%rec); |
241
|
180
|
|
|
|
|
926
|
@rec{@fields} = @row; |
242
|
180
|
|
|
|
|
464
|
for (keys %convert) { |
243
|
720
|
|
|
|
|
1405
|
$rec{$_} = $convert{$_}->($self,$rec{$_}); |
244
|
|
|
|
|
|
|
}; |
245
|
|
|
|
|
|
|
|
246
|
180
|
|
|
|
|
562
|
push @transactions, \%rec; |
247
|
|
|
|
|
|
|
}; |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
# Filter the transactions |
250
|
15
|
|
|
|
|
78
|
$self->{transactions} = \@transactions; |
251
|
|
|
|
|
|
|
|
252
|
15
|
|
|
|
|
218
|
$self |
253
|
|
|
|
|
|
|
}; |
254
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
sub transactions { |
256
|
84
|
|
|
84
|
1
|
107640
|
my ($self,%args) = @_; |
257
|
|
|
|
|
|
|
|
258
|
84
|
|
|
|
|
172
|
my ($start_date,$end_date); |
259
|
84
|
100
|
|
|
|
210
|
if (exists $args{on}) { |
260
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
croak "Options 'since'+'upto' and 'on' are incompatible" |
262
|
33
|
100
|
100
|
|
|
290
|
if (exists $args{since} and exists $args{upto}); |
263
|
|
|
|
|
|
|
croak "Options 'since' and 'on' are incompatible" |
264
|
32
|
100
|
|
|
|
171
|
if (exists $args{since}); |
265
|
|
|
|
|
|
|
croak "Options 'upto' and 'on' are incompatible" |
266
|
31
|
100
|
|
|
|
154
|
if (exists $args{upto}); |
267
|
|
|
|
|
|
|
$args{on} = strftime('%Y%m%d',localtime()) |
268
|
30
|
100
|
|
|
|
190
|
if ($args{on} eq 'today'); |
269
|
30
|
50
|
|
|
|
147
|
$args{on} =~ /^\d{8}$/ or croak "Argument {on => '$args{on}'} dosen't look like a date to me."; |
270
|
|
|
|
|
|
|
|
271
|
30
|
|
|
|
|
61
|
$start_date = $args{on} -1; |
272
|
30
|
|
|
|
|
56
|
$end_date = $args{on}; |
273
|
|
|
|
|
|
|
} else { |
274
|
51
|
|
100
|
|
|
166
|
$start_date = $args{since} || "00000000"; |
275
|
51
|
|
100
|
|
|
168
|
$end_date = $args{upto} || "99999999"; |
276
|
51
|
100
|
|
|
|
822
|
$start_date =~ /^\d{8}$/ or croak "Argument {since => '$start_date'} dosen't look like a date to me."; |
277
|
44
|
100
|
|
|
|
812
|
$end_date =~ /^\d{8}$/ or croak "Argument {upto => '$end_date'} dosen't look like a date to me."; |
278
|
37
|
100
|
|
|
|
281
|
$start_date < $end_date or croak "The 'since' argument must be less than the 'upto' argument"; |
279
|
|
|
|
|
|
|
}; |
280
|
|
|
|
|
|
|
|
281
|
|
|
|
|
|
|
# Filter the transactions |
282
|
65
|
100
|
|
|
|
106
|
grep { $_->{tradedate} > $start_date and $_->{tradedate} <= $end_date } @{$self->{transactions}}; |
|
780
|
|
|
|
|
2208
|
|
|
65
|
|
|
|
|
152
|
|
283
|
|
|
|
|
|
|
}; |
284
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
sub value_dates { |
286
|
1
|
|
|
1
|
1
|
8
|
my ($self) = @_; |
287
|
1
|
|
|
|
|
2
|
my %dates; |
288
|
1
|
|
|
|
|
3
|
$dates{$_->{valuedate}} = 1 for $self->transactions(); |
289
|
1
|
|
|
|
|
12
|
sort keys %dates; |
290
|
|
|
|
|
|
|
}; |
291
|
|
|
|
|
|
|
|
292
|
|
|
|
|
|
|
sub trade_dates { |
293
|
1
|
|
|
1
|
1
|
1359
|
my ($self) = @_; |
294
|
1
|
|
|
|
|
2
|
my %dates; |
295
|
1
|
|
|
|
|
4
|
$dates{$_->{tradedate}} = 1 for $self->transactions(); |
296
|
1
|
|
|
|
|
12
|
sort keys %dates; |
297
|
|
|
|
|
|
|
}; |
298
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
1; |
300
|
|
|
|
|
|
|
__END__ |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
=encoding ISO8859-1 |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
=head1 NAME |
305
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
Finance::Bank::Postbank_de::Account - Postbank bank account class |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
=head1 SYNOPSIS |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
=for example begin |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
use strict; |
313
|
|
|
|
|
|
|
require Crypt::SSLeay; # It's a prerequisite |
314
|
|
|
|
|
|
|
use Finance::Bank::Postbank_de::Account; |
315
|
|
|
|
|
|
|
my $statement = Finance::Bank::Postbank_de::Account->parse_statement( |
316
|
|
|
|
|
|
|
login => 'Petra.Pfiffig', |
317
|
|
|
|
|
|
|
password => '123456789', |
318
|
|
|
|
|
|
|
); |
319
|
|
|
|
|
|
|
# Retrieve account data : |
320
|
|
|
|
|
|
|
print "Balance : ",$statement->balance->[1]," EUR\n"; |
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
# Output CSV for the transactions |
323
|
|
|
|
|
|
|
for my $row ($statement->transactions) { |
324
|
|
|
|
|
|
|
print join( ";", map { $row->{$_} } (qw( tradedate valuedate type comment receiver sender amount ))),"\n"; |
325
|
|
|
|
|
|
|
}; |
326
|
|
|
|
|
|
|
|
327
|
|
|
|
|
|
|
=for example end |
328
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
=for example_testing |
330
|
|
|
|
|
|
|
isa_ok($statement,"Finance::Bank::Postbank_de::Account"); |
331
|
|
|
|
|
|
|
my $expected = <<EOX; |
332
|
|
|
|
|
|
|
Balance : 5314.05 EUR |
333
|
|
|
|
|
|
|
.berweisung;111111/1000000000/37050198 FINANZKASSE 3991234 STEUERNUMMER 00703434;Finanzkasse K.ln-S.d;PETRA PFIFFIG;-328.75 |
334
|
|
|
|
|
|
|
.berweisung;111111/3299999999/20010020 .BERTRAG AUF SPARCARD 3299999999;Petra Pfiffig;PETRA PFIFFIG;-228.61 |
335
|
|
|
|
|
|
|
Gutschrift;BEZ.GE PERS.NR. 70600170/01 ARBEITGEBER U. CO;PETRA PFIFFIG;Petra Pfiffig;2780.70 |
336
|
|
|
|
|
|
|
.berweisung;DA 1000001;Verlagshaus Scribere GmbH;PETRA PFIFFIG;-31.50 |
337
|
|
|
|
|
|
|
Scheckeinreichung;EINGANG VORBEHALTEN GUTBUCHUNG 12345;PETRA PFIFFIG;Ein Fremder;1830.00 |
338
|
|
|
|
|
|
|
Lastschrift;MIETE 600+250 EUR OBJ22/328 SCHULSTR.7, 12345 MEINHEIM;Eigenheim KG;PETRA PFIFFIG;-850.00 |
339
|
|
|
|
|
|
|
Inh. Scheck;;2000123456789;PETRA PFIFFIG;-75.00 |
340
|
|
|
|
|
|
|
Lastschrift;TEILNEHMERNR 1234567 RUNDFUNK 0103-1203;GEZ;PETRA PFIFFIG;-84.75 |
341
|
|
|
|
|
|
|
Lastschrift;RECHNUNG 03121999;Telefon AG Köln;PETRA PFIFFIG;-125.80 |
342
|
|
|
|
|
|
|
Lastschrift;STROMKOSTEN KD.NR.1462347 JAHRESABRECHNUNG;Stadtwerke Musterstadt;PETRA PFIFFIG;-580.06 |
343
|
|
|
|
|
|
|
Gutschrift;KINDERGELD KINDERGELD-NR. 1462347;PETRA PFIFFIG;Arbeitsamt Bonn;154.00 |
344
|
|
|
|
|
|
|
EOX |
345
|
|
|
|
|
|
|
for ($::_STDOUT_,$expected) { |
346
|
|
|
|
|
|
|
s!\r\n!!gsm; |
347
|
|
|
|
|
|
|
# Strip out all date references ... |
348
|
|
|
|
|
|
|
s/^\d{8};\d{8};//gm; |
349
|
|
|
|
|
|
|
s![\x80-\xff]!.!gsm; |
350
|
|
|
|
|
|
|
}; |
351
|
|
|
|
|
|
|
is_deeply([split /\n/, $::_STDOUT_],[split /\n/, $expected],"Retrieved the correct data") |
352
|
|
|
|
|
|
|
or do { |
353
|
|
|
|
|
|
|
diag "--- Expected"; |
354
|
|
|
|
|
|
|
diag $expected; |
355
|
|
|
|
|
|
|
diag "--- Got"; |
356
|
|
|
|
|
|
|
diag $::_STDOUT_; |
357
|
|
|
|
|
|
|
}; |
358
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
=head1 DESCRIPTION |
360
|
|
|
|
|
|
|
|
361
|
|
|
|
|
|
|
This module provides a rudimentary interface to the Postbank online banking system at |
362
|
|
|
|
|
|
|
https://banking.postbank.de/. You will need either Crypt::SSLeay or IO::Socket::SSL |
363
|
|
|
|
|
|
|
installed for HTTPS support to work with LWP. |
364
|
|
|
|
|
|
|
|
365
|
|
|
|
|
|
|
The interface was cooked up by me without taking a look at the other Finance::Bank |
366
|
|
|
|
|
|
|
modules. If you have any proposals for a change, they are welcome ! |
367
|
|
|
|
|
|
|
|
368
|
|
|
|
|
|
|
=head1 WARNING |
369
|
|
|
|
|
|
|
|
370
|
|
|
|
|
|
|
This is code for online banking, and that means your money, and that means BE CAREFUL. You are encouraged, nay, expected, to audit the source of this module yourself to reassure yourself that I am not doing anything untoward with your banking data. This software is useful to me, but is provided under NO GUARANTEE, explicit or implied. |
371
|
|
|
|
|
|
|
|
372
|
|
|
|
|
|
|
=head1 WARNUNG |
373
|
|
|
|
|
|
|
|
374
|
|
|
|
|
|
|
Dieser Code beschaeftigt sich mit Online Banking, das heisst, hier geht es um Dein Geld und das bedeutet SEI VORSICHTIG ! Ich gehe |
375
|
|
|
|
|
|
|
davon aus, dass Du den Quellcode persoenlich anschaust, um Dich zu vergewissern, dass ich nichts unrechtes mit Deinen Bankdaten |
376
|
|
|
|
|
|
|
anfange. Diese Software finde ich persoenlich nuetzlich, aber ich stelle sie OHNE JEDE GARANTIE zur Verfuegung, weder eine |
377
|
|
|
|
|
|
|
ausdrueckliche noch eine implizierte Garantie. |
378
|
|
|
|
|
|
|
|
379
|
|
|
|
|
|
|
=head1 METHODS |
380
|
|
|
|
|
|
|
|
381
|
|
|
|
|
|
|
=head2 new |
382
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
Creates a new object. It takes three named parameters : |
384
|
|
|
|
|
|
|
|
385
|
|
|
|
|
|
|
=over 4 |
386
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
=item number => '9999999999' |
388
|
|
|
|
|
|
|
|
389
|
|
|
|
|
|
|
This is the number of the account. If you don't know it (for example, you |
390
|
|
|
|
|
|
|
are reading in an account statement from disk), leave it undef. |
391
|
|
|
|
|
|
|
|
392
|
|
|
|
|
|
|
=back |
393
|
|
|
|
|
|
|
|
394
|
|
|
|
|
|
|
=head2 $account->parse_statement %ARGS |
395
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
Parses an account statement and returns it as a hash reference. The account statement |
397
|
|
|
|
|
|
|
can be passed in via two named parameters. If no parameter is given, the current statement |
398
|
|
|
|
|
|
|
is fetched via the website through a call to C<get_account_statement> (is this so?). |
399
|
|
|
|
|
|
|
|
400
|
|
|
|
|
|
|
Parameters : |
401
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
=over 4 |
403
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
=item file => $filename |
405
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
Parses the file C<$filename> instead of downloading data from the web. |
407
|
|
|
|
|
|
|
|
408
|
|
|
|
|
|
|
=item content => $string |
409
|
|
|
|
|
|
|
|
410
|
|
|
|
|
|
|
Parses the content of C<$string> instead of downloading data from the web. |
411
|
|
|
|
|
|
|
|
412
|
|
|
|
|
|
|
=back |
413
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
=head2 $account->iban |
415
|
|
|
|
|
|
|
|
416
|
|
|
|
|
|
|
Returns the IBAN for the account as a string. Later, a move to L<Business::IBAN> is |
417
|
|
|
|
|
|
|
planned. The IBAN is a unique identifier for every account, that identifies the country, |
418
|
|
|
|
|
|
|
bank and account with that bank. |
419
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
=head2 $account->transactions %ARGS |
421
|
|
|
|
|
|
|
|
422
|
|
|
|
|
|
|
Delivers you all transactions within a statement. The transactions may be filtered |
423
|
|
|
|
|
|
|
by date by specifying the parameters 'since', 'upto' or 'on'. The values are, as always, |
424
|
|
|
|
|
|
|
8-digit strings denoting YYYYMMDD dates. |
425
|
|
|
|
|
|
|
|
426
|
|
|
|
|
|
|
Parameters : |
427
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
=over 4 |
429
|
|
|
|
|
|
|
|
430
|
|
|
|
|
|
|
=item since => $date |
431
|
|
|
|
|
|
|
|
432
|
|
|
|
|
|
|
Removes all transactions that happened on or before $date. $date must |
433
|
|
|
|
|
|
|
be in the format YYYYMMDD. If the line is missing, C<since =E<gt> '00000000'> |
434
|
|
|
|
|
|
|
is assumed. |
435
|
|
|
|
|
|
|
|
436
|
|
|
|
|
|
|
=item upto => $date |
437
|
|
|
|
|
|
|
|
438
|
|
|
|
|
|
|
Removes all transactions that happened after $date. $date must |
439
|
|
|
|
|
|
|
be in the format YYYYMMDD. If the line is missing, C<upto =E<gt> '99999999'> |
440
|
|
|
|
|
|
|
is assumed. |
441
|
|
|
|
|
|
|
|
442
|
|
|
|
|
|
|
=item on => $date |
443
|
|
|
|
|
|
|
|
444
|
|
|
|
|
|
|
Removes all transactions that happened on a date that is not C<eq> to $date. $date must |
445
|
|
|
|
|
|
|
be in the format YYYYMMDD. $date may also be the special string 'today', which will |
446
|
|
|
|
|
|
|
be converted to a YYYYMMDD string corresponding to todays date. |
447
|
|
|
|
|
|
|
|
448
|
|
|
|
|
|
|
=back |
449
|
|
|
|
|
|
|
|
450
|
|
|
|
|
|
|
=head2 $account->value_dates |
451
|
|
|
|
|
|
|
|
452
|
|
|
|
|
|
|
C<value_dates> is a convenience method that returns all value dates on the account statement. |
453
|
|
|
|
|
|
|
|
454
|
|
|
|
|
|
|
=cut |
455
|
|
|
|
|
|
|
|
456
|
|
|
|
|
|
|
=head2 $account->trade_dates |
457
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
C<trade_dates> is a convenience method that returns all trade dates on the account statement. |
459
|
|
|
|
|
|
|
|
460
|
|
|
|
|
|
|
=cut |
461
|
|
|
|
|
|
|
|
462
|
|
|
|
|
|
|
=head2 Converting a daily download to a sequence |
463
|
|
|
|
|
|
|
|
464
|
|
|
|
|
|
|
=for example begin |
465
|
|
|
|
|
|
|
|
466
|
|
|
|
|
|
|
#!/usr/bin/perl -w |
467
|
|
|
|
|
|
|
use strict; |
468
|
|
|
|
|
|
|
|
469
|
|
|
|
|
|
|
use Finance::Bank::Postbank_de::Account; |
470
|
|
|
|
|
|
|
use Tie::File; |
471
|
|
|
|
|
|
|
use List::Sliding::Changes qw(find_new_elements); |
472
|
|
|
|
|
|
|
use FindBin; |
473
|
|
|
|
|
|
|
use MIME::Lite; |
474
|
|
|
|
|
|
|
|
475
|
|
|
|
|
|
|
my $filename = "$FindBin::Bin/statement.txt"; |
476
|
|
|
|
|
|
|
tie my @statement, 'Tie::File', $filename |
477
|
|
|
|
|
|
|
or die "Couldn't tie to '$filename' : $!"; |
478
|
|
|
|
|
|
|
|
479
|
|
|
|
|
|
|
my @transactions; |
480
|
|
|
|
|
|
|
|
481
|
|
|
|
|
|
|
# See what has happened since we last polled |
482
|
|
|
|
|
|
|
my $retrieved_statement = Finance::Bank::Postbank_de::Account->parse_statement( |
483
|
|
|
|
|
|
|
number => '9999999999', |
484
|
|
|
|
|
|
|
password => '11111', |
485
|
|
|
|
|
|
|
); |
486
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
# Output CSV for the transactions |
488
|
|
|
|
|
|
|
for my $row (reverse @{$retrieved_statement->transactions()}) { |
489
|
|
|
|
|
|
|
push @transactions, join( ";", map { $row->{$_} } (qw( tradedate valuedate type comment receiver sender amount ))); |
490
|
|
|
|
|
|
|
}; |
491
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
# Find out what we did not already communicate |
493
|
|
|
|
|
|
|
my (@new) = find_new_elements(\@statement,\@transactions); |
494
|
|
|
|
|
|
|
if (@new) { |
495
|
|
|
|
|
|
|
my ($body) = "<html><body><table>"; |
496
|
|
|
|
|
|
|
my ($date,$balance) = @{$retrieved_statement->balance}; |
497
|
|
|
|
|
|
|
$body .= "<b>Balance ($date) :</b> $balance<br>"; |
498
|
|
|
|
|
|
|
$body .= "<tr><th>"; |
499
|
|
|
|
|
|
|
$body .= join( "</th><th>", qw( tradedate valuedate type comment receiver sender amount )). "</th></tr>"; |
500
|
|
|
|
|
|
|
for my $line (@{[@new]}) { |
501
|
|
|
|
|
|
|
$line =~ s!;!</td><td>!g; |
502
|
|
|
|
|
|
|
$body .= "<tr><td>$line</td></tr>\n"; |
503
|
|
|
|
|
|
|
}; |
504
|
|
|
|
|
|
|
$body .= "</body></html>"; |
505
|
|
|
|
|
|
|
MIME::Lite->new( |
506
|
|
|
|
|
|
|
From =>'update.pl', |
507
|
|
|
|
|
|
|
To =>'you', |
508
|
|
|
|
|
|
|
Subject =>"Account update $date", |
509
|
|
|
|
|
|
|
Type =>'text/html', |
510
|
|
|
|
|
|
|
Encoding =>'base64', |
511
|
|
|
|
|
|
|
Data => $body, |
512
|
|
|
|
|
|
|
)->send; |
513
|
|
|
|
|
|
|
}; |
514
|
|
|
|
|
|
|
|
515
|
|
|
|
|
|
|
# And update our log with what we have seen |
516
|
|
|
|
|
|
|
push @statement, @new; |
517
|
|
|
|
|
|
|
|
518
|
|
|
|
|
|
|
=for example end |
519
|
|
|
|
|
|
|
|
520
|
|
|
|
|
|
|
=head1 AUTHOR |
521
|
|
|
|
|
|
|
|
522
|
|
|
|
|
|
|
Max Maischein, E<lt>corion@cpan.orgE<gt> |
523
|
|
|
|
|
|
|
|
524
|
|
|
|
|
|
|
=head1 SEE ALSO |
525
|
|
|
|
|
|
|
|
526
|
|
|
|
|
|
|
L<perl>, L<Finance::Bank::Postbank_de>. |
527
|
|
|
|
|
|
|
|
528
|
|
|
|
|
|
|
=cut |