Bug 12583: (followup) rename new test file
[koha.git] / circ / overdue.pl
blobc2a0db113338ca5122cc70baa406149a9b9bdffb
1 #!/usr/bin/perl
4 # Copyright 2000-2002 Katipo Communications
5 # Parts copyright 2010 BibLibre
7 # This file is part of Koha.
9 # Koha is free software; you can redistribute it and/or modify it under the
10 # terms of the GNU General Public License as published by the Free Software
11 # Foundation; either version 2 of the License, or (at your option) any later
12 # version.
14 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
15 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License along
19 # with Koha; if not, write to the Free Software Foundation, Inc.,
20 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 use strict;
23 use warnings;
24 use C4::Context;
25 use C4::Output;
26 use CGI qw(-oldstyle_urls);
27 use C4::Auth;
28 use C4::Branch;
29 use C4::Debug;
30 use C4::Dates qw/format_date format_date_in_iso/;
31 use Text::CSV_XS;
32 use Koha::DateUtils;
33 use DateTime;
35 my $input = new CGI;
36 my $order = $input->param('order') || '';
37 my $showall = $input->param('showall');
38 my $bornamefilter = $input->param('borname') || '';
39 my $borcatfilter = $input->param('borcat') || '';
40 my $itemtypefilter = $input->param('itemtype') || '';
41 my $borflagsfilter = $input->param('borflag') || '';
42 my $branchfilter = $input->param('branch') || '';
43 my $homebranchfilter = $input->param('homebranch') || '';
44 my $holdingbranchfilter = $input->param('holdingbranch') || '';
45 my $op = $input->param('op') || '';
46 my $dateduefrom = format_date_in_iso($input->param( 'dateduefrom' )) || '';
47 my $datedueto = format_date_in_iso($input->param( 'datedueto' )) || '';
48 # FIXME This is a kludge to include times
49 if ($datedueto) {
50 $datedueto .= ' 23:59';
52 if ($dateduefrom) {
53 $dateduefrom .= ' 00:00';
55 # kludge end
56 my $isfiltered = $op =~ /apply/i && $op =~ /filter/i;
57 my $noreport = C4::Context->preference('FilterBeforeOverdueReport') && ! $isfiltered && $op ne "csv";
59 my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
61 template_name => "circ/overdue.tt",
62 query => $input,
63 type => "intranet",
64 authnotrequired => 0,
65 flagsrequired => { circulate => "overdues_report" },
66 debug => 1,
70 my $dbh = C4::Context->dbh;
72 my $req;
73 $req = $dbh->prepare( "select categorycode, description from categories order by description");
74 $req->execute;
75 my @borcatloop;
76 while (my ($catcode, $description) =$req->fetchrow) {
77 push @borcatloop, {
78 value => $catcode,
79 selected => $catcode eq $borcatfilter ? 1 : 0,
80 catname => $description,
84 $req = $dbh->prepare( "select itemtype, description from itemtypes order by description");
85 $req->execute;
86 my @itemtypeloop;
87 while (my ($itemtype, $description) =$req->fetchrow) {
88 push @itemtypeloop, {
89 value => $itemtype,
90 selected => $itemtype eq $itemtypefilter ? 1 : 0,
91 itemtypename => $description,
94 my $onlymine =
95 C4::Context->preference('IndependentBranches')
96 && C4::Context->userenv
97 && !C4::Context->IsSuperLibrarian()
98 && C4::Context->userenv->{branch};
100 $branchfilter = C4::Context->userenv->{'branch'} if ($onlymine && !$branchfilter);
102 # Filtering by Patron Attributes
103 # @patron_attr_filter_loop is non empty if there are any patron attribute filters
104 # %cgi_attrcode_to_attrvalues contains the patron attribute filter values, as returned by the CGI
105 # %borrowernumber_to_attributes is populated by those borrowernumbers matching the patron attribute filters
107 my %cgi_attrcode_to_attrvalues; # ( patron_attribute_code => [ zero or more attribute filter values from the CGI ] )
108 for my $attrcode (grep { /^patron_attr_filter_/ } $input->param) {
109 if (my @attrvalues = grep { length($_) > 0 } $input->param($attrcode)) {
110 $attrcode =~ s/^patron_attr_filter_//;
111 $cgi_attrcode_to_attrvalues{$attrcode} = \@attrvalues;
112 print STDERR ">>>param($attrcode)[@{[scalar @attrvalues]}] = '@attrvalues'\n" if $debug;
115 my $have_pattr_filter_data = keys(%cgi_attrcode_to_attrvalues) > 0;
117 my @patron_attr_filter_loop; # array of [ domid cgivalue ismany isclone ordinal code description repeatable authorised_value_category ]
118 my @patron_attr_order_loop; # array of { label => $patron_attr_label, value => $patron_attr_order }
120 my @sort_roots = qw(borrower title barcode date_due);
121 push @sort_roots, map {$_ . " desc"} @sort_roots;
122 my @order_loop = ({selected => $order ? 0 : 1}); # initial blank row
123 foreach (@sort_roots) {
124 my $tmpl_name = $_;
125 $tmpl_name =~ s/\s/_/g;
126 push @order_loop, {
127 selected => $order eq $_ ? 1 : 0,
128 ordervalue => $_,
129 'order_' . $tmpl_name => 1,
133 my $sth = $dbh->prepare('SELECT code,description,repeatable,authorised_value_category
134 FROM borrower_attribute_types
135 WHERE staff_searchable <> 0
136 ORDER BY description');
137 $sth->execute();
138 my $ordinal = 0;
139 while (my $row = $sth->fetchrow_hashref) {
140 $row->{ordinal} = $ordinal;
141 my $code = $row->{code};
142 my $cgivalues = $cgi_attrcode_to_attrvalues{$code} || [ '' ];
143 my $isclone = 0;
144 $row->{ismany} = @$cgivalues > 1;
145 my $serial = 0;
146 for (@$cgivalues) {
147 $row->{domid} = $ordinal * 1000 + $serial;
148 $row->{cgivalue} = $_;
149 $row->{isclone} = $isclone;
150 push @patron_attr_filter_loop, { %$row }; # careful: must store a *deep copy* of the modified row
151 } continue { $isclone = 1, ++$serial }
152 foreach my $sortorder ('asc', 'desc') {
153 my $ordervalue = "patron_attr_${sortorder}_${code}";
154 push @order_loop, {
155 selected => $order eq $ordervalue ? 1 : 0,
156 ordervalue => $ordervalue,
157 label => $row->{description},
158 $sortorder => 1,
161 } continue { ++$ordinal }
162 for (@patron_attr_order_loop) { $_->{selected} = 1 if $order eq $_->{value} }
164 $template->param(ORDER_LOOP => \@order_loop);
166 my %borrowernumber_to_attributes; # hash of { borrowernumber => { attrcode => [ [val,display], [val,display], ... ] } }
167 # i.e. val differs from display when attr is an authorised value
168 if (@patron_attr_filter_loop) {
169 # MAYBE FIXME: currently, *all* borrower_attributes are loaded into %borrowernumber_to_attributes
170 # then filtered and honed down to match the patron attribute filters. If this is
171 # too resource intensive, MySQL can be used to do the filtering, i.e. rewire the
172 # SQL below to select only those attribute values that match the filters.
174 my $sql = q(SELECT borrowernumber AS bn, b.code, attribute AS val, category AS avcategory, lib AS avdescription
175 FROM borrower_attributes b
176 JOIN borrower_attribute_types bt ON (b.code = bt.code)
177 LEFT JOIN authorised_values a ON (a.category = bt.authorised_value_category AND a.authorised_value = b.attribute));
178 my $sth = $dbh->prepare($sql);
179 $sth->execute();
180 while (my $row = $sth->fetchrow_hashref) {
181 my $pattrs = $borrowernumber_to_attributes{$row->{bn}} ||= { };
182 push @{ $pattrs->{$row->{code}} }, [
183 $row->{val},
184 defined $row->{avdescription} ? $row->{avdescription} : $row->{val},
188 for my $bn (keys %borrowernumber_to_attributes) {
189 my $pattrs = $borrowernumber_to_attributes{$bn};
190 my $keep = 1;
191 for my $code (keys %cgi_attrcode_to_attrvalues) {
192 # discard patrons that do not match (case insensitive) at least one of each attribute filter value
193 my $discard = 1;
194 for my $attrval (map { lc $_ } @{ $cgi_attrcode_to_attrvalues{$code} }) {
195 ## if (grep { $attrval eq lc($_->[0]) } @{ $pattrs->{$code} })
196 if (grep { $attrval eq lc($_->[1]) } @{ $pattrs->{$code} }) {
197 $discard = 0;
198 last;
201 if ($discard) {
202 $keep = 0;
203 last;
206 if ($debug) {
207 my $showkeep = $keep ? 'keep' : 'do NOT keep';
208 print STDERR ">>> patron $bn: $showkeep attributes: ";
209 for (sort keys %$pattrs) { my @a=map { "$_->[0]/$_->[1] " } @{$pattrs->{$_}}; print STDERR "attrcode $_ = [@a] " }
210 print STDERR "\n";
212 delete $borrowernumber_to_attributes{$bn} if !$keep;
217 $template->param(
218 patron_attr_header_loop => [ map { { header => $_->{description} } } grep { ! $_->{isclone} } @patron_attr_filter_loop ],
219 branchloop => GetBranchesLoop($branchfilter, $onlymine),
220 homebranchloop => GetBranchesLoop( $homebranchfilter, $onlymine ),
221 holdingbranchloop => GetBranchesLoop( $holdingbranchfilter, $onlymine ),
222 branchfilter => $branchfilter,
223 homebranchfilter => $homebranchfilter,
224 holdingbranchfilter => $homebranchfilter,
225 borcatloop=> \@borcatloop,
226 itemtypeloop => \@itemtypeloop,
227 patron_attr_filter_loop => \@patron_attr_filter_loop,
228 borname => $bornamefilter,
229 order => $order,
230 showall => $showall,
231 dateduefrom => $input->param( 'dateduefrom' ) || '',
232 datedueto => $input->param( 'datedueto' ) || '',
235 if ($noreport) {
236 # la de dah ... page comes up presto-quicko
237 $template->param( noreport => $noreport );
238 } else {
239 # FIXME : the left joins + where clauses make the following SQL query really slow with large datasets :(
241 # FIX 1: use the table with the least rows as first in the join, second least second, etc
242 # ref: http://www.fiftyfoureleven.com/weblog/web-development/programming-and-scripts/mysql-optimization-tip
244 # FIX 2: ensure there are indexes for columns participating in the WHERE clauses, where feasible/reasonable
247 my $today_dt = DateTime->now(time_zone => C4::Context->tz);
248 $today_dt->truncate(to => 'minute');
249 my $todaysdate = $today_dt->strftime('%Y-%m-%d %H:%M');
251 $bornamefilter =~s/\*/\%/g;
252 $bornamefilter =~s/\?/\_/g;
254 my $strsth="SELECT date_due,
255 borrowers.title as borrowertitle,
256 borrowers.surname,
257 borrowers.firstname,
258 borrowers.streetnumber,
259 borrowers.streettype,
260 borrowers.address,
261 borrowers.address2,
262 borrowers.city,
263 borrowers.zipcode,
264 borrowers.country,
265 borrowers.phone,
266 borrowers.email,
267 issues.itemnumber,
268 issues.issuedate,
269 items.barcode,
270 items.homebranch,
271 items.holdingbranch,
272 biblio.title,
273 biblio.author,
274 borrowers.borrowernumber,
275 biblio.biblionumber,
276 borrowers.branchcode,
277 items.itemcallnumber,
278 items.replacementprice,
279 items.enumchron
280 FROM issues
281 LEFT JOIN borrowers ON (issues.borrowernumber=borrowers.borrowernumber )
282 LEFT JOIN items ON (issues.itemnumber=items.itemnumber)
283 LEFT JOIN biblioitems ON (biblioitems.biblioitemnumber=items.biblioitemnumber)
284 LEFT JOIN biblio ON (biblio.biblionumber=items.biblionumber )
285 WHERE 1=1 "; # placeholder, since it is possible that none of the additional
286 # conditions will be selected by user
287 $strsth.=" AND date_due < '" . $todaysdate . "' " unless ($showall);
288 $strsth.=" AND (borrowers.firstname like '".$bornamefilter."%' or borrowers.surname like '".$bornamefilter."%' or borrowers.cardnumber like '".$bornamefilter."%')" if($bornamefilter) ;
289 $strsth.=" AND borrowers.categorycode = '" . $borcatfilter . "' " if $borcatfilter;
290 if( $itemtypefilter ){
291 if( C4::Context->preference('item-level_itypes') ){
292 $strsth.=" AND items.itype = '" . $itemtypefilter . "' ";
293 } else {
294 $strsth.=" AND biblioitems.itemtype = '" . $itemtypefilter . "' ";
297 if ( $borflagsfilter eq 'gonenoaddress' ) {
298 $strsth .= " AND borrowers.gonenoaddress <> 0";
300 elsif ( $borflagsfilter eq 'debarred' ) {
301 $strsth .= " AND borrowers.debarred >= CURDATE()" ;
303 elsif ( $borflagsfilter eq 'lost') {
304 $strsth .= " AND borrowers.lost <> 0";
306 $strsth.=" AND borrowers.branchcode = '" . $branchfilter . "' " if $branchfilter;
307 $strsth.=" AND items.homebranch = '" . $homebranchfilter . "' " if $homebranchfilter;
308 $strsth.=" AND items.holdingbranch = '" . $holdingbranchfilter . "' " if $holdingbranchfilter;
309 $strsth.=" AND date_due < '" . $datedueto . "' " if $datedueto;
310 $strsth.=" AND date_due > '" . $dateduefrom . "' " if $dateduefrom;
311 # restrict patrons (borrowers) to those matching the patron attribute filter(s), if any
312 my $bnlist = $have_pattr_filter_data ? join(',',keys %borrowernumber_to_attributes) : '';
313 $strsth =~ s/WHERE 1=1/WHERE 1=1 AND borrowers.borrowernumber IN ($bnlist)/ if $bnlist;
314 $strsth =~ s/WHERE 1=1/WHERE 0=1/ if $have_pattr_filter_data && !$bnlist; # no match if no borrowers matched patron attrs
315 $strsth.=" ORDER BY " . (
316 ($order eq "borrower") ? "surname, firstname, date_due" :
317 ($order eq "borrower desc") ? "surname desc, firstname desc, date_due" :
318 ($order eq "title" or $order eq "title desc") ? "$order, date_due, surname, firstname" :
319 ($order eq "barcode" or $order eq "barcode desc") ? "items.$order, date_due, surname, firstname" :
320 ($order eq "date_due desc") ? "date_due DESC, surname, firstname" :
321 "date_due, surname, firstname" # default sort order
323 $template->param(sql=>$strsth);
324 my $sth=$dbh->prepare($strsth);
325 #warn "overdue.pl : query string ".$strsth;
326 $sth->execute();
328 my @overduedata;
329 while (my $data = $sth->fetchrow_hashref) {
331 # most of the overdue report data is linked to the database schema, i.e. things like borrowernumber and phone
332 # but the patron attributes (patron_attr_value_loop) are unnormalised and varies dynamically from one db to the next
334 my $pattrs = $borrowernumber_to_attributes{$data->{borrowernumber}} || {}; # patron attrs for this borrower
335 # $pattrs is a hash { attrcode => [ [value,displayvalue], [value,displayvalue]... ] }
337 my @patron_attr_value_loop; # template array [ {value=>v1}, {value=>v2} ... } ]
338 for my $pattr_filter (grep { ! $_->{isclone} } @patron_attr_filter_loop) {
339 my @displayvalues = map { $_->[1] } @{ $pattrs->{$pattr_filter->{code}} }; # grab second value from each subarray
340 push @patron_attr_value_loop, { value => join(', ', sort { lc $a cmp lc $b } @displayvalues) };
342 my $dt = dt_from_string($data->{date_due}, 'sql');
344 push @overduedata, {
345 duedate => output_pref($dt),
346 borrowernumber => $data->{borrowernumber},
347 barcode => $data->{barcode},
348 itemnum => $data->{itemnumber},
349 issuedate => format_date($data->{issuedate}),
350 borrowertitle => $data->{borrowertitle},
351 surname => $data->{surname},
352 firstname => $data->{firstname},
353 streetnumber => $data->{streetnumber},
354 streettype => $data->{streettype},
355 address => $data->{address},
356 address2 => $data->{address2},
357 city => $data->{city},
358 zipcode => $data->{zipcode},
359 country => $data->{country},
360 phone => $data->{phone},
361 email => $data->{email},
362 biblionumber => $data->{biblionumber},
363 title => $data->{title},
364 author => $data->{author},
365 branchcode => $data->{branchcode},
366 homebranchcode => $data->{homebranchcode},
367 holdingbranchcode => $data->{holdingbranchcode},
368 itemcallnumber => $data->{itemcallnumber},
369 replacementprice => $data->{replacementprice},
370 enumchron => $data->{enumchron},
371 patron_attr_value_loop => \@patron_attr_value_loop,
375 my ($attrorder) = $order =~ /patron_attr_(.*)$/;
376 my $patrorder = '';
377 my $sortorder = 'asc';
378 if (defined $attrorder) {
379 ($sortorder, $patrorder) = split /_/, $attrorder, 2;
381 print STDERR ">>> order is $order, patrorder is $patrorder, sortorder is $sortorder\n" if $debug;
383 if (my @attrtype = grep { $_->{'code'} eq $patrorder } @patron_attr_filter_loop) { # sort by patron attrs perhaps?
384 my $ordinal = $attrtype[0]{ordinal};
385 print STDERR ">>> sort ordinal is $ordinal\n" if $debug;
387 sub patronattr_sorter_asc {
388 lc $a->{patron_attr_value_loop}[$ordinal]{value}
390 lc $b->{patron_attr_value_loop}[$ordinal]{value} }
392 sub patronattr_sorter_des { -patronattr_sorter_asc() }
394 my $sorter = $sortorder eq 'desc' ? \&patronattr_sorter_des : \&patronattr_sorter_asc;
395 @overduedata = sort $sorter @overduedata;
398 if ($op eq 'csv') {
399 binmode(STDOUT, ":encoding(UTF-8)");
400 my $csv = build_csv(\@overduedata);
401 print $input->header(-type => 'application/vnd.sun.xml.calc',
402 -encoding => 'utf-8',
403 -attachment=>"overdues.csv",
404 -filename=>"overdues.csv" );
405 print $csv;
406 exit;
409 # generate parameter list for CSV download link
410 my $new_cgi = CGI->new($input);
411 $new_cgi->delete('op');
412 my $csv_param_string = $new_cgi->query_string();
414 $template->param(
415 csv_param_string => $csv_param_string,
416 todaysdate => output_pref($today_dt),
417 overdueloop => \@overduedata,
418 nnoverdue => scalar(@overduedata),
419 noverdue_is_plural => scalar(@overduedata) != 1,
420 noreport => $noreport,
421 isfiltered => $isfiltered,
422 borflag_gonenoaddress => $borflagsfilter eq 'gonenoaddress',
423 borflag_debarred => $borflagsfilter eq 'debarred',
424 borflag_lost => $borflagsfilter eq 'lost',
429 output_html_with_http_headers $input, $cookie, $template->output;
432 sub build_csv {
433 my $overdues = shift;
435 return "" if scalar(@$overdues) == 0;
437 my @lines = ();
439 # build header ...
440 my @keys = qw /duedate title author borrowertitle firstname surname phone barcode email address address2 zipcode city country
441 branchcode itemcallnumber biblionumber borrowernumber itemnum issuedate replacementprice streetnumber streettype/;
442 my $csv = Text::CSV_XS->new();
443 $csv->combine(@keys);
444 push @lines, $csv->string();
446 # ... and rest of report
447 foreach my $overdue ( @{ $overdues } ) {
448 push @lines, $csv->string() if $csv->combine(map { $overdue->{$_} } @keys);
451 return join("\n", @lines) . "\n";