Bug 15366 - Fix breadcrumbs and html page title in guided reports
[koha.git] / reports / guided_reports.pl
1 #!/usr/bin/perl
2
3 # Copyright 2007 Liblime ltd
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21 use CGI qw/-utf8/;
22 use Text::CSV::Encoded;
23 use Encode qw( decode );
24 use URI::Escape;
25 use File::Temp;
26 use File::Basename qw( dirname );
27 use C4::Reports::Guided;
28 use C4::Auth qw/:DEFAULT get_session/;
29 use C4::Output;
30 use C4::Debug;
31 use C4::Branch; # XXX subfield_is_koha_internal_p
32 use C4::Koha qw/IsAuthorisedValueCategory GetFrameworksLoop/;
33 use C4::Context;
34 use C4::Log;
35 use Koha::DateUtils qw/dt_from_string output_pref/;
36
37 =head1 NAME
38
39 guided_reports.pl
40
41 =head1 DESCRIPTION
42
43 Script to control the guided report creation
44
45 =cut
46
47 my $input = new CGI;
48 my $usecache = C4::Context->ismemcached;
49
50 my $phase = $input->param('phase');
51 my $flagsrequired;
52 if ( $phase eq 'Build new' or $phase eq 'Delete Saved' ) {
53     $flagsrequired = 'create_reports';
54 }
55 elsif ( $phase eq 'Use saved' ) {
56     $flagsrequired = 'execute_reports';
57 } else {
58     $flagsrequired = '*';
59 }
60
61 my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
62     {
63         template_name   => "reports/guided_reports_start.tt",
64         query           => $input,
65         type            => "intranet",
66         authnotrequired => 0,
67         flagsrequired   => { reports => $flagsrequired },
68         debug           => 1,
69     }
70 );
71 my $session = $cookie ? get_session($cookie->value) : undef;
72
73 my $filter;
74 if ( $input->param("filter_set") ) {
75     $filter = {};
76     $filter->{$_} = $input->param("filter_$_") foreach qw/date author keyword group subgroup/;
77     $session->param('report_filter', $filter) if $session;
78     $template->param( 'filter_set' => 1 );
79 }
80 elsif ($session and not $input->param('clear_filters')) {
81     $filter = $session->param('report_filter');
82 }
83
84
85 my @errors = ();
86 if ( !$phase ) {
87     $template->param( 'start' => 1 );
88     # show welcome page
89 }
90 elsif ( $phase eq 'Build new' ) {
91     # build a new report
92     $template->param( 'build1' => 1 );
93     $template->param(
94         'areas'        => get_report_areas(),
95         'usecache'     => $usecache,
96         'cache_expiry' => 300,
97         'public'       => '0',
98     );
99 } elsif ( $phase eq 'Use saved' ) {
100
101     # use a saved report
102     # get list of reports and display them
103     my $group = $input->param('group');
104     my $subgroup = $input->param('subgroup');
105     $filter->{group} = $group;
106     $filter->{subgroup} = $subgroup;
107     $template->param(
108         'saved1' => 1,
109         'savedreports' => get_saved_reports($filter),
110         'usecache' => $usecache,
111         'groups_with_subgroups'=> groups_with_subgroups($group, $subgroup),
112         filters => $filter,
113     );
114 }
115
116 elsif ( $phase eq 'Delete Multiple') {
117     my @ids = $input->param('ids');
118     delete_report( @ids );
119     print $input->redirect("/cgi-bin/koha/reports/guided_reports.pl?phase=Use%20saved");
120     exit;
121 }
122
123 elsif ( $phase eq 'Delete Saved') {
124         
125         # delete a report from the saved reports list
126     my $ids = $input->param('reports');
127     delete_report($ids);
128     print $input->redirect("/cgi-bin/koha/reports/guided_reports.pl?phase=Use%20saved");
129         exit;
130 }               
131
132 elsif ( $phase eq 'Show SQL'){
133         
134     my $id = $input->param('reports');
135     my $report = get_saved_report($id);
136     $template->param(
137         'id'      => $id,
138         'reportname' => $report->{report_name},
139         'notes'      => $report->{notes},
140         'sql'     => $report->{savedsql},
141         'showsql' => 1,
142     );
143 }
144
145 elsif ( $phase eq 'Edit SQL'){
146     my $id = $input->param('reports');
147     my $report = get_saved_report($id);
148     my $group = $report->{report_group};
149     my $subgroup  = $report->{report_subgroup};
150     $template->param(
151         'sql'        => $report->{savedsql},
152         'reportname' => $report->{report_name},
153         'groups_with_subgroups' => groups_with_subgroups($group, $subgroup),
154         'notes'      => $report->{notes},
155         'id'         => $id,
156         'cache_expiry' => $report->{cache_expiry},
157         'public' => $report->{public},
158         'usecache' => $usecache,
159         'editsql'    => 1,
160     );
161 }
162
163 elsif ( $phase eq 'Update SQL'){
164     my $id         = $input->param('id');
165     my $sql        = $input->param('sql');
166     my $reportname = $input->param('reportname');
167     my $group      = $input->param('group');
168     my $subgroup   = $input->param('subgroup');
169     my $notes      = $input->param('notes');
170     my $cache_expiry = $input->param('cache_expiry');
171     my $cache_expiry_units = $input->param('cache_expiry_units');
172     my $public = $input->param('public');
173     my $save_anyway = $input->param('save_anyway');
174
175     my @errors;
176
177     # if we have the units, then we came from creating a report from SQL and thus need to handle converting units
178     if( $cache_expiry_units ){
179       if( $cache_expiry_units eq "minutes" ){
180         $cache_expiry *= 60;
181       } elsif( $cache_expiry_units eq "hours" ){
182         $cache_expiry *= 3600; # 60 * 60
183       } elsif( $cache_expiry_units eq "days" ){
184         $cache_expiry *= 86400; # 60 * 60 * 24
185       }
186     }
187     # check $cache_expiry isnt too large, Memcached::set requires it to be less than 30 days or it will be treated as if it were an absolute time stamp
188     if( $cache_expiry >= 2592000 ){
189       push @errors, {cache_expiry => $cache_expiry};
190     }
191
192     create_non_existing_group_and_subgroup($input, $group, $subgroup);
193
194     if ($sql =~ /;?\W?(UPDATE|DELETE|DROP|INSERT|SHOW|CREATE)\W/i) {
195         push @errors, {sqlerr => $1};
196     }
197     elsif ($sql !~ /^(SELECT)/i) {
198         push @errors, {queryerr => "No SELECT"};
199     }
200
201     if (@errors) {
202         $template->param(
203             'errors'    => \@errors,
204             'sql'       => $sql,
205         );
206     } else {
207
208         # Check defined SQL parameters for authorised value validity
209         my $problematic_authvals = ValidateSQLParameters($sql);
210
211         if ( scalar @$problematic_authvals > 0 && not $save_anyway ) {
212             # There's at least one problematic parameter, report to the
213             # GUI and provide all user input for further actions
214             $template->param(
215                 'id' => $id,
216                 'sql' => $sql,
217                 'reportname' => $reportname,
218                 'group' => $group,
219                 'subgroup' => $subgroup,
220                 'notes' => $notes,
221                 'public' => $public,
222                 'problematic_authvals' => $problematic_authvals,
223                 'warn_authval_problem' => 1,
224                 'phase_update' => 1
225             );
226
227         } else {
228             # No params problem found or asked to save anyway
229             update_sql( $id, {
230                     sql => $sql,
231                     name => $reportname,
232                     group => $group,
233                     subgroup => $subgroup,
234                     notes => $notes,
235                     public => $public,
236                     cache_expiry => $cache_expiry,
237                 } );
238             $template->param(
239                 'save_successful'       => 1,
240                 'reportname'            => $reportname,
241                 'id'                    => $id,
242             );
243             logaction( "REPORTS", "MODIFY", $id, "$reportname | $sql" ) if C4::Context->preference("ReportsLog");
244         }
245         if ( $usecache ) {
246             $template->param(
247                 cache_expiry => $cache_expiry,
248                 cache_expiry_units => $cache_expiry_units,
249             );
250         }
251     }
252 }
253
254 elsif ($phase eq 'retrieve results') {
255         my $id = $input->param('id');
256         my ($results,$name,$notes) = format_results($id);
257         # do something
258         $template->param(
259                 'retresults' => 1,
260                 'results' => $results,
261                 'name' => $name,
262                 'notes' => $notes,
263     );
264 }
265
266 elsif ( $phase eq 'Report on this Area' ) {
267     my $cache_expiry_units = $input->param('cache_expiry_units'),
268     my $cache_expiry = $input->param('cache_expiry');
269
270     # we need to handle converting units
271     if( $cache_expiry_units eq "minutes" ){
272       $cache_expiry *= 60;
273     } elsif( $cache_expiry_units eq "hours" ){
274       $cache_expiry *= 3600; # 60 * 60
275     } elsif( $cache_expiry_units eq "days" ){
276       $cache_expiry *= 86400; # 60 * 60 * 24
277     }
278     # check $cache_expiry isnt too large, Memcached::set requires it to be less than 30 days or it will be treated as if it were an absolute time stamp
279     if( $cache_expiry >= 2592000 ){ # oops, over the limit of 30 days
280       # report error to user
281       $template->param(
282         'cache_error' => 1,
283         'build1' => 1,
284         'areas'   => get_report_areas(),
285         'cache_expiry' => $cache_expiry,
286         'usecache' => $usecache,
287         'public' => $input->param('public'),
288       );
289     } else {
290       # they have choosen a new report and the area to report on
291       $template->param(
292           'build2' => 1,
293           'area'   => $input->param('area'),
294           'types'  => get_report_types(),
295           'cache_expiry' => $cache_expiry,
296           'public' => $input->param('public'),
297       );
298     }
299 }
300
301 elsif ( $phase eq 'Choose this type' ) {
302     # they have chosen type and area
303     # get area and type and pass them to the template
304     my $area = $input->param('area');
305     my $type = $input->param('types');
306     $template->param(
307         'build3' => 1,
308         'area'   => $area,
309         'type'   => $type,
310         columns  => get_columns($area,$input),
311         'cache_expiry' => $input->param('cache_expiry'),
312         'public' => $input->param('public'),
313     );
314 }
315
316 elsif ( $phase eq 'Choose these columns' ) {
317     # we now know type, area, and columns
318     # next step is the constraints
319     my $area    = $input->param('area');
320     my $type    = $input->param('type');
321     my @columns = $input->param('columns');
322     my $column  = join( ',', @columns );
323
324     $template->param(
325         'build4' => 1,
326         'area'   => $area,
327         'type'   => $type,
328         'column' => $column,
329         definitions => get_from_dictionary($area),
330         criteria    => get_criteria($area,$input),
331         'public' => $input->param('public'),
332     );
333     if ( $usecache ) {
334         $template->param(
335             cache_expiry => $input->param('cache_expiry'),
336             cache_expiry_units => $input->param('cache_expiry_units'),
337         );
338     }
339
340 }
341
342 elsif ( $phase eq 'Choose these criteria' ) {
343     my $area     = $input->param('area');
344     my $type     = $input->param('type');
345     my $column   = $input->param('column');
346     my @definitions = $input->param('definition');
347     my $definition = join (',',@definitions);
348     my @criteria = $input->param('criteria_column');
349     my $query_criteria;
350     foreach my $crit (@criteria) {
351         my $value = $input->param( $crit . "_value" );
352
353         # If value is not defined, then it may be range values
354         if (!defined $value) {
355
356             my $fromvalue = $input->param( "from_" . $crit . "_value" );
357             my $tovalue   = $input->param( "to_"   . $crit . "_value" );
358
359             # If the range values are dates
360             my $fromvalue_dt;
361             $fromvalue_dt = eval { dt_from_string( $fromvalue ); } if ( $fromvalue );
362             my $tovalue_dt;
363             $tovalue_dt = eval { dt_from_string( $tovalue ); } if ($tovalue);
364             if ( $fromvalue_dt && $tovalue_dt ) {
365                 $fromvalue = output_pref( { dt => dt_from_string( $fromvalue_dt ), dateonly => 1, dateformat => 'iso' } );
366                 $tovalue   = output_pref( { dt => dt_from_string( $tovalue_dt ), dateonly => 1, dateformat => 'iso' } );
367             }
368
369             if ($fromvalue && $tovalue) {
370                 $query_criteria .= " AND $crit >= '$fromvalue' AND $crit <= '$tovalue'";
371             }
372
373         } else {
374
375             # If value is a date
376             my $value_dt;
377             $value_dt  =  eval { dt_from_string( $value ); } if ( $value );
378             if ( $value_dt ) {
379                 $value = output_pref( { dt => dt_from_string( $value_dt ), dateonly => 1, dateformat => 'iso' } );
380             }
381             # don't escape runtime parameters, they'll be at runtime
382             if ($value =~ /<<.*>>/) {
383                 $query_criteria .= " AND $crit=$value";
384             } else {
385                 $query_criteria .= " AND $crit='$value'";
386             }
387         }
388     }
389     $template->param(
390         'build5'         => 1,
391         'area'           => $area,
392         'type'           => $type,
393         'column'         => $column,
394         'definition'     => $definition,
395         'criteriastring' => $query_criteria,
396         'public' => $input->param('public'),
397     );
398     if ( $usecache ) {
399         $template->param(
400             cache_expiry => $input->param('cache_expiry'),
401             cache_expiry_units => $input->param('cache_expiry_units'),
402         );
403     }
404
405     # get columns
406     my @columns = split( ',', $column );
407     my @total_by;
408
409     # build structue for use by tmpl_loop to choose columns to order by
410     # need to do something about the order of the order :)
411         # we also want to use the %columns hash to get the plain english names
412     foreach my $col (@columns) {
413         my %total = (name => $col);
414         my @selects = map {+{ value => $_ }} (qw(sum min max avg count));
415         $total{'select'} = \@selects;
416         push @total_by, \%total;
417     }
418
419     $template->param( 'total_by' => \@total_by );
420 }
421
422 elsif ( $phase eq 'Choose these operations' ) {
423     my $area     = $input->param('area');
424     my $type     = $input->param('type');
425     my $column   = $input->param('column');
426     my $criteria = $input->param('criteria');
427         my $definition = $input->param('definition');
428     my @total_by = $input->param('total_by');
429     my $totals;
430     foreach my $total (@total_by) {
431         my $value = $input->param( $total . "_tvalue" );
432         $totals .= "$value($total),";
433     }
434
435     $template->param(
436         'build6'         => 1,
437         'area'           => $area,
438         'type'           => $type,
439         'column'         => $column,
440         'criteriastring' => $criteria,
441         'totals'         => $totals,
442         'definition'     => $definition,
443         'cache_expiry' => $input->param('cache_expiry'),
444         'public' => $input->param('public'),
445     );
446
447     # get columns
448     my @columns = split( ',', $column );
449     my @order_by;
450
451     # build structue for use by tmpl_loop to choose columns to order by
452     # need to do something about the order of the order :)
453     foreach my $col (@columns) {
454         my %order = (name => $col);
455         my @selects = map {+{ value => $_ }} (qw(asc desc));
456         $order{'select'} = \@selects;
457         push @order_by, \%order;
458     }
459
460     $template->param( 'order_by' => \@order_by );
461 }
462
463 elsif ( $phase eq 'Build report' ) {
464
465     # now we have all the info we need and can build the sql
466     my $area     = $input->param('area');
467     my $type     = $input->param('type');
468     my $column   = $input->param('column');
469     my $crit     = $input->param('criteria');
470     my $totals   = $input->param('totals');
471     my $definition = $input->param('definition');
472     my $query_criteria=$crit;
473     # split the columns up by ,
474     my @columns = split( ',', $column );
475     my @order_by = $input->param('order_by');
476
477     my $query_orderby;
478     foreach my $order (@order_by) {
479         my $value = $input->param( $order . "_ovalue" );
480         if ($query_orderby) {
481             $query_orderby .= ",$order $value";
482         }
483         else {
484             $query_orderby = " ORDER BY $order $value";
485         }
486     }
487
488     # get the sql
489     my $sql =
490       build_query( \@columns, $query_criteria, $query_orderby, $area, $totals, $definition );
491     $template->param(
492         'showreport' => 1,
493         'area'       => $area,
494         'sql'        => $sql,
495         'type'       => $type,
496         'cache_expiry' => $input->param('cache_expiry'),
497         'public' => $input->param('public'),
498     );
499 }
500
501 elsif ( $phase eq 'Save' ) {
502     # Save the report that has just been built
503     my $area           = $input->param('area');
504     my $sql  = $input->param('sql');
505     my $type = $input->param('type');
506     $template->param(
507         'save' => 1,
508         'area'  => $area,
509         'sql'  => $sql,
510         'type' => $type,
511         'cache_expiry' => $input->param('cache_expiry'),
512         'public' => $input->param('public'),
513         'groups_with_subgroups' => groups_with_subgroups($area), # in case we have a report group that matches area
514     );
515 }
516
517 elsif ( $phase eq 'Save Report' ) {
518     # save the sql pasted in by a user
519     my $area  = $input->param('area');
520     my $group = $input->param('group');
521     my $subgroup = $input->param('subgroup');
522     my $sql   = $input->param('sql');
523     my $name  = $input->param('reportname');
524     my $type  = $input->param('types');
525     my $notes = $input->param('notes');
526     my $cache_expiry = $input->param('cache_expiry');
527     my $cache_expiry_units = $input->param('cache_expiry_units');
528     my $public = $input->param('public');
529     my $save_anyway = $input->param('save_anyway');
530
531
532     # if we have the units, then we came from creating a report from SQL and thus need to handle converting units
533     if( $cache_expiry_units ){
534       if( $cache_expiry_units eq "minutes" ){
535         $cache_expiry *= 60;
536       } elsif( $cache_expiry_units eq "hours" ){
537         $cache_expiry *= 3600; # 60 * 60
538       } elsif( $cache_expiry_units eq "days" ){
539         $cache_expiry *= 86400; # 60 * 60 * 24
540       }
541     }
542     # check $cache_expiry isnt too large, Memcached::set requires it to be less than 30 days or it will be treated as if it were an absolute time stamp
543     if( $cache_expiry && $cache_expiry >= 2592000 ){
544       push @errors, {cache_expiry => $cache_expiry};
545     }
546
547     create_non_existing_group_and_subgroup($input, $group, $subgroup);
548
549     ## FIXME this is AFTER entering a name to save the report under
550     if ($sql =~ /;?\W?(UPDATE|DELETE|DROP|INSERT|SHOW|CREATE)\W/i) {
551         push @errors, {sqlerr => $1};
552     }
553     elsif ($sql !~ /^(SELECT)/i) {
554         push @errors, {queryerr => "No SELECT"};
555     }
556
557     if (@errors) {
558         $template->param(
559             'errors'    => \@errors,
560             'sql'       => $sql,
561             'reportname'=> $name,
562             'type'      => $type,
563             'notes'     => $notes,
564             'cache_expiry' => $cache_expiry,
565             'public'    => $public,
566         );
567     } else {
568         # Check defined SQL parameters for authorised value validity
569         my $problematic_authvals = ValidateSQLParameters($sql);
570
571         if ( scalar @$problematic_authvals > 0 && not $save_anyway ) {
572             # There's at least one problematic parameter, report to the
573             # GUI and provide all user input for further actions
574             $template->param(
575                 'area' => $area,
576                 'group' =>  $group,
577                 'subgroup' => $subgroup,
578                 'sql' => $sql,
579                 'reportname' => $name,
580                 'type' => $type,
581                 'notes' => $notes,
582                 'public' => $public,
583                 'problematic_authvals' => $problematic_authvals,
584                 'warn_authval_problem' => 1,
585                 'phase_save' => 1
586             );
587             if ( $usecache ) {
588                 $template->param(
589                     cache_expiry => $cache_expiry,
590                     cache_expiry_units => $cache_expiry_units,
591                 );
592             }
593         } else {
594             # No params problem found or asked to save anyway
595             my $id = save_report( {
596                     borrowernumber => $borrowernumber,
597                     sql            => $sql,
598                     name           => $name,
599                     area           => $area,
600                     group          => $group,
601                     subgroup       => $subgroup,
602                     type           => $type,
603                     notes          => $notes,
604                     cache_expiry   => $cache_expiry,
605                     public         => $public,
606                 } );
607                 logaction( "REPORTS", "ADD", $id, "$name | $sql" ) if C4::Context->preference("ReportsLog");
608             $template->param(
609                 'save_successful' => 1,
610                 'reportname'      => $name,
611                 'id'              => $id,
612             );
613         }
614     }
615 }
616
617 elsif ($phase eq 'Run this report'){
618     # execute a saved report
619     my $limit      = $input->param('limit') || 20;
620     my $offset     = 0;
621     my $report_id  = $input->param('reports');
622     my @sql_params = $input->param('sql_params');
623     # offset algorithm
624     if ($input->param('page')) {
625         $offset = ($input->param('page') - 1) * $limit;
626     }
627
628     $template->param(
629         'limit'   => $limit,
630         'report_id' => $report_id,
631     );
632
633     my ( $sql, $type, $name, $notes );
634     if (my $report = get_saved_report($report_id)) {
635         $sql   = $report->{savedsql};
636         $name  = $report->{report_name};
637         $notes = $report->{notes};
638
639         my @rows = ();
640         # if we have at least 1 parameter, and it's not filled, then don't execute but ask for parameters
641         if ($sql =~ /<</ && !@sql_params) {
642             # split on ??. Each odd (2,4,6,...) entry should be a parameter to fill
643             my @split = split /<<|>>/,$sql;
644             my @tmpl_parameters;
645             my @authval_errors;
646             for(my $i=0;$i<($#split/2);$i++) {
647                 my ($text,$authorised_value) = split /\|/,$split[$i*2+1];
648                 my $input;
649                 my $labelid;
650                 if ( not defined $authorised_value ) {
651                     # no authorised value input, provide a text box
652                     $input = "text";
653                 } elsif ( $authorised_value eq "date" ) {
654                     # require a date, provide a date picker
655                     $input = 'date';
656                 } else {
657                     # defined $authorised_value, and not 'date'
658                     my $dbh=C4::Context->dbh;
659                     my @authorised_values;
660                     my %authorised_lib;
661                     # builds list, depending on authorised value...
662                     if ( $authorised_value eq "branches" ) {
663                         my $branches = GetBranchesLoop();
664                         foreach my $thisbranch (@$branches) {
665                             push @authorised_values, $thisbranch->{value};
666                             $authorised_lib{$thisbranch->{value}} = $thisbranch->{branchname};
667                         }
668                     }
669                     elsif ( $authorised_value eq "itemtypes" ) {
670                         my $sth = $dbh->prepare("SELECT itemtype,description FROM itemtypes ORDER BY description");
671                         $sth->execute;
672                         while ( my ( $itemtype, $description ) = $sth->fetchrow_array ) {
673                             push @authorised_values, $itemtype;
674                             $authorised_lib{$itemtype} = $description;
675                         }
676                     }
677                     elsif ( $authorised_value eq "biblio_framework" ) {
678                         my $frameworks = GetFrameworksLoop();
679                         my $default_source = '';
680                         push @authorised_values,$default_source;
681                         $authorised_lib{$default_source} = 'Default';
682                         foreach my $framework (@$frameworks) {
683                             push @authorised_values, $framework->{value};
684                             $authorised_lib{$framework->{value}} = $framework->{description};
685                         }
686                     }
687                     elsif ( $authorised_value eq "cn_source" ) {
688                         my $class_sources = GetClassSources();
689                         my $default_source = C4::Context->preference("DefaultClassificationSource");
690                         foreach my $class_source (sort keys %$class_sources) {
691                             next unless $class_sources->{$class_source}->{'used'} or
692                                         ($class_source eq $default_source);
693                             push @authorised_values, $class_source;
694                             $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
695                         }
696                     }
697                     elsif ( $authorised_value eq "categorycode" ) {
698                         my $sth = $dbh->prepare("SELECT categorycode, description FROM categories ORDER BY description");
699                         $sth->execute;
700                         while ( my ( $categorycode, $description ) = $sth->fetchrow_array ) {
701                             push @authorised_values, $categorycode;
702                             $authorised_lib{$categorycode} = $description;
703                         }
704
705                         #---- "true" authorised value
706                     }
707                     else {
708                         if ( IsAuthorisedValueCategory($authorised_value) ) {
709                             my $query = '
710                             SELECT authorised_value,lib
711                             FROM authorised_values
712                             WHERE category=?
713                             ORDER BY lib
714                             ';
715                             my $authorised_values_sth = $dbh->prepare($query);
716                             $authorised_values_sth->execute( $authorised_value);
717
718                             while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
719                                 push @authorised_values, $value;
720                                 $authorised_lib{$value} = $lib;
721                                 # For item location, we show the code and the libelle
722                                 $authorised_lib{$value} = $lib;
723                             }
724                         } else {
725                             # not exists $authorised_value_categories{$authorised_value})
726                             push @authval_errors, {'entry' => $text,
727                                                    'auth_val' => $authorised_value };
728                             # tell the template there's an error
729                             $template->param( auth_val_error => 1 );
730                             # skip scrolling list creation and params push
731                             next;
732                         }
733                     }
734                     $labelid = $text;
735                     $labelid =~ s/\W//g;
736                     $input = {
737                         name    => "sql_params",
738                         id      => "sql_params_".$labelid,
739                         values  => \@authorised_values,
740                         labels  => \%authorised_lib,
741                     };
742                 }
743
744                 push @tmpl_parameters, {'entry' => $text, 'input' => $input, 'labelid' => $labelid };
745             }
746             $template->param('sql'         => $sql,
747                             'name'         => $name,
748                             'sql_params'   => \@tmpl_parameters,
749                             'auth_val_errors'  => \@authval_errors,
750                             'enter_params' => 1,
751                             'reports'      => $report_id,
752                             );
753         } else {
754             # OK, we have parameters, or there are none, we run the report
755             # if there were parameters, replace before running
756             # split on ??. Each odd (2,4,6,...) entry should be a parameter to fill
757             my @split = split /<<|>>/,$sql;
758             my @tmpl_parameters;
759             for(my $i=0;$i<$#split/2;$i++) {
760                 my $quoted = $sql_params[$i];
761                 # if there are special regexp chars, we must \ them
762                 $split[$i*2+1] =~ s/(\||\?|\.|\*|\(|\)|\%)/\\$1/g;
763                 if ($split[$i*2+1] =~ /\|\s*date\s*$/) {
764                     $quoted = output_pref({ dt => dt_from_string($quoted), dateformat => 'iso', dateonly => 1 }) if $quoted;
765                 }
766                 $quoted = C4::Context->dbh->quote($quoted);
767                 $sql =~ s/<<$split[$i*2+1]>>/$quoted/;
768             }
769             my ($sth, $errors) = execute_query($sql, $offset, $limit);
770             my $total = nb_rows($sql) || 0;
771             unless ($sth) {
772                 die "execute_query failed to return sth for report $report_id: $sql";
773             } else {
774                 my $headers = header_cell_loop($sth);
775                 $template->param(header_row => $headers);
776                 while (my $row = $sth->fetchrow_arrayref()) {
777                     my @cells = map { +{ cell => $_ } } @$row;
778                     push @rows, { cells => \@cells };
779                 }
780             }
781
782             my $totpages = int($total/$limit) + (($total % $limit) > 0 ? 1 : 0);
783             my $url = "/cgi-bin/koha/reports/guided_reports.pl?reports=$report_id&amp;phase=Run%20this%20report&amp;limit=$limit";
784             if (@sql_params) {
785                 $url = join('&amp;sql_params=', $url, map { URI::Escape::uri_escape_utf8($_) } @sql_params);
786             }
787             $template->param(
788                 'results' => \@rows,
789                 'sql'     => $sql,
790                 'id'      => $report_id,
791                 'execute' => 1,
792                 'name'    => $name,
793                 'notes'   => $notes,
794                 'errors'  => defined($errors) ? [ $errors ] : undef,
795                 'pagination_bar'  => pagination_bar($url, $totpages, $input->param('page')),
796                 'unlimited_total' => $total,
797                 'sql_params'      => \@sql_params,
798             );
799         }
800     }
801     else {
802         push @errors, { no_sql_for_id => $report_id };
803     }
804 }
805
806 elsif ($phase eq 'Export'){
807
808         # export results to tab separated text or CSV
809         my $sql    = $input->param('sql');  # FIXME: use sql from saved report ID#, not new user-supplied SQL!
810     my $format = $input->param('format');
811     my $reportname = $input->param('reportname');
812     my $reportfilename = $reportname ? "$reportname-reportresults.$format" : "reportresults.$format" ;
813         my ($sth, $q_errors) = execute_query($sql);
814     unless ($q_errors and @$q_errors) {
815         my ( $type, $content );
816         if ($format eq 'tab') {
817             $type = 'application/octet-stream';
818             $content .= join("\t", header_cell_values($sth)) . "\n";
819             $content = Encode::decode('UTF-8', $content);
820             while (my $row = $sth->fetchrow_arrayref()) {
821                 $content .= join("\t", @$row) . "\n";
822             }
823         } else {
824             my $delimiter = C4::Context->preference('delimiter') || ',';
825             if ( $format eq 'csv' ) {
826                 $type = 'application/csv';
827                 my $csv = Text::CSV::Encoded->new({ encoding_out => 'UTF-8', sep_char => $delimiter});
828                 $csv or die "Text::CSV::Encoded->new({binary => 1}) FAILED: " . Text::CSV::Encoded->error_diag();
829                 if ($csv->combine(header_cell_values($sth))) {
830                     $content .= Encode::decode('UTF-8', $csv->string()) . "\n";
831                 } else {
832                     push @$q_errors, { combine => 'HEADER ROW: ' . $csv->error_diag() } ;
833                 }
834                 while (my $row = $sth->fetchrow_arrayref()) {
835                     if ($csv->combine(@$row)) {
836                         $content .= $csv->string() . "\n";
837                     } else {
838                         push @$q_errors, { combine => $csv->error_diag() } ;
839                     }
840                 }
841             }
842             elsif ( $format eq 'ods' ) {
843                 $type = 'application/vnd.oasis.opendocument.spreadsheet';
844                 my $ods_fh = File::Temp->new( UNLINK => 0 );
845                 my $ods_filepath = $ods_fh->filename;
846
847                 use OpenOffice::OODoc;
848                 my $tmpdir = dirname $ods_filepath;
849                 odfWorkingDirectory( $tmpdir );
850                 my $container = odfContainer( $ods_filepath, create => 'spreadsheet' );
851                 my $doc = odfDocument (
852                     container => $container,
853                     part      => 'content'
854                 );
855                 my $table = $doc->getTable(0);
856                 my @headers = header_cell_values( $sth );
857                 my $rows = $sth->fetchall_arrayref();
858                 my ( $nb_rows, $nb_cols ) = ( 0, 0 );
859                 $nb_rows = @$rows;
860                 $nb_cols = @headers;
861                 $doc->expandTable( $table, $nb_rows + 1, $nb_cols );
862
863                 my $row = $doc->getRow( $table, 0 );
864                 my $j = 0;
865                 for my $header ( @headers ) {
866                     $doc->cellValue( $row, $j, $header );
867                     $j++;
868                 }
869                 my $i = 1;
870                 for ( @$rows ) {
871                     $row = $doc->getRow( $table, $i );
872                     for ( my $j = 0 ; $j < $nb_cols ; $j++ ) {
873                         my $value = Encode::encode( 'UTF8', $rows->[$i - 1][$j] );
874                         $doc->cellValue( $row, $j, $value );
875                     }
876                     $i++;
877                 }
878                 $doc->save();
879                 binmode(STDOUT);
880                 open $ods_fh, '<', $ods_filepath;
881                 $content .= $_ while <$ods_fh>;
882                 unlink $ods_filepath;
883             }
884         }
885         print $input->header(
886             -type => $type,
887             -attachment=> $reportfilename
888         );
889         print $content;
890
891         foreach my $err (@$q_errors, @errors) {
892             print "# ERROR: " . (map {$_ . ": " . $err->{$_}} keys %$err) . "\n";
893         }   # here we print all the non-fatal errors at the end.  Not super smooth, but better than nothing.
894         exit;
895     }
896     $template->param(
897         'sql'           => $sql,
898         'execute'       => 1,
899         'name'          => 'Error exporting report!',
900         'notes'         => '',
901         'errors'        => $q_errors,
902     );
903 }
904
905 elsif ( $phase eq 'Create report from SQL' ) {
906
907     my ($group, $subgroup);
908     # allow the user to paste in sql
909     if ( $input->param('sql') ) {
910         $group = $input->param('report_group');
911         $subgroup  = $input->param('report_subgroup');
912         $template->param(
913             'sql'           => $input->param('sql') // '',
914             'reportname'    => $input->param('reportname') // '',
915             'notes'         => $input->param('notes') // '',
916         );
917     }
918     $template->param(
919         'create' => 1,
920         'groups_with_subgroups' => groups_with_subgroups($group, $subgroup),
921         'public' => '0',
922         'cache_expiry' => 300,
923         'usecache' => $usecache,
924     );
925 }
926
927 elsif ($phase eq 'Create Compound Report'){
928         $template->param( 'savedreports' => get_saved_reports(),
929                 'compound' => 1,
930         );
931 }
932
933 elsif ($phase eq 'Save Compound'){
934     my $master    = $input->param('master');
935         my $subreport = $input->param('subreport');
936         my ($mastertables,$subtables) = create_compound($master,$subreport);
937         $template->param( 'save_compound' => 1,
938                 master=>$mastertables,
939                 subsql=>$subtables
940         );
941 }
942
943 # pass $sth, get back an array of names for the column headers
944 sub header_cell_values {
945     my $sth = shift or return ();
946     return '' unless ($sth->{NAME});
947     return @{$sth->{NAME}};
948 }
949
950 # pass $sth, get back a TMPL_LOOP-able set of names for the column headers
951 sub header_cell_loop {
952     my @headers = map { +{ cell => decode('UTF-8',$_) } } header_cell_values (shift);
953     return \@headers;
954 }
955
956 foreach (1..6) {
957      $template->{VARS}->{'build' . $_} and last;
958 }
959 $template->param(   'referer' => $input->referer(),
960                 );
961
962 output_html_with_http_headers $input, $cookie, $template->output;
963
964 sub groups_with_subgroups {
965     my ($group, $subgroup) = @_;
966
967     my $groups_with_subgroups = get_report_groups();
968     my @g_sg;
969     my @sorted_keys = sort {
970         $groups_with_subgroups->{$a}->{name} cmp $groups_with_subgroups->{$b}->{name}
971     } keys %$groups_with_subgroups;
972     foreach my $g_id (@sorted_keys) {
973         my $v = $groups_with_subgroups->{$g_id};
974         my @subgroups;
975         if (my $sg = $v->{subgroups}) {
976             foreach my $sg_id (sort { $sg->{$a} cmp $sg->{$b} } keys %$sg) {
977                 push @subgroups, {
978                     id => $sg_id,
979                     name => $sg->{$sg_id},
980                     selected => ($group && $g_id eq $group && $subgroup && $sg_id eq $subgroup ),
981                 };
982             }
983         }
984         push @g_sg, {
985             id => $g_id,
986             name => $v->{name},
987             selected => ($group && $g_id eq $group),
988             subgroups => \@subgroups,
989         };
990     }
991     return \@g_sg;
992 }
993
994 sub create_non_existing_group_and_subgroup {
995     my ($input, $group, $subgroup) = @_;
996
997     if (defined $group and $group ne '') {
998         my $report_groups = C4::Reports::Guided::get_report_groups;
999         if (not exists $report_groups->{$group}) {
1000             my $groupdesc = $input->param('groupdesc') // $group;
1001             C4::Koha::AddAuthorisedValue('REPORT_GROUP', $group, $groupdesc);
1002         }
1003         if (defined $subgroup and $subgroup ne '') {
1004             if (not exists $report_groups->{$group}->{subgroups}->{$subgroup}) {
1005                 my $subgroupdesc = $input->param('subgroupdesc') // $subgroup;
1006                 C4::Koha::AddAuthorisedValue('REPORT_SUBGROUP', $subgroup, $subgroupdesc, $group);
1007             }
1008         }
1009     }
1010 }