Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision f7d51d3e

Von Sven Schöling vor etwa 12 Jahren hinzugefügt

SelfTests

Es gibt jetzt ein Grundgerüst um Selbsttests durchzuführen, und bei Problemen
einen Administrator per Mail zu benachrichtigen. Die Selbsttests werden Über
das SelfTest Modul für den Taskserver verwaltet, und in config/lx_office.conf
im Block [self_test] konfiguriert. Die Tests werden in TAP ausgeliefert und
können bei Bedarf weiter maschinell ausgewertet werden.

Weitere Tests können von SL::BackgroundJob::SelfTest::Base abgeleitet werden.

Zur Demonstration gibt es einen Selbsttest Transactions, der die Datenbank
auf Fehlbuchungen untersucht.

Unterschiede anzeigen:

SL/BackgroundJob/SelfTest.pm
1
package SL::BackgroundJob::SelfTest;
2

  
3
use strict;
4

  
5
use parent qw(SL::BackgroundJob::Base);
6

  
7
use Test::Builder;
8
use TAP::Parser;
9
use TAP::Parser::Aggregator;
10
use Sys::Hostname;
11
use FindBin;
12

  
13
use SL::DB::AuthUser;
14
use SL::Common;
15

  
16
use Rose::Object::MakeMethods::Generic (
17
  array => [
18
   'modules'     => {},
19
   'add_modules' => { interface => 'add', hash_key => 'modules' },
20
   'errors'      => {},
21
   'add_errors'  => { interface => 'add', hash_key => 'errors' },
22
   'full_diag'      => {},
23
   'add_full_diag'  => { interface => 'add', hash_key => 'full_diag' },
24
  ],
25
  scalar => [
26
   qw(diag tester config aggreg),
27
  ],
28
);
29

  
30
sub create_job {
31
  $_[0]->create_standard_job('20 2 * * *'); # every day at 2:20 am
32
}
33

  
34
sub setup {
35
  my ($self) = @_;
36

  
37
  $self->config($::lx_office_conf{self_test} || {});
38

  
39
  $self->tester(Test::Builder->new);
40
  $self->aggreg(TAP::Parser::Aggregator->new);
41

  
42
  $self->modules(split /\s+/, $self->config->{modules});
43
}
44

  
45
sub run {
46
  my $self        = shift;
47
  $self->setup;
48

  
49
  return 1 unless $self->modules;
50

  
51
  foreach my $module ($self->modules) {
52
    $self->run_module($module);
53
  }
54

  
55
  $self->log(
56
    sprintf "SelfTest status: %s, passed: %s, failed: %s, unexpectedly succeeded: %s",
57
             $self->aggreg->get_status,
58
             $self->aggreg->passed,
59
             $self->aggreg->failed,
60
             $self->aggreg->todo_passed,
61
  );
62

  
63
  if (!$self->aggreg->all_passed || $self->config->{send_email_on_success}) {
64
    $self->_send_email;
65
  }
66

  
67
  return 1;
68
}
69

  
70
sub run_module {
71
  my ($self, $module) = @_;
72

  
73
  # TAP usually prints out to STDOUT and STDERR, capture those for TAP::Parser
74
  my $output;
75

  
76
  $self->tester->output        (\$output);
77
  $self->tester->failure_output(\$output);
78
  $self->tester->todo_output   (\$output);
79

  
80
  # sanitize module name;
81
  # this allows unicode package names, which are known to be buggy in 5.10, you should avoid them
82
  $module =~ s/[^\w:]//g;
83
  $module = "SL::BackgroundJob::SelfTest::$module";
84

  
85
  # try to load module;
86
  (my $file = $module) =~ s|::|/|g;
87
  eval {
88
    require $file . '.pm';
89
    1
90
  } or $self->add_errors($::locale->text('Could not load class #1 (#2): "#3"', $module, $file, $@)) && return;
91

  
92
  eval {
93
    my $worker = $module->new;
94
    $worker->tester($self->tester);
95

  
96
    $worker->run;
97
    1;
98
  } or $self->add_errors($::locale->text('Could not load class #1, #2', $module, $@)) && return;
99

  
100
  $self->add_full_diag($output);
101
  $self->{diag_per_module}{$module} = $output;
102

  
103
  my $parser = TAP::Parser->new({ tap => $output});
104
  $parser->run;
105

  
106
  $self->aggreg->add($module => $parser);
107
}
108

  
109
sub _email_user {
110
  $_[0]{email_user} ||= SL::DB::Manager::AuthUser->find_by(login => $_[0]->config->{send_email_to});
111
}
112

  
113
sub _send_email {
114
  my ($self) = @_;
115

  
116
  return if !$self->config || !$self->config->{send_email_to};
117

  
118
  my $user  = $self->_email_user;
119
  my $email = $user ? $user->get_config_value('email') : undef;
120

  
121
  return unless $email;
122

  
123
  my ($output, $content_type) = $self->_prepare_report;
124

  
125
  my $mail              = Mailer->new(charset => $::locale->is_utf8 ? 'UTF-8' : Common->DEFAULT_CHARSET );
126
  $mail->{from}         = $self->config->{email_from};
127
  $mail->{to}           = $email;
128
  $mail->{subject}      = $self->config->{email_subject};
129
  $mail->{content_type} = $content_type;
130
  $mail->{message}      = $$output;
131

  
132
  $mail->send;
133
}
134

  
135
sub _prepare_report {
136
  my ($self) = @_;
137

  
138
  my $user = $self->_email_user;
139
  my $template = Template->new({ 'INTERPOLATE' => 0,
140
                                 'EVAL_PERL'   => 0,
141
                                 'ABSOLUTE'    => 1,
142
                                 'CACHE_SIZE'  => 0,
143
                               });
144

  
145
  return unless $template;
146
  my $email_template = $self->config->{email_template};
147
  my $filename       = $email_template || ( ($user->get_config_value('templates') || "templates/mails") . "/self_test/status_mail.txt" );
148
  my $content_type   = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
149

  
150

  
151
  my %params = (
152
    SELF     => $self,
153
    host     => hostname,
154
    database => $::myconfig{dbname},
155
    path     => $FindBin::Bin,
156
  );
157

  
158
  my $output;
159
  $template->process($filename, \%params, \$output) || die $template->error;
160

  
161
  return (\$output, $content_type);
162
}
163

  
164
sub log {
165
  my $self = shift;
166
  $::lxdebug->message(0, "[" . __PACKAGE__ . "] @_") if $self->config->{log_to_file};
167
}
168

  
169

  
170
1;
171

  
172
__END__
173

  
174
=head1 NAME
175

  
176
SL::BackgroundJob::TelfTests - pluggable self testing
177

  
178
=head1 SYNOPSIS
179

  
180
  use SL::BackgroundJob::SelfTests;
