]> wagnertech.de Git - mfinanz.git/blob - SL/Controller/ListTransactions.pm
Merge branch 'master' of http://wagnertech.de/git/mfinanz
[mfinanz.git] / SL / Controller / ListTransactions.pm
1 package SL::Controller::ListTransactions;
2
3 use strict;
4 use parent qw(SL::Controller::Base);
5
6 use POSIX qw(strftime);
7 use List::Util qw(first);
8 use Archive::Zip;
9
10 use SL::DB::Chart;
11 use SL::DB::AccTransaction;
12 use SL::CA;
13
14 use SL::ReportGenerator;
15 use SL::Controller::Helper::ReportGenerator;
16 use SL::Locale::String;
17 use SL::SessionFile::Random;
18 use SL::Helper::Flash qw(flash);
19 use SL::Presenter::EscapedText qw(escape);
20
21 use SL::Presenter::DatePeriod qw(get_dialog_defaults_from_report_generator
22                                   populate_hidden_variables);
23
24 use Rose::Object::MakeMethods::Generic (
25   scalar                  => [ qw(defaults title account from_date to_date report_type report) ],
26   'scalar --get_set_init' => [ qw(accounts_list) ],
27 );
28
29 __PACKAGE__->run_before(sub { $::auth->assert('report'); });
30
31 # actions
32
33 sub action_report_settings {
34   my ($self) = @_;
35
36   $self->set_defaults;
37
38   # if we're coming from a linked entry we want to pre-select the chart picker
39   if ($::form->{link}) {
40     my $account = first { $_->{accno} eq $::form->{accno} } @{ $self->accounts_list };
41     $self->defaults->{chart_id} = $account->{chart_id};
42   }
43
44   $self->setup_report_settings_action_bar;
45   $self->render('list_transactions/report_settings',
46     title => t8('List Transactions'),
47     accounts_list => $self->accounts_list,
48     defaults => $self->defaults,
49   );
50 }
51
52 sub action_list {
53   my ($self) = @_;
54
55   $self->set_dates;
56
57   $self->report_type('HTML');
58
59   # set account number from chart picker chart_id
60   my $account = first { $_->{chart_id} eq $::form->{chart_id} } @{ $self->accounts_list };
61   if (!$account) {
62     flash('error', t8('No account selected. Please select an account.'));
63     return $self->action_report_settings;
64   }
65   $::form->{accno} = $account->{accno};
66
67   $self->set_title;
68
69   $self->setup_list_action_bar;
70   $self->prepare_report;
71   $self->set_report_data;
72   $self->report->generate_with_headers;
73 }
74
75 sub action_export_options_all_charts {
76   my ($self) = @_;
77
78   $self->set_dates;
79
80   # misusing the set_defaults function here a bit to easily
81   # get the values from the form,
82   # we have to get the values here because we have to forward them
83   # to the csv options form using a hidden array
84   $self->set_defaults;
85
86   $self->defaults->{fromdate} = $self->from_date;
87   $self->defaults->{todate} = $self->to_date;
88
89   # dialog state is returned in a nested hash, this has to be flattened here
90   my @hidden;
91   push @hidden, map {
92     { key => 'dateperiod_selected_preset_' . $_, value => $self->defaults->{dialog}->{$_} }
93   } keys %{ $self->defaults->{dialog} };
94   delete($self->defaults->{dialog});
95
96   # handle the rest of the state
97   push @hidden, map {
98     { key => $_, value => $self->defaults->{$_} }
99   } keys %{ $self->defaults };
100
101   if ($::form->{output_format} eq 'PDF') {
102     $self->setup_export_options_action_bar(output_format => 'PDF');
103     $self->render('report_generator/pdf_export_options',
104       title => t8('PDF export -- options'),
105       HIDDEN => \@hidden,
106     );
107   } else {
108     $self->setup_export_options_action_bar(output_format => 'CSV');
109     $self->render('report_generator/csv_export_options',
110       title => t8('CSV export -- options'),
111       HIDDEN => \@hidden,
112     );
113   }
114 }
115
116 sub action_export_all_charts {
117   my ($self) = @_;
118
119   my $output_format = $::form->{output_format} // 'CSV';
120
121   my $zip = Archive::Zip->new();
122
123   for my $account (@{ $self->accounts_list }) {
124     next if $account->{charttype} eq "H" || !defined($account->{balance});
125
126     $::form->{accno} = $account->{accno};
127
128     my $sfile = SL::SessionFile::Random->new(mode => "w");
129
130     $self->set_title;
131     $self->report_type($output_format);
132
133     $self->prepare_report;
134     $self->set_report_data;
135     if  ($output_format eq 'PDF') {
136       my $output = $self->report->generate_pdf_content(want_binary_pdf => 1);
137       $sfile->fh->print($output);
138     } else {
139       $self->report->_generate_csv_content($sfile->fh);
140     }
141     $sfile->fh->close;
142
143     # we need to sanitize the account number before using it in the filename
144     # to prevent unexpected outcomes due to slashes etc.
145     my $sanitized_accno = $account->{accno} =~ s/[^A-Za-z0-9\-\.\_\ ]/_/gr;
146
147     $zip->addFile(
148       $sfile->{file_name},
149       t8('list_of_transactions') . "_" . t8('account') . "_" . $sanitized_accno . ($output_format eq 'PDF' ? '.pdf' : '.csv')
150     );
151   }
152
153   my $zipfile = SL::SessionFile::Random->new(mode => "w");
154   unless ( $zip->writeToFileNamed($zipfile->file_name) == Archive::Zip::AZ_OK ) {
155     die 'zipfile write error';
156   }
157   $zipfile->fh->close;
158
159   $self->send_file(
160     $zipfile->file_name,
161     type => 'application/zip',
162     name => t8('list_of_transactions') . strftime('_%Y%m%d', localtime time) . '.zip',
163   );
164 }
165
166 # local functions
167
168 sub set_defaults {
169   my ($self) = @_;
170
171   # use values from form, then report generator form, then fallback
172   my %fallback = (
173     #accno         => $self->accounts_list->[0]->{accno},
174     chart_id      => '',
175     reporttype    => 'custom',
176     year          => DateTime->today->year,
177     duetyp        => '13',
178     dateperiod_from_date => '',
179     dateperiod_to_date => '',
180     show_subtotals => 0,
181     sort          => 'transdate',
182   );
183   my %defaults;
184   for (keys %fallback) {
185     $defaults{$_} = $::form->{$_} // $::form->{'report_generator_hidden_' . $_} // $fallback{$_};
186   }
187
188   $defaults{dialog} = get_dialog_defaults_from_report_generator('dateperiod');
189
190   $self->defaults(\%defaults);
191 }
192
193 sub set_title {
194   my ($self) = @_;
195   my $account = first { $_->{accno} eq $::form->{accno} } @{ $self->accounts_list };
196   $self->title(escape(join(" ", t8('List Transactions'), t8('Account'), $account->{text})));
197 }
198
199 sub set_dates {
200   my ($self) = @_;
201
202   # set dates according to selection
203   $self->from_date($::form->{dateperiod_from_date});
204   $self->to_date($::form->{dateperiod_to_date});
205
206   # set this into form here for the CA-> routines
207   $::form->{fromdate} = $self->from_date;
208   $::form->{todate}   = $self->to_date;
209   # (no further checks needed, a reasonable error is shown when dates are invalid)
210 }
211
212 sub prepare_report {
213   my ($self) = @_;
214
215   $self->report(SL::ReportGenerator->new(\%::myconfig, $::form));
216
217   my @columns     = qw(transdate reference description gegenkonto debit credit ustkonto ustrate balance);
218   my %column_defs = (
219     transdate   => { text => t8('Date'), },
220     reference   => { text => t8('Reference'), },
221     description => { text => t8('Description'), },
222     debit       => { text => t8('Debit'), },
223     credit      => { text => t8('Credit'), },
224     gegenkonto  => { text => t8('Gegenkonto'), },
225     ustkonto    => { text => t8('USt-Konto'), },
226     balance     => { text => t8('Balance'), },
227     ustrate     => { text => t8('Satz %'), },
228   );
229
230   $self->report->set_options(
231     std_column_visibility => 1,
232     controller_class      => 'ListTransactions',
233     output_format         => $self->report_type,
234     title                 => $self->title,
235     allow_pdf_export      => 1,
236     allow_csv_export      => 1,
237     allow_chart_export    => 0,
238     attachment_basename   => t8('list_of_transactions') . strftime('_%Y%m%d', localtime time),
239     top_info_text         => $self->get_top_info_text,
240   );
241   $self->report->set_columns(%column_defs);
242   $self->report->set_column_order(@columns);
243
244   my @hidden_variables = qw(accno chart_id show_subtotals sort);
245   populate_hidden_variables('dateperiod', \@hidden_variables);
246
247   $self->report->set_export_options(qw(list), @hidden_variables);
248   $self->report->set_options_from_form;
249   $self->report->set_sort_indicator($::form->{sort}, 1);
250   # this is getting triggered but doesn't seem to have an effect
251   #$::locale->set_numberformat_wo_thousands_separator(\%::myconfig) if lc($self->report->{options}->{output_format}) eq 'csv';
252 }
253
254 sub set_report_data {
255   my ($self) = @_;
256
257   CA->all_transactions(\%::myconfig, \%$::form);
258
259   # this data is used in custom header
260   $self->{eb_value} = $::form->{beginning_balance};
261   $self->{saldo_old} = $::form->{saldo_old} + $::form->{beginning_balance};
262   # "Jahresverkehrszahlen alt"
263   $self->{debit_old} = $::form->{old_balance_debit};
264   $self->{credit_old} = $::form->{old_balance_credit};
265
266   $self->set_report_custom_headers();
267
268   # initialise totals
269   $self->{total_debit} = 0.;
270   $self->{total_credit} = 0.;
271   my $subtotal_debit = 0.;
272   my $subtotal_credit = 0.;
273   $self->{balance} = $self->{saldo_old};
274
275   # used for subtotals below
276   my $sort_key = $::form->{sort};
277
278   my $idx = 0;
279   for my $tr (@{ $::form->{CA} }) {
280
281     # sum up totals
282     $self->{total_debit} += $tr->{debit};
283     $self->{total_credit} += $tr->{credit};
284     $subtotal_debit += $tr->{debit};
285     $subtotal_credit += $tr->{credit};
286     $self->{balance} -= $tr->{debit};
287     $self->{balance} += $tr->{credit};
288
289     # formatting
290     my $credit = $tr->{credit} ? $::form->format_amount(\%::myconfig, $tr->{credit}, 2) : '0';
291     my $debit = $tr->{debit} ? $::form->format_amount(\%::myconfig, $tr->{debit}, 2) : '0';
292     my $ustrate = '';
293     if ($tr->{ustrate}) {
294       # only format to decimal point when not zero (analog to previous behavior in ca.pl)
295       $ustrate = $tr->{ustrate} != 0 ? $::form->format_amount(\%::myconfig, $tr->{ustrate} * 100, 2) : '0';
296     }
297
298     my $gegenkonto_string = "";
299     foreach my $gegenkonto (@{ $tr->{GEGENKONTO} }) {
300       if ($gegenkonto_string eq "") {
301         $gegenkonto_string = $gegenkonto->{accno};
302       } else {
303         $gegenkonto_string .= ", " . $gegenkonto->{accno};
304       }
305     }
306
307     my $reference_link = "$tr->{module}.pl?action=edit&id=$tr->{id}";
308
309     my %data = (
310       transdate   => { data => $tr->{transdate}, },
311       reference   => { data => $tr->{reference}, link => $reference_link },
312       description => { data => $tr->{description}, },
313       gegenkonto  => { data => $gegenkonto_string, },
314       debit       => { data => $debit },
315       credit      => { data => $credit },
316       ustkonto    => { data => $tr->{ustkonto}, },
317       ustrate     => { data => $ustrate },
318       balance     => { data => $::form->format_amount(\%::myconfig, $self->{balance}, 2, 'DRCR') },
319     );
320     $data{$_}->{align} = 'right' for qw(debit credit ustkonto ustrate balance);
321     # use a row set here in order to keep the table coloring intact
322     my @row_set;
323     push @row_set, \%data;
324
325     # show subtotals if setting enabled and ( last element reached or
326     # next element has a different value in the field selected by sort key )
327     if ( ($::form->{show_subtotals}) &&
328       ( ($idx == scalar @{ $::form->{CA} } - 1) ||
329         ($tr->{$sort_key} ne $::form->{CA}->[$idx + 1]->{$sort_key}) ) ) {
330
331       my %data = map { $_ => { class => 'listtotal' } } keys %{ $self->report->{columns} };
332       $data{credit}->{data} = $::form->format_amount(\%::myconfig, $subtotal_credit, 2);
333       $data{debit}->{data} = $::form->format_amount(\%::myconfig, $subtotal_debit, 2);
334       $data{$_}->{align} = 'right' for qw(debit credit);
335       push @row_set, \%data;
336
337       $subtotal_credit = 0.;
338       $subtotal_debit = 0.;
339     }
340     $self->report->add_data(\@row_set);
341     $idx++;
342   }
343
344   # debit credit and balance totals line
345   my %data = map { $_ => { class => 'listtotal' } } keys %{ $self->report->{columns} };
346   $data{credit}->{data} = $::form->format_amount(\%::myconfig, $self->{total_credit}, 2);
347   $data{debit}->{data} = $::form->format_amount(\%::myconfig, $self->{total_debit}, 2);
348   $data{balance}->{data} = $::form->format_amount(\%::myconfig, $self->{balance}, 2, 'DRCR');
349   $data{$_}->{align} = 'right' for qw(debit credit balance);
350   $self->report->add_data(\%data);
351
352   # get data for the footer line from the CA->all_transactions request
353   $self->{saldo_new} = $::form->{saldo_new} + $::form->{beginning_balance};
354   # "Jahresverkehrszahlen neu"
355   $self->{debit_new} = $::form->{current_balance_debit};
356   $self->{credit_new} = $::form->{current_balance_credit};
357
358   $self->set_report_footer_lines();
359 }
360
361 sub set_report_footer_lines {
362   my ($self) = @_;
363   # line 1
364   my %data = map { $_ => { class => 'listtotal' } } keys %{ $self->report->{columns} };
365   $data{reference}->{data} = t8('EB-Wert');
366   $data{description}  = { data => t8('Saldo neu'), class => 'listtotal', colspan => 2 };
367   $data{debit}        = { data => t8('Jahresverkehrszahlen neu'), class => 'listtotal', colspan => 2 };
368   $self->report->add_data(\%data);
369
370   # line 2
371   my %data2 = map { $_ => { class => 'listtotal' } } keys %{ $self->report->{columns} };
372   $data2{reference}->{data} = format_debit_credit($self->{eb_value});
373   $data2{description} = { data => format_debit_credit($self->{saldo_new}), class => 'listtotal', colspan => 2 };
374   $data2{debit}->{data} = $::form->format_amount(\%::myconfig, abs($self->{debit_new}) , 2) . " S";
375   $data2{credit}->{data} = $::form->format_amount(\%::myconfig, $self->{credit_new}, 2) . " H";
376   $self->report->add_data(\%data2);
377 }
378
379 sub set_report_custom_headers {
380   my ($self) = @_;
381
382   my @custom_headers = ();
383   # line 1
384   push @custom_headers, [
385     { text => t8('Letzte Buchung'), },
386     { text => t8('EB-Wert'), },
387     { text => t8('Saldo alt'), 'colspan' => 2, },
388     { text => t8('Jahresverkehrszahlen alt'), 'colspan' => 2, },
389     { text => '', 'colspan' => 2, },
390   ];
391   push @custom_headers, [
392     { text => $::form->{last_transaction}, },
393     { text => format_debit_credit($self->{eb_value}), },
394     { text => format_debit_credit($self->{saldo_old}), 'colspan' => 2, },
395     { text => $::form->format_amount(\%::myconfig, abs($self->{debit_old}), 2) . " S", },
396     { text => $::form->format_amount(\%::myconfig, $self->{credit_old}, 2) . " H", },
397     { text => '', 'colspan' => 2, },
398   ];
399   # line 2
400   # sorting is selected with radio button
401   #my $link = "controller.pl?action=ListTransactions%2freport_settings&accno=$::form->{accno}&fromdate=$::form->{fromdate}&todate=$::form->{todate}&show_subtotals=$::form->{show_subtotals}";
402   push @custom_headers, [
403     { text => t8('Date'), }, # link => $link . "&sort=transdate", },
404     { text => t8('Reference'), }, #'link' => $link . "&sort=reference",  },
405     { text => t8('Description'), }, #'link' => $link . "&sort=description",  },
406     { text => t8('Gegenkonto'), },
407     { text => t8('Debit'), },
408     { text => t8('Credit'), },
409     { text => t8('USt-Konto'), },
410     { text => t8('Satz %'), },
411     { text => t8('Balance'), },
412   ];
413
414   $self->report->set_custom_headers(@custom_headers);
415 }
416
417 # action bar
418
419 sub setup_report_settings_action_bar {
420   my ($self, %params) = @_;
421
422   for my $bar ($::request->layout->get('actionbar')) {
423     $bar->add(
424       action => [
425         t8('Show'),
426         submit    => [ '#report_settings', { action => 'ListTransactions/list' } ],
427         accesskey => 'enter',
428       ],
429       combobox => [
430         action => [
431           t8('Export'),
432         ],
433         action => [
434           t8('Export all accounts to CSV (ZIP file)'),
435           submit => [ '#report_settings', {
436             action => 'ListTransactions/export_options_all_charts',
437             output_format => 'CSV',
438           } ],
439         ],
440         action => [
441           t8('Export all accounts to PDF (ZIP file)'),
442           submit => [ '#report_settings', {
443             action => 'ListTransactions/export_options_all_charts',
444             output_format => 'PDF',
445           } ],
446         ],
447       ], # end of combobox "Export"
448     );
449   }
450 }
451
452 sub setup_export_options_action_bar {
453   my ($self, %params) = @_;
454   for my $bar ($::request->layout->get('actionbar')) {
455     $bar->add(
456       action => [
457         t8('Export'),
458         submit    => [ '#report_generator_form', {
459           action => 'ListTransactions/export_all_charts',
460           output_format => $params{output_format},
461         } ],
462         accesskey => 'enter',
463       ],
464       action => [
465         t8('Back'),
466         submit => [ '#report_generator_form', { action => 'ListTransactions/report_settings' } ],
467       ],
468     );
469   }
470 }
471
472 sub setup_list_action_bar {
473   my ($self, %params) = @_;
474   for my $bar ($::request->layout->get('actionbar')) {
475     $bar->add(
476       action => [
477         t8('Back'),
478         submit => [ '#report_generator_form', { action => 'ListTransactions/report_settings' } ],
479       ],
480     );
481   }
482 }
483
484 # helper
485
486 sub get_top_info_text {
487   my ($self) = @_;
488   my @text;
489   if ($::form->{department}) {
490     my ($department) = split /--/, $::form->{department};
491     push @text, $::locale->text('Department') . " : $department";
492   }
493   if ($::form->{projectnumber}) {
494     push @text, $::locale->text('Project Number') . " : $::form->{projectnumber}<br>";
495   }
496   push @text, join " ", t8('Period:'), $::form->{fromdate}, t8('to'), $::form->{todate};
497   push @text, join " ", t8('Report date:'), $::locale->format_date_object(DateTime->now_local);
498   push @text, join " ", t8('Company:'), $::instance_conf->get_company;
499   join "\n", @text;
500 }
501
502 sub format_debit_credit {
503   my $dc = shift;
504   my $formatted_dc  = $::form->format_amount(\%::myconfig, abs($dc), 2) . ' ';
505   $formatted_dc    .= ($dc > 0) ? t8('Credit (one letter abbreviation)') : t8('Debit (one letter abbreviation)');
506   $formatted_dc;
507 }
508
509 sub init_accounts_list {
510   CA->all_accounts(\%::myconfig, \%$::form);
511   my @accounts_list = map { {
512     text => "$_->{accno} - $_->{description}",
513     accno => $_->{accno},
514     chart_id => $_->{id},
515     balance => $_->{amount},
516     charttype => $_->{charttype},
517   } } @{ $::form->{CA} };
518   \@accounts_list;
519 }
520
521 1;
522
523 __END__
524
525 =encoding utf-8
526
527 =head1 NAME
528
529 SL::Controller::ListTransactions - Controller for the ListTransactions report
530
531 =head1 SYNOPSIS
532
533 New controller for Reports -> ListTransactions.
534
535 This replaces the functions from bin/mozilla/ca.pl.
536
537 The chart_of_accounts functionality is implemented separately in
538 SL::Controller::ChartOfAccounts.
539
540 =head1 DESCRIPTION / Key Features
541
542 A form is shown to select the accounts and the date period, as well as
543 options and the sorting of the report.
544
545 At this point, exporting all accounts is possible via Export -> Export all
546 accounts to CSV / PDF (ZIP file).
547
548 This will export all accounts for the selected time period and options,
549 and offer the resulting file for download.
550
551 The date period selection makes use of a new presenter SL::Presenter::DatePeriod.
552
553 If no date is selected all transactions are shown.
554
555 The resulting report should be equivalent to the old behavior, except
556 for the sorting, that has to be selected in advance now.
557
558 =head1 CAVEATS / TODO
559
560 Database queries are still from SL::CA.
561
562 The database queries in SL::CA are quite sophisticated, therefore i'm still using
563 these for now.
564
565 =head1 BUGS
566
567 None yet.
568
569 =head1 AUTHOR
570
571 Cem Aydin E<lt>cem.aydin@revamp-it.chE<gt>
572
573 =cut