MT2116 : Addons to the CSV Export
[koha.git] / circ / overdue.pl
blob4257fdf4230e0eecf6b4e1364668f86fbfeb45e0
1 #!/usr/bin/perl
4 # Copyright 2000-2002 Katipo Communications
6 # This file is part of Koha.
8 # Koha is free software; you can redistribute it and/or modify it under the
9 # terms of the GNU General Public License as published by the Free Software
10 # Foundation; either version 2 of the License, or (at your option) any later
11 # version.
13 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
14 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License along with
18 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
19 # Suite 330, Boston, MA 02111-1307 USA
21 use strict;
22 use warnings;
23 use C4::Context;
24 use C4::Output;
25 use CGI qw(-oldstyle_urls);
26 use C4::Auth;
27 use C4::Branch;
28 use C4::Debug;
29 use C4::Dates qw/format_date/;
30 use Date::Calc qw/Today/;
31 use Text::CSV_XS;
33 my $input = new CGI;
34 my $order = $input->param('order') || '';
35 my $showall = $input->param('showall');
36 my $bornamefilter = $input->param('borname') || '';
37 my $borcatfilter = $input->param('borcat') || '';
38 my $itemtypefilter = $input->param('itemtype') || '';
39 my $borflagsfilter = $input->param('borflag') || '';
40 my $branchfilter = $input->param('branch') || '';
41 my $op = $input->param('op') || '';
42 my $isfiltered = $op =~ /apply/i && $op =~ /filter/i;
43 my $noreport = C4::Context->preference('FilterBeforeOverdueReport') && ! $isfiltered && $op ne "csv";
45 my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
47 template_name => "circ/overdue.tmpl",
48 query => $input,
49 type => "intranet",
50 authnotrequired => 0,
51 flagsrequired => { reports => 1, circulate => "circulate_remaining_permissions" },
52 debug => 1,
56 my $dbh = C4::Context->dbh;
58 my $req;
59 $req = $dbh->prepare( "select categorycode, description from categories order by description");
60 $req->execute;
61 my @borcatloop;
62 while (my ($catcode, $description) =$req->fetchrow) {
63 push @borcatloop, {
64 value => $catcode,
65 selected => $catcode eq $borcatfilter ? 1 : 0,
66 catname => $description,
70 $req = $dbh->prepare( "select itemtype, description from itemtypes order by description");
71 $req->execute;
72 my @itemtypeloop;
73 while (my ($itemtype, $description) =$req->fetchrow) {
74 push @itemtypeloop, {
75 value => $itemtype,
76 selected => $itemtype eq $itemtypefilter ? 1 : 0,
77 itemtypename => $description,
80 my $onlymine=C4::Context->preference('IndependantBranches') &&
81 C4::Context->userenv &&
82 C4::Context->userenv->{flags} % 2 !=1 &&
83 C4::Context->userenv->{branch};
85 $branchfilter = C4::Context->userenv->{'branch'} if ($onlymine && !$branchfilter);
87 # Filtering by Patron Attributes
88 # @patron_attr_filter_loop is non empty if there are any patron attribute filters
89 # %cgi_attrcode_to_attrvalues contains the patron attribute filter values, as returned by the CGI
90 # %borrowernumber_to_attributes is populated by those borrowernumbers matching the patron attribute filters
92 my %cgi_attrcode_to_attrvalues; # ( patron_attribute_code => [ zero or more attribute filter values from the CGI ] )
93 for my $attrcode (grep { /^patron_attr_filter_/ } $input->param) {
94 if (my @attrvalues = grep { length($_) > 0 } $input->param($attrcode)) {
95 $attrcode =~ s/^patron_attr_filter_//;
96 $cgi_attrcode_to_attrvalues{$attrcode} = \@attrvalues;
97 print STDERR ">>>param($attrcode)[@{[scalar @attrvalues]}] = '@attrvalues'\n" if $debug;
100 my $have_pattr_filter_data = keys(%cgi_attrcode_to_attrvalues) > 0;
102 my @patron_attr_filter_loop; # array of [ domid cgivalue ismany isclone ordinal code description repeatable authorised_value_category ]
103 #my @patron_attr_order_loop; # array of { label => $patron_attr_label, value => $patron_attr_order }
105 my @sort_roots = qw(borrower title barcode date_due);
106 push @sort_roots, map {$_ . " desc"} @sort_roots;
107 my @order_loop = ({selected => $order ? 0 : 1}); # initial blank row
108 foreach (@sort_roots) {
109 my $tmpl_name = $_;
110 $tmpl_name =~ s/\s/_/g;
111 push @order_loop, {
112 selected => $order eq $_ ? 1 : 0,
113 ordervalue => $_,
114 'order_' . $tmpl_name => 1,
118 my $sth = $dbh->prepare('SELECT code,description,repeatable,authorised_value_category
119 FROM borrower_attribute_types
120 WHERE staff_searchable <> 0
121 ORDER BY description');
122 $sth->execute();
123 my $ordinal = 0;
124 while (my $row = $sth->fetchrow_hashref) {
125 $row->{ordinal} = $ordinal;
126 my $code = $row->{code};
127 my $cgivalues = $cgi_attrcode_to_attrvalues{$code} || [ '' ];
128 my $isclone = 0;
129 $row->{ismany} = @$cgivalues > 1;
130 my $serial = 0;
131 for (@$cgivalues) {
132 $row->{domid} = $ordinal * 1000 + $serial;
133 $row->{cgivalue} = $_;
134 $row->{isclone} = $isclone;
135 push @patron_attr_filter_loop, { %$row }; # careful: must store a *deep copy* of the modified row
136 } continue { $isclone = 1, ++$serial }
137 foreach my $sortorder ('asc', 'desc') {
138 my $ordervalue = "patron_attr_${sortorder}_${code}";
139 push @order_loop, {
140 selected => $order eq $ordervalue ? 1 : 0,
141 ordervalue => $ordervalue,
142 label => $row->{description},
143 $sortorder => 1,
146 } continue { ++$ordinal }
147 #for (@patron_attr_order_loop) { $_->{selected} = 1 if $order eq $_->{value} }
149 $template->param(ORDER_LOOP => \@order_loop);
151 my %borrowernumber_to_attributes; # hash of { borrowernumber => { attrcode => [ [val,display], [val,display], ... ] } }
152 # i.e. val differs from display when attr is an authorised value
153 if (0 && @patron_attr_filter_loop) {
154 # MAYBE FIXME: currently, *all* borrower_attributes are loaded into %borrowernumber_to_attributes
155 # then filtered and honed down to match the patron attribute filters. If this is
156 # too resource intensive, MySQL can be used to do the filtering, i.e. rewire the
157 # SQL below to select only those attribute values that match the filters.
159 my $sql = q(SELECT borrowernumber AS bn, b.code, attribute AS val, category AS avcategory, lib AS avdescription
160 FROM borrower_attributes b
161 JOIN borrower_attribute_types bt ON (b.code = bt.code)
162 LEFT JOIN authorised_values a ON (a.category = bt.authorised_value_category AND a.authorised_value = b.attribute));
163 my $sth = $dbh->prepare($sql);
164 $sth->execute();
165 while (my $row = $sth->fetchrow_hashref) {
166 my $pattrs = $borrowernumber_to_attributes{$row->{bn}} ||= { };
167 push @{ $pattrs->{$row->{code}} }, [
168 $row->{val},
169 defined $row->{avdescription} ? $row->{avdescription} : $row->{val},
173 for my $bn (keys %borrowernumber_to_attributes) {
174 my $pattrs = $borrowernumber_to_attributes{$bn};
175 my $keep = 1;
176 for my $code (keys %cgi_attrcode_to_attrvalues) {
177 # discard patrons that do not match (case insensitive) at least one of each attribute filter value
178 my $discard = 1;
179 for my $attrval (map { lc $_ } @{ $cgi_attrcode_to_attrvalues{$code} }) {
180 ## if (grep { $attrval eq lc($_->[0]) } @{ $pattrs->{$code} })
181 if (grep { $attrval eq lc($_->[1]) } @{ $pattrs->{$code} }) {
182 $discard = 0;
183 last;
186 if ($discard) {
187 $keep = 0;
188 last;
191 if ($debug) {
192 my $showkeep = $keep ? 'keep' : 'do NOT keep';
193 print STDERR ">>> patron $bn: $showkeep attributes: ";
194 for (sort keys %$pattrs) { my @a=map { "$_->[0]/$_->[1] " } @{$pattrs->{$_}}; print STDERR "attrcode $_ = [@a] " }
195 print STDERR "\n";
197 delete $borrowernumber_to_attributes{$bn} if !$keep;
202 $template->param(
203 branchloop => GetBranchesLoop($branchfilter, $onlymine),
204 branchfilter => $branchfilter,
205 borcatloop=> \@borcatloop,
206 itemtypeloop => \@itemtypeloop,
207 # patron_attr_filter_loop => \@patron_attr_filter_loop,
208 borname => $bornamefilter,
209 order => $order,
210 showall => $showall);
212 if ($noreport) {
213 # la de dah ... page comes up presto-quicko
214 $template->param( noreport => $noreport );
215 } else {
216 # FIXME : the left joins + where clauses make the following SQL query really slow with large datasets :(
218 # FIX 1: use the table with the least rows as first in the join, second least second, etc
219 # ref: http://www.fiftyfoureleven.com/weblog/web-development/programming-and-scripts/mysql-optimization-tip
221 # FIX 2: ensure there are indexes for columns participating in the WHERE clauses, where feasible/reasonable
224 my $todaysdate = sprintf("%-04.4d-%-02.2d-%02.2d", Today());
226 $bornamefilter =~s/\*/\%/g;
227 $bornamefilter =~s/\?/\_/g;
229 my $strsth="SELECT date_due,
230 concat(surname,' ', firstname) as borrower,
231 borrowers.phone,
232 borrowers.email,
233 issues.itemnumber,
234 items.barcode,
235 biblio.title,
236 biblio.author,
237 borrowers.borrowernumber,
238 biblio.biblionumber,
239 borrowers.branchcode,
240 items.itemcallnumber,
241 items.replacementprice
242 FROM issues
243 LEFT JOIN borrowers ON (issues.borrowernumber=borrowers.borrowernumber )
244 LEFT JOIN items ON (issues.itemnumber=items.itemnumber)
245 LEFT JOIN biblioitems ON (biblioitems.biblioitemnumber=items.biblioitemnumber)
246 LEFT JOIN biblio ON (biblio.biblionumber=items.biblionumber )
247 WHERE 1=1 "; # placeholder, since it is possible that none of the additional
248 # conditions will be selected by user
249 $strsth.=" AND date_due < '" . $todaysdate . "' " unless ($showall);
250 $strsth.=" AND (borrowers.firstname like '".$bornamefilter."%' or borrowers.surname like '".$bornamefilter."%' or borrowers.cardnumber like '".$bornamefilter."%')" if($bornamefilter) ;
251 $strsth.=" AND borrowers.categorycode = '" . $borcatfilter . "' " if $borcatfilter;
252 $strsth.=" AND biblioitems.itemtype = '" . $itemtypefilter . "' " if $itemtypefilter;
253 $strsth.=" AND borrowers.flags = '" . $borflagsfilter . "' " if $borflagsfilter;
254 $strsth.=" AND borrowers.branchcode = '" . $branchfilter . "' " if $branchfilter;
255 # restrict patrons (borrowers) to those matching the patron attribute filter(s), if any
256 my $bnlist = $have_pattr_filter_data ? join(',',keys %borrowernumber_to_attributes) : '';
257 $strsth =~ s/WHERE 1=1/WHERE 1=1 AND borrowers.borrowernumber IN ($bnlist)/ if $bnlist;
258 $strsth =~ s/WHERE 1=1/WHERE 0=1/ if $have_pattr_filter_data && !$bnlist; # no match if no borrowers matched patron attrs
259 $strsth.=" ORDER BY " . (
260 ($order eq "borrower" or $order eq "borrower desc") ? "$order, date_due" :
261 ($order eq "title" or $order eq "title desc") ? "$order, date_due, borrower" :
262 ($order eq "barcode" or $order eq "barcode desc") ? "items.$order, date_due, borrower" :
263 ($order eq "date_due desc") ? "date_due DESC, borrower" :
264 "date_due, borrower" # default sort order
266 $template->param(sql=>$strsth);
267 my $sth=$dbh->prepare($strsth);
268 #warn "overdue.pl : query string ".$strsth;
269 $sth->execute();
271 my @overduedata;
272 while (my $data = $sth->fetchrow_hashref) {
274 # most of the overdue report data is linked to the database schema, i.e. things like borrowernumber and phone
275 # but the patron attributes (patron_attr_value_loop) are unnormalised and varies dynamically from one db to the next
277 my $pattrs = $borrowernumber_to_attributes{$data->{borrowernumber}} || {}; # patron attrs for this borrower
278 # $pattrs is a hash { attrcode => [ [value,displayvalue], [value,displayvalue]... ] }
280 my @patron_attr_value_loop; # template array [ {value=>v1}, {value=>v2} ... } ]
281 for my $pattr_filter (grep { ! $_->{isclone} } @patron_attr_filter_loop) {
282 my @displayvalues = map { $_->[1] } @{ $pattrs->{$pattr_filter->{code}} }; # grab second value from each subarray
283 push @patron_attr_value_loop, { value => join(', ', sort { lc $a cmp lc $b } @displayvalues) };
286 push @overduedata, {
287 duedate => format_date($data->{date_due}),
288 borrowernumber => $data->{borrowernumber},
289 barcode => $data->{barcode},
290 itemnum => $data->{itemnumber},
291 name => $data->{borrower},
292 phone => $data->{phone},
293 email => $data->{email},
294 biblionumber => $data->{biblionumber},
295 title => $data->{title},
296 author => $data->{author},
297 branchcode => $data->{branchcode},
298 itemcallnumber => $data->{itemcallnumber},
299 replacementprice => $data->{replacementprice},
300 patron_attr_value_loop => \@patron_attr_value_loop,
304 my ($attrorder) = $order =~ /patron_attr_(.*)$/;
305 my $patrorder = '';
306 my $sortorder = 'asc';
307 if (defined $attrorder) {
308 ($sortorder, $patrorder) = split /_/, $attrorder, 2;
310 print STDERR ">>> order is $order, patrorder is $patrorder, sortorder is $sortorder\n" if $debug;
312 if (my @attrtype = grep { $_->{'code'} eq $patrorder } @patron_attr_filter_loop) { # sort by patron attrs perhaps?
313 my $ordinal = $attrtype[0]{ordinal};
314 print STDERR ">>> sort ordinal is $ordinal\n" if $debug;
316 sub patronattr_sorter_asc {
317 lc $a->{patron_attr_value_loop}[$ordinal]{value}
319 lc $b->{patron_attr_value_loop}[$ordinal]{value} }
321 sub patronattr_sorter_des { -patronattr_sorter_asc() }
323 my $sorter = $sortorder eq 'desc' ? \&patronattr_sorter_des : \&patronattr_sorter_asc;
324 @overduedata = sort $sorter @overduedata;
327 if ($op eq 'csv') {
328 binmode(STDOUT, ":utf8");
329 my $csv = build_csv(\@overduedata);
330 print $input->header(-type => 'application/vnd.sun.xml.calc',
331 -encoding => 'utf-8',
332 -attachment=>"overdues.csv",
333 -filename=>"overdues.csv" );
334 print $csv;
335 exit;
338 # generate parameter list for CSV download link
339 my $new_cgi = CGI->new($input);
340 $new_cgi->delete('op');
341 my $csv_param_string = $new_cgi->query_string();
343 $template->param(
344 csv_param_string => $csv_param_string,
345 todaysdate => format_date($todaysdate),
346 overdueloop => \@overduedata,
347 nnoverdue => scalar(@overduedata),
348 noverdue_is_plural => scalar(@overduedata) != 1,
349 noreport => $noreport,
350 isfiltered => $isfiltered,
351 borflag_gonenoaddress => $borflagsfilter eq 'gonenoaddress',
352 borflag_debarred => $borflagsfilter eq 'debarred',
353 borflag_lost => $borflagsfilter eq 'lost',
358 output_html_with_http_headers $input, $cookie, $template->output;
361 sub build_csv {
362 my $overdues = shift;
364 return "" if scalar(@$overdues) == 0;
366 my @lines = ();
368 # build header ...
369 my @keys = grep { $_ ne 'patron_attr_value_loop' } sort keys %{ $overdues->[0] };
370 my $csv = Text::CSV_XS->new();
371 $csv->combine(@keys);
372 push @lines, $csv->string();
374 # ... and rest of report
375 foreach my $overdue ( @{ $overdues } ) {
376 push @lines, $csv->string() if $csv->combine(map { $overdue->{$_} } @keys);
379 return join("\n", @lines) . "\n";