181
  SL::BackgroundJob::SelfTests->new->run;;
182

  
183
=head1 DESCRIPTION
184

  
185

  
186

  
187
=head1 FUNCTIONS
188

  
189
=head1 BUGS
190

  
191
=head1 AUTHOR
192

  
193
=cut
SL/BackgroundJob/SelfTest/Base.pm
1
package SL::BackgroundJob::SelfTest::Base;
2

  
3
use Test::Builder;
4

  
5
use parent qw(Rose::Object);
6

  
7
use Rose::Object::MakeMethods::Generic (
8
  'scalar --get_set_init' => 'tester',
9
);
10

  
11
sub run {
12
  my ($self) = @_;
13
  die 'needs to be overwritten';
14
}
15

  
16
sub todo {
17
  0
18
}
19

  
20
sub skipped {
21
  0
22
}
23

  
24

  
25
sub init_tester {
26
  Test::Builder->new;
27
}
28

  
29
1;
30

  
31
__END__
32

  
33
=encoding utf-8
34

  
35
=head1 NAME
36

  
37
SL::BackgroundJob::SelfTests::Base - Base class for background job self tests.
38

  
39
=head1 SYNOPSIS
40

  
41
  # in self test:
42
  use parent qw(SL::BackgroundJob::SelfTests::Base);
43

  
44
  # optionally use a different tester
45
  sub init_tester {
46
    Test::Deeply->new;
47
  }
48

  
49
  # implement interface
50
  sub run {
51
    my $self = shift;
52

  
53
    $self->tester->plan(tests => 1);
54

  
55
    $self->tester->ok($value_from_database == $expected_value, 'short explanation');
56
  }
