1 package SL::Controller::ListTransactions;
4 use parent qw(SL::Controller::Base);
6 use POSIX qw(strftime);
7 use List::Util qw(first);
11 use SL::DB::AccTransaction;
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);
21 use SL::Presenter::DatePeriod qw(get_dialog_defaults_from_report_generator
22 populate_hidden_variables);
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) ],
29 __PACKAGE__->run_before(sub { $::auth->assert('report'); });
33 sub action_report_settings {
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};
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,
57 $self->report_type('HTML');
59 # set account number from chart picker chart_id
60 my $account = first { $_->{chart_id} eq $::form->{chart_id} } @{ $self->accounts_list };
62 flash('error', t8('No account selected. Please select an account.'));
63 return $self->action_report_settings;
65 $::form->{accno} = $account->{accno};
69 $self->setup_list_action_bar;
70 $self->prepare_report;
71 $self->set_report_data;
72 $self->report->generate_with_headers;
75 sub action_export_options_all_charts {
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
86 $self->defaults->{fromdate} = $self->from_date;
87 $self->defaults->{todate} = $self->to_date;
89 # dialog state is returned in a nested hash, this has to be flattened here
92 { key => 'dateperiod_selected_preset_' . $_, value => $self->defaults->{dialog}->{$_} }
93 } keys %{ $self->defaults->{dialog} };
94 delete($self->defaults->{dialog});
96 # handle the rest of the state
98 { key => $_, value => $self->defaults->{$_} }
99 } keys %{ $self->defaults };
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'),
108 $self->setup_export_options_action_bar(output_format => 'CSV');
109 $self->render('report_generator/csv_export_options',
110 title => t8('CSV export -- options'),
116 sub action_export_all_charts {
119 my $output_format = $::form->{output_format} // 'CSV';
121 my $zip = Archive::Zip->new();
123 for my $account (@{ $self->accounts_list }) {
124 next if $account->{charttype} eq "H" || !defined($account->{balance});
126 $::form->{accno} = $account->{accno};
128 my $sfile = SL::SessionFile::Random->new(mode => "w");
131 $self->report_type($output_format);
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);
139 $self->report->_generate_csv_content($sfile->fh);
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;
149 t8('list_of_transactions') . "_" . t8('account') . "_" . $sanitized_accno . ($output_format eq 'PDF' ? '.pdf' : '.csv')
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';
161 type => 'application/zip',
162 name => t8('list_of_transactions') . strftime('_%Y%m%d', localtime time) . '.zip',
171 # use values from form, then report generator form, then fallback
173 #accno => $self->accounts_list->[0]->{accno},
175 reporttype => 'custom',
176 year => DateTime->today->year,
178 dateperiod_from_date => '',
179 dateperiod_to_date => '',
184 for (keys %fallback) {
185 $defaults{$_} = $::form->{$_} // $::form->{'report_generator_hidden_' . $_} // $fallback{$_};
188 $defaults{dialog} = get_dialog_defaults_from_report_generator('dateperiod');
190 $self->defaults(\%defaults);
195 my $account = first { $_->{accno} eq $::form->{accno} } @{ $self->accounts_list };
196 $self->title(escape(join(" ", t8('List Transactions'), t8('Account'), $account->{text})));
202 # set dates according to selection
203 $self->from_date($::form->{dateperiod_from_date});
204 $self->to_date($::form->{dateperiod_to_date});
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)
215 $self->report(SL::ReportGenerator->new(\%::myconfig, $::form));
217 my @columns = qw(transdate reference description gegenkonto debit credit ustkonto ustrate balance);
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 %'), },
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,
241 $self->report->set_columns(%column_defs);
242 $self->report->set_column_order(@columns);
244 my @hidden_variables = qw(accno chart_id show_subtotals sort);
245 populate_hidden_variables('dateperiod', \@hidden_variables);
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';
254 sub set_report_data {
257 CA->all_transactions(\%::myconfig, \%$::form);
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};
266 $self->set_report_custom_headers();
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};
275 # used for subtotals below
276 my $sort_key = $::form->{sort};
279 for my $tr (@{ $::form->{CA} }) {
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};
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';
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';
298 my $gegenkonto_string = "";
299 foreach my $gegenkonto (@{ $tr->{GEGENKONTO} }) {
300 if ($gegenkonto_string eq "") {
301 $gegenkonto_string = $gegenkonto->{accno};
303 $gegenkonto_string .= ", " . $gegenkonto->{accno};
307 my $reference_link = "$tr->{module}.pl?action=edit&id=$tr->{id}";
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') },
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
323 push @row_set, \%data;
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}) ) ) {
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;
337 $subtotal_credit = 0.;
338 $subtotal_debit = 0.;
340 $self->report->add_data(\@row_set);
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);
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};
358 $self->set_report_footer_lines();
361 sub set_report_footer_lines {
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);
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);
379 sub set_report_custom_headers {
382 my @custom_headers = ();
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, },
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, },
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'), },
414 $self->report->set_custom_headers(@custom_headers);
419 sub setup_report_settings_action_bar {
420 my ($self, %params) = @_;
422 for my $bar ($::request->layout->get('actionbar')) {
426 submit => [ '#report_settings', { action => 'ListTransactions/list' } ],
427 accesskey => 'enter',
434 t8('Export all accounts to CSV (ZIP file)'),
435 submit => [ '#report_settings', {
436 action => 'ListTransactions/export_options_all_charts',
437 output_format => 'CSV',
441 t8('Export all accounts to PDF (ZIP file)'),
442 submit => [ '#report_settings', {
443 action => 'ListTransactions/export_options_all_charts',
444 output_format => 'PDF',
447 ], # end of combobox "Export"
452 sub setup_export_options_action_bar {
453 my ($self, %params) = @_;
454 for my $bar ($::request->layout->get('actionbar')) {
458 submit => [ '#report_generator_form', {
459 action => 'ListTransactions/export_all_charts',
460 output_format => $params{output_format},
462 accesskey => 'enter',
466 submit => [ '#report_generator_form', { action => 'ListTransactions/report_settings' } ],
472 sub setup_list_action_bar {
473 my ($self, %params) = @_;
474 for my $bar ($::request->layout->get('actionbar')) {
478 submit => [ '#report_generator_form', { action => 'ListTransactions/report_settings' } ],
486 sub get_top_info_text {
489 if ($::form->{department}) {
490 my ($department) = split /--/, $::form->{department};
491 push @text, $::locale->text('Department') . " : $department";
493 if ($::form->{projectnumber}) {
494 push @text, $::locale->text('Project Number') . " : $::form->{projectnumber}<br>";
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;
502 sub format_debit_credit {
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)');
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} };
529 SL::Controller::ListTransactions - Controller for the ListTransactions report
533 New controller for Reports -> ListTransactions.
535 This replaces the functions from bin/mozilla/ca.pl.
537 The chart_of_accounts functionality is implemented separately in
538 SL::Controller::ChartOfAccounts.
540 =head1 DESCRIPTION / Key Features
542 A form is shown to select the accounts and the date period, as well as
543 options and the sorting of the report.
545 At this point, exporting all accounts is possible via Export -> Export all
546 accounts to CSV / PDF (ZIP file).
548 This will export all accounts for the selected time period and options,
549 and offer the resulting file for download.
551 The date period selection makes use of a new presenter SL::Presenter::DatePeriod.
553 If no date is selected all transactions are shown.
555 The resulting report should be equivalent to the old behavior, except
556 for the sorting, that has to be selected in advance now.
558 =head1 CAVEATS / TODO
560 Database queries are still from SL::CA.
562 The database queries in SL::CA are quite sophisticated, therefore i'm still using
571 Cem Aydin E<lt>cem.aydin@revamp-it.chE<gt>