Bug 11032: Check a valid MARC::Record passed to Biblio
[koha.git] / circ / overdue.pl
blob4343592e5af8c600ceca43c59d701dd36849b863
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.tmpl",
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=C4::Context->preference('IndependentBranches') &&
95 C4::Context->userenv &&
96 C4::Context->userenv->{flags} % 2 !=1 &&
97 C4::Context->userenv->{branch};
99 $branchfilter = C4::Context->userenv->{'branch'} if ($onlymine && !$branchfilter);
101 # Filtering by Patron Attributes
102 # @patron_attr_filter_loop is non empty if there are any patron attribute filters
103 # %cgi_attrcode_to_attrvalues contains the patron attribute filter values, as returned by the CGI
104 # %borrowernumber_to_attributes is populated by those borrowernumbers matching the patron attribute filters
106 my %cgi_attrcode_to_attrvalues; # ( patron_attribute_code => [ zero or more attribute filter values from the CGI ] )
107 for my $attrcode (grep { /^patron_attr_filter_/ } $input->param) {
108 if (my @attrvalues = grep { length($_) > 0 } $input->param($attrcode)) {
109 $attrcode =~ s/^patron_attr_filter_//;
110 $cgi_attrcode_to_attrvalues{$attrcode} = \@attrvalues;
111 print STDERR ">>>param($attrcode)[@{[scalar @attrvalues]}] = '@attrvalues'\n" if $debug;
114 my $have_pattr_filter_data = keys(%cgi_attrcode_to_attrvalues) > 0;
116 my @patron_attr_filter_loop; # array of [ domid cgivalue ismany isclone ordinal code description repeatable authorised_value_category ]
117 my @patron_attr_order_loop; # array of { label => $patron_attr_label, value => $patron_attr_order }
119 my @sort_roots = qw(borrower title barcode date_due);
120 push @sort_roots, map {$_ . " desc"} @sort_roots;
121 my @order_loop = ({selected => $order ? 0 : 1}); # initial blank row
122 foreach (@sort_roots) {
123 my $tmpl_name = $_;
124 $tmpl_name =~ s/\s/_/g;
125 push @order_loop, {
126 selected => $order eq $_ ? 1 : 0,
127 ordervalue => $_,
128 'order_' . $tmpl_name => 1,
132 my $sth = $dbh->prepare('SELECT code,description,repeatable,authorised_value_category
133 FROM borrower_attribute_types
134 WHERE staff_searchable <> 0
135 ORDER BY description');
136 $sth->execute();
137 my $ordinal = 0;
138 while (my $row = $sth->fetchrow_hashref) {
139 $row->{ordinal} = $ordinal;
140 my $code = $row->{code};
141 my $cgivalues = $cgi_attrcode_to_attrvalues{$code} || [ '' ];
142 my $isclone = 0;
143 $row->{ismany} = @$cgivalues > 1;
144 my $serial = 0;
145 for (@$cgivalues) {
146 $row->{domid} = $ordinal * 1000 + $serial;
147 $row->{cgivalue} = $_;
148 $row->{isclone} = $isclone;
149 push @patron_attr_filter_loop, { %$row }; # careful: must store a *deep copy* of the modified row
150 } continue { $isclone = 1, ++$serial }
151 foreach my $sortorder ('asc', 'desc') {
152 my $ordervalue = "patron_attr_${sortorder}_${code}";
153 push @order_loop, {
154 selected => $order eq $ordervalue ? 1 : 0,
155 ordervalue => $ordervalue,
156 label => $row->{description},
157 $sortorder => 1,
160 } continue { ++$ordinal }
161 for (@patron_attr_order_loop) { $_->{selected} = 1 if $order eq $_->{value} }
163 $template->param(ORDER_LOOP => \@order_loop);
165 my %borrowernumber_to_attributes; # hash of { borrowernumber => { attrcode => [ [val,display], [val,display], ... ] } }
166 # i.e. val differs from display when attr is an authorised value
167 if (@patron_attr_filter_loop) {
168 # MAYBE FIXME: currently, *all* borrower_attributes are loaded into %borrowernumber_to_attributes
169 # then filtered and honed down to match the patron attribute filters. If this is
170 # too resource intensive, MySQL can be used to do the filtering, i.e. rewire the
171 # SQL below to select only those attribute values that match the filters.
173 my $sql = q(SELECT borrowernumber AS bn, b.code, attribute AS val, category AS avcategory, lib AS avdescription
174 FROM borrower_attributes b
175 JOIN borrower_attribute_types bt ON (b.code = bt.code)
176 LEFT JOIN authorised_values a ON (a.category = bt.authorised_value_category AND a.authorised_value = b.attribute));
177 my $sth = $dbh->prepare($sql);
178 $sth->execute();
179 while (my $row = $sth->fetchrow_hashref) {
180 my $pattrs = $borrowernumber_to_attributes{$row->{bn}} ||= { };
181 push @{ $pattrs->{$row->{code}} }, [
182 $row->{val},
183 defined $row->{avdescription} ? $row->{avdescription} : $row->{val},
187 for my $bn (keys %borrowernumber_to_attributes) {
188 my $pattrs = $borrowernumber_to_attributes{$bn};
189 my $keep = 1;
190 for my $code (keys %cgi_attrcode_to_attrvalues) {
191 # discard patrons that do not match (case insensitive) at least one of each attribute filter value
192 my $discard = 1;
193 for my $attrval (map { lc $_ } @{ $cgi_attrcode_to_attrvalues{$code} }) {
194 ## if (grep { $attrval eq lc($_->[0]) } @{ $pattrs->{$code} })
195 if (grep { $attrval eq lc($_->[1]) } @{ $pattrs->{$code} }) {
196 $discard = 0;
197 last;
200 if ($discard) {
201 $keep = 0;
202 last;
205 if ($debug) {
206 my $showkeep = $keep ? 'keep' : 'do NOT keep';
207 print STDERR ">>> patron $bn: $showkeep attributes: ";
208 for (sort keys %$pattrs) { my @a=map { "$_->[0]/$_->[1] " } @{$pattrs->{$_}}; print STDERR "attrcode $_ = [@a] " }
209 print STDERR "\n";
211 delete $borrowernumber_to_attributes{$bn} if !$keep;
216 $template->param(
217 patron_attr_header_loop => [ map { { header => $_->{description} } } grep { ! $_->{isclone} } @patron_attr_filter_loop ],
218 branchloop => GetBranchesLoop($branchfilter, $onlymine),
219 homebranchloop => GetBranchesLoop( $homebranchfilter, $onlymine ),
220 holdingbranchloop => GetBranchesLoop( $holdingbranchfilter, $onlymine ),
221 branchfilter => $branchfilter,
222 homebranchfilter => $homebranchfilter,
223 holdingbranchfilter => $homebranchfilter,
224 borcatloop=> \@borcatloop,
225 itemtypeloop => \@itemtypeloop,
226 patron_attr_filter_loop => \@patron_attr_filter_loop,
227 borname => $bornamefilter,
228 order => $order,
229 showall => $showall,
230 dateduefrom => $input->param( 'dateduefrom' ) || '',
231 datedueto => $input->param( 'datedueto' ) || '',
234 if ($noreport) {
235 # la de dah ... page comes up presto-quicko
236 $template->param( noreport => $noreport );
237 } else {
238 # FIXME : the left joins + where clauses make the following SQL query really slow with large datasets :(
240 # FIX 1: use the table with the least rows as first in the join, second least second, etc
241 # ref: http://www.fiftyfoureleven.com/weblog/web-development/programming-and-scripts/mysql-optimization-tip
243 # FIX 2: ensure there are indexes for columns participating in the WHERE clauses, where feasible/reasonable
246 my $today_dt = DateTime->now(time_zone => C4::Context->tz);
247 $today_dt->truncate(to => 'minute');
248 my $todaysdate = $today_dt->strftime('%Y-%m-%d %H:%M');
250 $bornamefilter =~s/\*/\%/g;
251 $bornamefilter =~s/\?/\_/g;
253 my $strsth="SELECT date_due,
254 borrowers.title as borrowertitle,
255 borrowers.surname,
256 borrowers.firstname,
257 borrowers.streetnumber,
258 borrowers.streettype,
259 borrowers.address,
260 borrowers.address2,
261 borrowers.city,
262 borrowers.zipcode,
263 borrowers.country,
264 borrowers.phone,
265 borrowers.email,
266 issues.itemnumber,
267 issues.issuedate,
268 items.barcode,
269 items.homebranch,
270 items.holdingbranch,
271 biblio.title,
272 biblio.author,
273 borrowers.borrowernumber,
274 biblio.biblionumber,
275 borrowers.branchcode,
276 items.itemcallnumber,
277 items.replacementprice,
278 items.enumchron
279 FROM issues
280 LEFT JOIN borrowers ON (issues.borrowernumber=borrowers.borrowernumber )
281 LEFT JOIN items ON (issues.itemnumber=items.itemnumber)
282 LEFT JOIN biblioitems ON (biblioitems.biblioitemnumber=items.biblioitemnumber)
283 LEFT JOIN biblio ON (biblio.biblionumber=items.biblionumber )
284 WHERE 1=1 "; # placeholder, since it is possible that none of the additional
285 # conditions will be selected by user
286 $strsth.=" AND date_due < '" . $todaysdate . "' " unless ($showall);
287 $strsth.=" AND (borrowers.firstname like '".$bornamefilter."%' or borrowers.surname like '".$bornamefilter."%' or borrowers.cardnumber like '".$bornamefilter."%')" if($bornamefilter) ;
288 $strsth.=" AND borrowers.categorycode = '" . $borcatfilter . "' " if $borcatfilter;
289 if( $itemtypefilter ){
290 if( C4::Context->preference('item-level_itypes') ){
291 $strsth.=" AND items.itype = '" . $itemtypefilter . "' ";
292 } else {
293 $strsth.=" AND biblioitems.itemtype = '" . $itemtypefilter . "' ";
296 if ( $borflagsfilter eq 'gonenoaddress' ) {
297 $strsth .= " AND borrowers.gonenoaddress <> 0";
299 elsif ( $borflagsfilter eq 'debarred' ) {
300 $strsth .= " AND borrowers.debarred >= CURDATE()" ;
302 elsif ( $borflagsfilter eq 'lost') {
303 $strsth .= " AND borrowers.lost <> 0";
305 $strsth.=" AND borrowers.branchcode = '" . $branchfilter . "' " if $branchfilter;
306 $strsth.=" AND items.homebranch = '" . $homebranchfilter . "' " if $homebranchfilter;
307 $strsth.=" AND items.holdingbranch = '" . $holdingbranchfilter . "' " if $holdingbranchfilter;
308 $strsth.=" AND date_due < '" . $datedueto . "' " if $datedueto;
309 $strsth.=" AND date_due > '" . $dateduefrom . "' " if $dateduefrom;
310 # restrict patrons (borrowers) to those matching the patron attribute filter(s), if any
311 my $bnlist = $have_pattr_filter_data ? join(',',keys %borrowernumber_to_attributes) : '';
312 $strsth =~ s/WHERE 1=1/WHERE 1=1 AND borrowers.borrowernumber IN ($bnlist)/ if $bnlist;
313 $strsth =~ s/WHERE 1=1/WHERE 0=1/ if $have_pattr_filter_data && !$bnlist; # no match if no borrowers matched patron attrs
314 $strsth.=" ORDER BY " . (
315 ($order eq "borrower") ? "surname, firstname, date_due" :
316 ($order eq "borrower desc") ? "surname desc, firstname desc, date_due" :
317 ($order eq "title" or $order eq "title desc") ? "$order, date_due, surname, firstname" :
318 ($order eq "barcode" or $order eq "barcode desc") ? "items.$order, date_due, surname, firstname" :
319 ($order eq "date_due desc") ? "date_due DESC, surname, firstname" :
320 "date_due, surname, firstname" # default sort order
322 $template->param(sql=>$strsth);
323 my $sth=$dbh->prepare($strsth);
324 #warn "overdue.pl : query string ".$strsth;
325 $sth->execute();
327 my @overduedata;
328 while (my $data = $sth->fetchrow_hashref) {
330 # most of the overdue report data is linked to the database schema, i.e. things like borrowernumber and phone
331 # but the patron attributes (patron_attr_value_loop) are unnormalised and varies dynamically from one db to the next
333 my $pattrs = $borrowernumber_to_attributes{$data->{borrowernumber}} || {}; # patron attrs for this borrower
334 # $pattrs is a hash { attrcode => [ [value,displayvalue], [value,displayvalue]... ] }
336 my @patron_attr_value_loop; # template array [ {value=>v1}, {value=>v2} ... } ]
337 for my $pattr_filter (grep { ! $_->{isclone} } @patron_attr_filter_loop) {
338 my @displayvalues = map { $_->[1] } @{ $pattrs->{$pattr_filter->{code}} }; # grab second value from each subarray
339 push @patron_attr_value_loop, { value => join(', ', sort { lc $a cmp lc $b } @displayvalues) };
341 my $dt = dt_from_string($data->{date_due}, 'sql');
343 push @overduedata, {
344 duedate => output_pref($dt),
345 borrowernumber => $data->{borrowernumber},
346 barcode => $data->{barcode},
347 itemnum => $data->{itemnumber},
348 issuedate => format_date($data->{issuedate}),
349 borrowertitle => $data->{borrowertitle},
350 surname => $data->{surname},
351 firstname => $data->{firstname},
352 streetnumber => $data->{streetnumber},
353 streettype => $data->{streettype},
354 address => $data->{address},
355 address2 => $data->{address2},
356 city => $data->{city},
357 zipcode => $data->{zipcode},
358 country => $data->{country},
359 phone => $data->{phone},
360 email => $data->{email},
361 biblionumber => $data->{biblionumber},
362 title => $data->{title},
363 author => $data->{author},
364 branchcode => $data->{branchcode},
365 homebranchcode => $data->{homebranchcode},
366 holdingbranchcode => $data->{holdingbranchcode},
367 itemcallnumber => $data->{itemcallnumber},
368 replacementprice => $data->{replacementprice},
369 enumchron => $data->{enumchron},
370 patron_attr_value_loop => \@patron_attr_value_loop,
374 my ($attrorder) = $order =~ /patron_attr_(.*)$/;
375 my $patrorder = '';
376 my $sortorder = 'asc';
377 if (defined $attrorder) {
378 ($sortorder, $patrorder) = split /_/, $attrorder, 2;
380 print STDERR ">>> order is $order, patrorder is $patrorder, sortorder is $sortorder\n" if $debug;
382 if (my @attrtype = grep { $_->{'code'} eq $patrorder } @patron_attr_filter_loop) { # sort by patron attrs perhaps?
383 my $ordinal = $attrtype[0]{ordinal};
384 print STDERR ">>> sort ordinal is $ordinal\n" if $debug;
386 sub patronattr_sorter_asc {
387 lc $a->{patron_attr_value_loop}[$ordinal]{value}
389 lc $b->{patron_attr_value_loop}[$ordinal]{value} }
391 sub patronattr_sorter_des { -patronattr_sorter_asc() }
393 my $sorter = $sortorder eq 'desc' ? \&patronattr_sorter_des : \&patronattr_sorter_asc;
394 @overduedata = sort $sorter @overduedata;
397 if ($op eq 'csv') {
398 binmode(STDOUT, ":encoding(UTF-8)");
399 my $csv = build_csv(\@overduedata);
400 print $input->header(-type => 'application/vnd.sun.xml.calc',
401 -encoding => 'utf-8',
402 -attachment=>"overdues.csv",
403 -filename=>"overdues.csv" );
404 print $csv;
405 exit;
408 # generate parameter list for CSV download link
409 my $new_cgi = CGI->new($input);
410 $new_cgi->delete('op');
411 my $csv_param_string = $new_cgi->query_string();
413 $template->param(
414 csv_param_string => $csv_param_string,
415 todaysdate => output_pref($today_dt),
416 overdueloop => \@overduedata,
417 nnoverdue => scalar(@overduedata),
418 noverdue_is_plural => scalar(@overduedata) != 1,
419 noreport => $noreport,
420 isfiltered => $isfiltered,
421 borflag_gonenoaddress => $borflagsfilter eq 'gonenoaddress',
422 borflag_debarred => $borflagsfilter eq 'debarred',
423 borflag_lost => $borflagsfilter eq 'lost',
428 output_html_with_http_headers $input, $cookie, $template->output;
431 sub build_csv {
432 my $overdues = shift;
434 return "" if scalar(@$overdues) == 0;
436 my @lines = ();
438 # build header ...
439 my @keys = qw /duedate title author borrowertitle firstname surname phone barcode email address address2 zipcode city country
440 branchcode itemcallnumber biblionumber borrowernumber itemnum issuedate replacementprice streetnumber streettype/;
441 my $csv = Text::CSV_XS->new();
442 $csv->combine(@keys);
443 push @lines, $csv->string();
445 # ... and rest of report
446 foreach my $overdue ( @{ $overdues } ) {
447 push @lines, $csv->string() if $csv->combine(map { $overdue->{$_} } @keys);
450 return join("\n", @lines) . "\n";