57

  
58
=head1 DESCRIPTION
59

  
60
This is a base class for self tests.
61

  
62
=head1 INTERFACE
63

  
64
Your class will inherit L<Rose::Object> so you can use the class building utils
65
from there, and won't need to worry about writing a new constructor.
66

  
67
Your test will be instanciated and the run method will be invoked. The output
68
of your tester object will be collected and processed.
69

  
70
=head2 THE TESTER
71

  
72
=over 4
73

  
74
=item E<tester>
75

  
76
=item E<init_tester>
77

  
78
If you don't bother overriding E<init_tester>, your test will use a
79
L<Test::More> object by default. Any other L<Test::Builder> object will do.
80

  
81
The TAP output of your builder will be collected and processed for further handling.
82

  
83
=back
84

  
85
=head1 ERROR HANDLING
86

  
87
If a self test module dies, it will be recorded as failed, and the bubbled
88
exception will be used as diagnosis.
89

  
90
=head1 TODO
91

  
92
It is currently not possible to indicate if a test skipped (indicating no actual testing was done but it wasn't an error) nor returning a todo status (indicating that the test failed, but that being ok, because it's a todo).
93

  
94
Stub methods "todo" and "skipped" exist, but are currently not used.
95

  
96
=head1 AUTHOR
97

  
98
Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>
99

  
100
=cut
SL/BackgroundJob/SelfTest/Transactions.pm
1
package SL::BackgroundJob::SelfTest::Transactions;
2

  
3
use utf8;
4
use strict;
5
use parent qw(SL::BackgroundJob::SelfTest::Base);
6

  
7
use SL::DBUtils;
8

  
9
use Rose::Object::MakeMethods::Generic (
10
  scalar => [ qw(dbh fromdate todate) ],
11
);
12

  
13
sub run {
14
  my ($self) = @_;
15

  
16
  $self->_setup;
17

  
18
  $self->tester->plan(tests => 14);
19

  
20
  $self->check_konten_mit_saldo_nicht_in_guv;
21
  $self->check_balanced_individual_transactions;
22
  $self->check_verwaiste_acc_trans_eintraege;
23
  $self->check_netamount_laut_invoice_ar;
24
  $self->check_invnumbers_unique;
25
  $self->check_summe_stornobuchungen;
26
  $self->check_ar_paid;
27
  $self->check_ap_paid;
28
  $self->check_ar_overpayments;
29
  $self->check_ap_overpayments;
30
  $self->check_paid_stornos;
31
  $self->check_stornos_ohne_partner;
32
  $self->check_overpayments;
33
  $self->calc_saldenvortraege;
34
}
35

  
36
sub _setup {
37
  my ($self) = @_;
38

  
39
  # TODO FIXME calc dates better, unless this is wanted
40
  $self->fromdate(DateTime->new(day => 1, month => 1, year => DateTime->today->year));
41
  $self->todate($self->fromdate->clone->add(years => 1)->add(days => -1));
42

  
43
  $self->dbh($::form->get_standard_dbh);
44
}
45

  
46
sub check_konten_mit_saldo_nicht_in_guv {
47
  my ($self) = @_;
48

  
49
  my $query = qq|
50
    SELECT c.accno, c.description, c.category, SUM(a.amount) AS Saldo
51
    FROM chart c,
52
         acc_trans a
53
    WHERE c.id = a.chart_id
54
     and  (c.category like 'I' or c.category like 'E')
55
     and  amount != 0
56
     and  pos_eur is null
57
         and  a.transdate >= ? and a.transdate <= ?
58
    GROUP BY c.accno,c.description,c.category,c.pos_bilanz,c.pos_eur
59
    ORDER BY c.accno|;
60

  
61
  my $konten_nicht_in_guv =  selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
62

  
63
  my $correct = 0 == scalar grep { $_->{Saldo} } @$konten_nicht_in_guv;
64

  
65
  $self->tester->ok($correct, "Erfolgskonten mit Saldo nicht in GuV (Saldenvortragskonten können ignoriert werden, sollten aber 0 sein)");
66
  if (!$correct) {
67
    for my $konto (@$konten_nicht_in_guv) {
68
      $self->tester->diag($konto);
69
    }
70
  }
71
}
72

  
73
sub check_balanced_individual_transactions {
74
  my ($self) = @_;
75

  
76
  my $query = qq|
77
    select sum(ac.amount) as amount,trans_id,ar.invnumber as ar,ap.invnumber as ap,gl.reference as gl
78
      from acc_trans ac
79
      left join ar on (ar.id = ac.trans_id)
80
      left join ap on (ap.id = ac.trans_id)
81
      left join gl on (gl.id = ac.trans_id)
82
    where ac.transdate >= ? AND ac.transdate <= ?
83
    group by trans_id,ar.invnumber,ap.invnumber,gl.reference
84
    having sum(ac.amount) != 0;|;
85

  
86
  my $acs = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
87
  if (@$acs) {
88
    $self->tester->ok(0, "Es gibt unausgeglichene acc_trans-Transaktionen:");
89
    for my $ac (@{ $acs }) {
90
      $self->tester->diag("trans_id: $ac->{trans_id},  amount = $ac->{amount}, ar: $ac->{ar} ap: $ac->{ap} gl: $ac->{gl}");
91
    }
92
  } else {
93
    $self->tester->ok(1, "Alle acc_trans Transaktionen ergeben in Summe 0, keine unausgeglichenen Transaktionen");
94
  }
95
}
96

  
97
sub check_verwaiste_acc_trans_eintraege {
98
  my ($self) = @_;
99

  
100
  my $query = qq|
101
      select trans_id,amount,accno,description from acc_trans a
102
    left join chart c on (c.id = a.chart_id)
103
    where trans_id not in (select id from gl union select id from ar union select id from ap order by id)
104
      and a.transdate >= ? and a.transdate <= ? ;|;
105

  
106
  my $verwaiste_acs = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
107
  if (@$verwaiste_acs) {
108
     $self->tester->ok(0, "Es gibt verwaiste acc-trans Einträge! (wo ar/ap/gl-Eintrag fehlt)");
109
     $self->tester->diag($_) for @$verwaiste_acs;
110
  } else {
111
     $self->tester->ok(1, "Keine verwaisten acc-trans Einträge (wo ar/ap/gl-Eintrag fehlt)");
112
  }
113
}
114

  
115
sub check_netamount_laut_invoice_ar {
116
  my ($self) = @_;
117
  my $query = qq|
118
    select sum(round(cast(i.qty*(i.fxsellprice * (1-i.discount)) as numeric), 2))
119
    from invoice i
120
    left join ar a on (a.id = i.trans_id)
121
    where a.transdate >= ? and a.transdate <= ?;|;
122
  my ($netamount_laut_invoice) =  selectfirst_array_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
123

  
124
  $query = qq| select sum(netamount) from ar where transdate >= ? and transdate <= ?; |;
125
  my ($netamount_laut_ar) =  selectfirst_array_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
126

  
127
  my $correct = $netamount_laut_invoice - $netamount_laut_ar == 0;
128

  
129
  $self->tester->ok($correct, "Summe laut Verkaufsbericht sollte gleich Summe aus Verkauf -> Berichte -> Rechnungen sein");
130
  if (!$correct) {
131
    $self->tester->diag("Netto-Summe laut Verkaufsbericht (invoice): $netamount_laut_invoice");
132
    $self->tester->diag("Netto-Summe laut Verkauf -> Berichte -> Rechnungen: $netamount_laut_ar");
133
  }
134
}
135

  
136
sub check_invnumbers_unique {
137
  my ($self) = @_;
138

  
139
  my $query = qq| select  invnumber,count(invnumber) as count from ar
140
               where transdate >= ? and transdate <= ?
141
               group by invnumber
142
               having count(invnumber) > 1; |;
143
  my $non_unique_invnumbers =  selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
144

  
145
  if (@$non_unique_invnumbers) {
146
    $self->tester->ok(0, "Es gibt doppelte Rechnungsnummern");
147
    for my $invnumber (@{ $non_unique_invnumbers }) {
148
      $self->tester->diag("invnumber: $invnumber->{invnumber}    $invnumber->{count}x");
149
    }
150
  } else {
151
    $self->tester->ok(1, "Alle Rechnungsnummern sind eindeutig");
152
  }
153
}
154

  
155
sub check_summe_stornobuchungen {
156
  my ($self) = @_;
157

  
158
  my $query = qq|
159
    select sum(amount) from ar a JOIN customer c ON (a.customer_id = c.id)
160
    WHERE storno is true
161
      AND a.transdate >= ? and a.transdate <= ?|;
162
  my ($summe_stornobuchungen_ar) = selectfirst_array_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
163

  
164
  $query = qq|
165
    select sum(amount) from ap a JOIN vendor c ON (a.vendor_id = c.id)
166
    WHERE storno is true
167
      AND a.transdate >= ? and a.transdate <= ?|;
168
  my ($summe_stornobuchungen_ap) = selectfirst_array_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
169

  
170
  $self->tester->ok($summe_stornobuchungen_ap == 0, 'Summe aller Einkaufsrechnungen (stornos + stronierte) soll 0 sein');
171
  $self->tester->ok($summe_stornobuchungen_ar == 0, 'Summe aller Verkaufsrechnungen (stornos + stronierte) soll 0 sein');
172
  $self->tester->diag("Summe Einkaufsrechnungen (ar): $summe_stornobuchungen_ar") if $summe_stornobuchungen_ar;
173
  $self->tester->diag("Summe Einkaufsrechnungen (ap): $summe_stornobuchungen_ap") if $summe_stornobuchungen_ap;
174
}
175

  
176
sub check_ar_paid {
177
  my ($self) = @_;
178

  
179
  my $query = qq|
180
      select invnumber,paid,
181
           (select sum(amount) from acc_trans a left join chart c on (c.id = a.chart_id) where trans_id = ar.id and c.link like '%AR_paid%') as accpaid ,
182
           paid+(select sum(amount) from acc_trans a left join chart c on (c.id = a.chart_id) where trans_id = ar.id and c.link like '%AR_paid%') as diff
183
    from ar
184
    where
185
          (select sum(amount) from acc_trans a left join chart c on (c.id = a.chart_id) where trans_id = ar.id and c.link like '%AR_paid%') is not null
186
            AND storno is false
187
      AND transdate >= ? and transdate <= ?
188
    order by diff |;
189

  
190
  my $paid_diffs_ar = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
191

  
192
  my $errors = scalar grep { $_->{diff} != 0 } @$paid_diffs_ar;
193

  
194
  $self->tester->ok(!$errors, "Vergleich ar paid mit acc_trans AR_paid");
195

  
196
  for my $paid_diff_ar (@{ $paid_diffs_ar }) {
197
    next if $paid_diff_ar->{diff} == 0;
198
    $self->tester->diag("ar invnumber: $paid_diff_ar->{invnumber} : paid: $paid_diff_ar->{paid}    acc_paid= $paid_diff_ar->{accpaid}    diff: $paid_diff_ar->{diff}");
199
  }
200
}
201

  
202
sub check_ap_paid {
203
  my ($self) = @_;
204

  
205
  my $query = qq|
206
      select invnumber,paid,
207
            (select sum(amount) from acc_trans a left join chart c on (c.id = a.chart_id) where trans_id = ap.id and c.link like '%AP_paid%') as accpaid ,
208
            paid-(select sum(amount) from acc_trans a left join chart c on (c.id = a.chart_id) where trans_id = ap.id and c.link like '%AP_paid%') as diff
209
     from ap
210
     where
211
           (select sum(amount) from acc_trans a left join chart c on (c.id = a.chart_id) where trans_id = ap.id and c.link like '%AP_paid%') is not null
212
       AND transdate >= ? and transdate <= ?
213
     order by diff |;
214

  
215
  my $paid_diffs_ap = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
216

  
217
  my $errors = scalar grep { $_->{diff} != 0 } @$paid_diffs_ap;
218

  
219
  $self->tester->ok(!$errors, "Vergleich ap paid mit acc_trans AP_paid");
220
  for my $paid_diff_ap (@{ $paid_diffs_ap }) {
221
     next if $paid_diff_ap->{diff} == 0;
222
     $self->tester->diag("ap invnumber: $paid_diff_ap->{invnumber} : paid: $paid_diff_ap->{paid}    acc_paid= $paid_diff_ap->{accpaid}    diff: $paid_diff_ap->{diff}");
223
  }
224
}
225

  
226
sub check_ar_overpayments {
227
  my ($self) = @_;
228

  
229
  my $query = qq|
230
       select invnumber,paid,amount,transdate,c.customernumber,c.name from ar left join customer c on (ar.customer_id = c.id)
231
     where abs(paid) > abs(amount)
232
       AND transdate >= ? and transdate <= ?
233
         order by invnumber;|;
234

  
235
  my $overpaids_ar =  selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
236

  
237
  my $correct = 0 == @$overpaids_ar;
238

  
239
  $self->tester->ok($correct, "Keine Überzahlungen laut ar.paid");
240
  for my $overpaid_ar (@{ $overpaids_ar }) {
241
    $self->tester->diag("ar invnumber: $overpaid_ar->{invnumber} : paid: $overpaid_ar->{paid}    amount= $overpaid_ar->{amount}  transdate = $overpaid_ar->{transdate}");
242
  }
243
}
244

  
245
sub check_ap_overpayments {
246
  my ($self) = @_;
247

  
248
  my $query = qq|
249
      select invnumber,paid,amount,transdate,vc.vendornumber,vc.name from ap left join vendor vc on (ap.vendor_id = vc.id)
250
    where abs(paid) > abs(amount)
251
      AND transdate >= ? and transdate <= ?
252
        order by invnumber;|;
253

  
254
  my $overpaids_ap =  selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
255

  
256
  my $correct = 0 == @$overpaids_ap;
257

  
258
  $self->tester->ok($correct, "Überzahlungen laut ap.paid:");
259
  for my $overpaid_ap (@{ $overpaids_ap }) {
260
    $self->tester->diag("ap invnumber: $overpaid_ap->{invnumber} : paid: $overpaid_ap->{paid}    amount= $overpaid_ap->{amount}  transdate = $overpaid_ap->{transdate}");
261
  }
262
}
263

  
264
sub check_paid_stornos {
265
  my ($self) = @_;
266

  
267
  my $query = qq|
268
    SELECT ar.invnumber,sum(amount - COALESCE((SELECT sum(amount)*-1 FROM acc_trans LEFT JOIN chart ON (acc_trans.chart_id=chart.id) WHERE link ilike '%paid%' AND acc_trans.trans_id=ar.id ),0)) as "open"
269
    FROM ar, customer
270
    WHERE paid != amount
271
      AND ar.storno
272
      AND (ar.customer_id = customer.id)
273
      AND ar.transdate >= ? and ar.transdate <= ?
274
    GROUP BY ar.invnumber;|;
275
  my $paid_stornos = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
276

  
277
  $self->tester->ok(0 == @$paid_stornos, "Keine bezahlten Stornos");
278
  for my $paid_storno (@{ $paid_stornos }) {
279
    $self->tester->diag("invnumber: $paid_storno->{invnumber}   offen: $paid_storno->{open}");
280
  }
281
}
282

  
283
sub check_stornos_ohne_partner {
284
  my ($self) = @_;
285

  
286
  my $query = qq|
287
      select ar.id,invnumber,storno,amount,transdate,type,customernumber
288
    from ar
289
    left join customer c on (c.id = ar.customer_id)
290
    where storno_id is null and storno is true and ar.id not in (select storno_id from ar where storno_id is not null and storno is true);
291
  |;
292
  my $stornos_ohne_partner =  selectall_hashref_query($::form, $self->dbh, $query);
293

  
294
  $self->tester->ok(@$stornos_ohne_partner == 0, 'Es sollte keine Stornos ohne Partner geben');
295
  if (@$stornos_ohne_partner) {
296
    $self->tester->diag("stornos ohne partner:   (kann passieren wenn Stornorechnung außerhalb Zeitraum liegt)");
297
    $self->tester->diag("gilt aber trotzdem als paid zu dem Zeitpunkt, oder?");
298
  }
299
  my $stornoheader = 0;
300
  for my $storno (@{ $stornos_ohne_partner }) {
301
    if (!$stornoheader++) {
302
      $self->tester->diag(join "\t", keys %$storno);
303
    }
304
    $self->tester->diag(join "\t", map { $storno->{$_} } keys %$storno);
305
  }
306
}
307

  
308
sub check_overpayments {
309
  my ($self) = @_;
310

  
311
  # Vergleich ar.paid und das was laut acc_trans bezahlt wurde
312
  # "als bezahlt markieren" ohne sauberes Ausbuchen führt zu Differenzen bei offenen Forderungen
313
  # geht nur auf wenn acc_trans Zahlungseingänge auch im Untersuchungszeitraum lagen
314
  # Stornos werden rausgefiltert
315
  my $query = qq|
316
SELECT
317
invnumber,customernumber,name,ar.transdate,ar.datepaid,
318
amount,
319
amount-paid as "open via ar",
320
paid as "paid via ar",
321
coalesce((SELECT sum(amount)*-1 FROM acc_trans LEFT JOIN chart ON (acc_trans.chart_id=chart.id) WHERE link ilike '%paid%' AND acc_trans.trans_id=ar.id AND acc_trans.transdate <= ?),0) as "paid via acc_trans"
322
FROM ar left join customer c on (c.id = ar.customer_id)
323
WHERE
324
 (ar.storno IS FALSE)
325
 AND (transdate <= ? )
326
;|;
327

  
328
  my $invoices = selectall_hashref_query($::form, $self->dbh, $query, $self->todate, $self->todate);
329

  
330
  my $count_overpayments = scalar grep {
331
       $_->{"paid via ar"} != $_->{"paid via acc_trans"}
332
    || (    $_->{"amount"} - $_->{"paid via acc_trans"} != $_->{"open via ar"}
333
         && $_->{"paid via ar"} != $_->{"paid via acc_trans"} )
334
  } @$invoices;
335

  
336
  $self->tester->ok($count_overpayments == 0, 'Vergleich ar.paid und das was laut acc_trans bezahlt wurde');
337

  
338
  if ($count_overpayments) {
339
    for my $invoice (@{ $invoices }) {
340
      if ($invoice->{"paid via ar"} != $invoice->{"paid via acc_trans"}) {
341
        $self->tester->diag("paid via ar (@{[ $invoice->{'paid via ar'} * 1 ]}) !=   paid via acc_trans  (@{[ $invoice->{'paid via acc_trans'} * 1 ]}) (at least until transdate!)");
342
        if (defined $invoice->{datepaid}) {
343
          $self->tester->diag("datepaid = $invoice->{datepaid})");
344
        }
345
        $self->tester->diag("Überzahlung!") if $invoice->{"paid via acc_trans"} > $invoice->{amount};
346
      } elsif ( $invoice->{"amount"} - $invoice->{"paid via acc_trans"} != $invoice->{"open via ar"} && $invoice->{"paid via ar"} != $invoice->{"paid via acc_trans"}) {
347
        $self->tester->diag("amount - paid_via_acc_trans !=  open_via_ar");
348
        $self->tester->diag("Überzahlung!") if $invoice->{"paid via acc_trans"} > $invoice->{amount};
349
      } else {
350
        # nothing wrong
351
      }
352
    }
353
  }
354
}
355

  
356
sub calc_saldenvortraege {
357
  my ($self) = @_;
358

  
359
  my $saldenvortragskonto = '9000';
360

  
361
  # Saldo Saldenvortragskonto 9000 am Jahresanfang
362
  my $query = qq|
363
      select sum(amount) from acc_trans where chart_id = (select id from chart where accno = ?) and transdate <= ?|;
364
  my ($saldo_9000_jahresanfang) = selectfirst_array_query($::form, $self->dbh, $query, $saldenvortragskonto, DateTime->new(day => 1, month => 1, year => DateTime->today->year));
365
  $self->tester->diag("Saldo 9000 am 01.01.@{[DateTime->today->year]}: @{[ $saldo_9000_jahresanfang * 1 ]}    (sollte 0 sein)");
366

  
367
    # Saldo Saldenvortragskonto 9000 am Jahresende
368
  $query = qq|
369
      select sum(amount) from acc_trans where chart_id = (select id from chart where accno = ?) and transdate <= ?|;
370
  my ($saldo_9000_jahresende) = selectfirst_array_query($::form, $self->dbh, $query, $saldenvortragskonto, DateTime->new(day => 31, month => 12, year => DateTime->today->year));
371
  $self->tester->diag("Saldo $saldenvortragskonto am 31.12.@{[DateTime->today->year]}: @{[ $saldo_9000_jahresende * 1 ]}    (sollte 0 sein)");
372
}
373

  
374
1;
375

  
376
__END__
377

  
378
=encoding utf-8
379

  
380
=head1 NAME
381

  
382
SL::BackgroundJob::SelfTest::Transactions - base tests
383

  
384
=head1 DESCRIPTION
385

  
386
Several tests for data integrity.
387

  
388
=head1 FUNCTIONS
389

  
390
=head1 BUGS
391

  
392
=head1 AUTHOR
393

  
394
Geoffrey Richardsom E<lt>information@richardsonbueren.deE<gt>
395
Jan Büren E<lt>information@richardsonbueren.deE<gt>
396
Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>
397

  
398
=cut
399

  
config/lx_office.conf.default
163 163
# The template file used for the email's body.
164 164
email_template = templates/webpages/oe/periodic_invoices_email.txt
165 165

  
166
[self_test]
167

  
168
# modules to be tested
169
# Add without SL::BackgroundJob::SelfTest:: prefix
170
# Separate with space.
171
modules = Transactions
172

  
173
# you probably don't want to be spammed with "everything ok" every day. enable
174
# this when you add new tests to make sure they run correctly for a few days
175
send_email_on_success = 0
176

  
177
# will log into the standard logfile
178
log_to_file = 0
179

  
180
# user login (!) to send the email to.
181
send_email_to  =
182
# will be used to send your report mail
183
email_from     =
184
# The subject line for your report mail
185
email_subject  = kivitendo self test report
186
# template. currently txt and html templates are recognized and correctly mime send.
187
email_template = templates/mail/self_test/status_mail.txt
188

  
166 189
[datev_check]
167 190
# it is possible to make a quick DATEV export everytime you post a record to ensure things
168 191
# work nicely with their data requirements. This will result in a slight overhead though
doc/changelog
19 19

  
20 20
Experimentelle Features:
21 21

  
22
- Automatisierte Selbsttests
23
  Es gibt jetzt ein Grundgerüst um Selbsttests durchzuführen, und bei Problemen
24
  einen Administrator per Mail zu benachrichtigen. Die Selbsttests werden Über
25
  das SelfTest Modul für den Taskserver verwaltet, und in config/lx_office.conf
26
  im Block [self_test] konfiguriert. Die Tests werden in TAP ausgeliefert und
27
  können bei Bedarf weiter maschinell ausgewertet werden.
28

  
29
  Zur Demonstration gibt es einen Selbsttest Transactions, der die Datenbank
30
  auf Fehlbuchungen untersucht.
31

  
22 32
- Es ist möglich benutzerdefinierte Variablen vom Typ "Lieferant" und "Ware"
23 33
  anzulegen. Für die Auswahl in den webpages steht ein L.vendor_selector und
24 34
  ein L.part_selector zur Verfügung, der einfach das select_tag verwendet.
locale/de/all
432 432
  'Corrections'                 => 'Korrekturen',
433 433
  'Costs'                       => 'Kosten',
434 434
  'Could not copy %s to %s. Reason: %s' => 'Die Datei &quot;%s&quot; konnte nicht nach &quot;%s&quot; kopiert werden. Grund: %s',
435
  'Could not load class #1 (#2): "#3"' => 'Konnte Klasse #1 (#2) nicht laden: "#3"',
436
  'Could not load class #1, #2' => 'Konnte Klasse #1 nicht laden: "#2"',
435 437
  'Could not load employee'     => 'Konnte Benutzer nicht laden',
436 438
  'Could not open the file users/members.' => 'Die Datei &quot;users/members&quot; konnte nicht ge&ouml;ffnet werden.',
437 439
  'Could not open the old memberfile.' => 'Die Datei mit den Benutzerdaten konnte nicht ge&ouml;ffnet werden.',
sql/Pg-upgrade2/self_test_background_job.pl
1
# @tag: self_test_background_job
2
# @description: Hintergrundjob für tägliche Selbsttests
3
# @depends: release_2_7_0
4
# @charset: utf-8
5

  
6
use strict;
7

  
8
use SL::BackgroundJob::SelfTest;
9

  
10
SL::BackgroundJob::SelfTest->create_job;
11

  
12
1;
templates/mail/self_test/status_mail.txt
1
kivitendo selftest report.
2

  
3
Host:   [% host %]
4
Path:   [% path %]
5
DB:     [% database %]
6
Result: [% SELF.aggreg.get_status %]
7

  
8
------------
9
Full report:
10
------------
11

  
12
[% FOREACH module = SELF.diag_per_module.keys %]
13
Module: [% module %]
14
--------------------
15

  
16
[% SELF.diag_per_module.$module %]
17

  
18
[% END %]

Auch abrufbar als: Unified diff