Bug 6448 - carp on invalid barcode
[koha.git] / circ / overdue.pl
blobe44e792f0a8a2af7e8889e613b0f623484b60619
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 $op = $input->param('op') || '';
44 my $dateduefrom = format_date_in_iso($input->param( 'dateduefrom' )) || '';
45 my $datedueto = format_date_in_iso($input->param( 'datedueto' )) || '';
46 # FIXME This is a kludge to include times
47 if ($datedueto) {
48 $datedueto .= ' 23:59';
50 if ($dateduefrom) {
51 $dateduefrom .= ' 00:00';
53 # kludge end
54 my $isfiltered = $op =~ /apply/i && $op =~ /filter/i;
55 my $noreport = C4::Context->preference('FilterBeforeOverdueReport') && ! $isfiltered && $op ne "csv";
57 my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
59 template_name => "circ/overdue.tmpl",
60 query => $input,
61 type => "intranet",
62 authnotrequired => 0,
63 flagsrequired => { reports => 1, circulate => "circulate_remaining_permissions" },
64 debug => 1,
68 my $dbh = C4::Context->dbh;
70 my $req;
71 $req = $dbh->prepare( "select categorycode, description from categories order by description");
72 $req->execute;
73 my @borcatloop;
74 while (my ($catcode, $description) =$req->fetchrow) {
75 push @borcatloop, {
76 value => $catcode,
77 selected => $catcode eq $borcatfilter ? 1 : 0,
78 catname => $description,
82 $req = $dbh->prepare( "select itemtype, description from itemtypes order by description");
83 $req->execute;
84 my @itemtypeloop;
85 while (my ($itemtype, $description) =$req->fetchrow) {
86 push @itemtypeloop, {
87 value => $itemtype,
88 selected => $itemtype eq $itemtypefilter ? 1 : 0,
89 itemtypename => $description,
92 my $onlymine=C4::Context->preference('IndependantBranches') &&
93 C4::Context->userenv &&
94 C4::Context->userenv->{flags} % 2 !=1 &&
95 C4::Context->userenv->{branch};
97 $branchfilter = C4::Context->userenv->{'branch'} if ($onlymine && !$branchfilter);
99 # Filtering by Patron Attributes
100 # @patron_attr_filter_loop is non empty if there are any patron attribute filters
101 # %cgi_attrcode_to_attrvalues contains the patron attribute filter values, as returned by the CGI
102 # %borrowernumber_to_attributes is populated by those borrowernumbers matching the patron attribute filters
104 my %cgi_attrcode_to_attrvalues; # ( patron_attribute_code => [ zero or more attribute filter values from the CGI ] )
105 for my $attrcode (grep { /^patron_attr_filter_/ } $input->param) {
106 if (my @attrvalues = grep { length($_) > 0 } $input->param($attrcode)) {
107 $attrcode =~ s/^patron_attr_filter_//;
108 $cgi_attrcode_to_attrvalues{$attrcode} = \@attrvalues;
109 print STDERR ">>>param($attrcode)[@{[scalar @attrvalues]}] = '@attrvalues'\n" if $debug;
112 my $have_pattr_filter_data = keys(%cgi_attrcode_to_attrvalues) > 0;
114 my @patron_attr_filter_loop; # array of [ domid cgivalue ismany isclone ordinal code description repeatable authorised_value_category ]
115 my @patron_attr_order_loop; # array of { label => $patron_attr_label, value => $patron_attr_order }
117 my @sort_roots = qw(borrower title barcode date_due);
118 push @sort_roots, map {$_ . " desc"} @sort_roots;
119 my @order_loop = ({selected => $order ? 0 : 1}); # initial blank row
120 foreach (@sort_roots) {
121 my $tmpl_name = $_;
122 $tmpl_name =~ s/\s/_/g;
123 push @order_loop, {
124 selected => $order eq $_ ? 1 : 0,
125 ordervalue => $_,
126 'order_' . $tmpl_name => 1,
130 my $sth = $dbh->prepare('SELECT code,description,repeatable,authorised_value_category
131 FROM borrower_attribute_types
132 WHERE staff_searchable <> 0
133 ORDER BY description');
134 $sth->execute();
135 my $ordinal = 0;
136 while (my $row = $sth->fetchrow_hashref) {
137 $row->{ordinal} = $ordinal;
138 my $code = $row->{code};
139 my $cgivalues = $cgi_attrcode_to_attrvalues{$code} || [ '' ];
140 my $isclone = 0;
141 $row->{ismany} = @$cgivalues > 1;
142 my $serial = 0;
143 for (@$cgivalues) {
144 $row->{domid} = $ordinal * 1000 + $serial;
145 $row->{cgivalue} = $_;
146 $row->{isclone} = $isclone;
147 push @patron_attr_filter_loop, { %$row }; # careful: must store a *deep copy* of the modified row
148 } continue { $isclone = 1, ++$serial }
149 foreach my $sortorder ('asc', 'desc') {
150 my $ordervalue = "patron_attr_${sortorder}_${code}";
151 push @order_loop, {
152 selected => $order eq $ordervalue ? 1 : 0,
153 ordervalue => $ordervalue,
154 label => $row->{description},
155 $sortorder => 1,
158 } continue { ++$ordinal }
159 for (@patron_attr_order_loop) { $_->{selected} = 1 if $order eq $_->{value} }
161 $template->param(ORDER_LOOP => \@order_loop);
163 my %borrowernumber_to_attributes; # hash of { borrowernumber => { attrcode => [ [val,display], [val,display], ... ] } }
164 # i.e. val differs from display when attr is an authorised value
165 if (@patron_attr_filter_loop) {
166 # MAYBE FIXME: currently, *all* borrower_attributes are loaded into %borrowernumber_to_attributes
167 # then filtered and honed down to match the patron attribute filters. If this is
168 # too resource intensive, MySQL can be used to do the filtering, i.e. rewire the
169 # SQL below to select only those attribute values that match the filters.
171 my $sql = q(SELECT borrowernumber AS bn, b.code, attribute AS val, category AS avcategory, lib AS avdescription
172 FROM borrower_attributes b
173 JOIN borrower_attribute_types bt ON (b.code = bt.code)
174 LEFT JOIN authorised_values a ON (a.category = bt.authorised_value_category AND a.authorised_value = b.attribute));
175 my $sth = $dbh->prepare($sql);
176 $sth->execute();
177 while (my $row = $sth->fetchrow_hashref) {
178 my $pattrs = $borrowernumber_to_attributes{$row->{bn}} ||= { };
179 push @{ $pattrs->{$row->{code}} }, [
180 $row->{val},
181 defined $row->{avdescription} ? $row->{avdescription} : $row->{val},
185 for my $bn (keys %borrowernumber_to_attributes) {
186 my $pattrs = $borrowernumber_to_attributes{$bn};
187 my $keep = 1;
188 for my $code (keys %cgi_attrcode_to_attrvalues) {
189 # discard patrons that do not match (case insensitive) at least one of each attribute filter value
190 my $discard = 1;
191 for my $attrval (map { lc $_ } @{ $cgi_attrcode_to_attrvalues{$code} }) {
192 ## if (grep { $attrval eq lc($_->[0]) } @{ $pattrs->{$code} })
193 if (grep { $attrval eq lc($_->[1]) } @{ $pattrs->{$code} }) {
194 $discard = 0;
195 last;
198 if ($discard) {
199 $keep = 0;
200 last;
203 if ($debug) {
204 my $showkeep = $keep ? 'keep' : 'do NOT keep';
205 print STDERR ">>> patron $bn: $showkeep attributes: ";
206 for (sort keys %$pattrs) { my @a=map { "$_->[0]/$_->[1] " } @{$pattrs->{$_}}; print STDERR "attrcode $_ = [@a] " }
207 print STDERR "\n";
209 delete $borrowernumber_to_attributes{$bn} if !$keep;
214 $template->param(
215 patron_attr_header_loop => [ map { { header => $_->{description} } } grep { ! $_->{isclone} } @patron_attr_filter_loop ],
216 branchloop => GetBranchesLoop($branchfilter, $onlymine),
217 branchfilter => $branchfilter,
218 borcatloop=> \@borcatloop,
219 itemtypeloop => \@itemtypeloop,
220 patron_attr_filter_loop => \@patron_attr_filter_loop,
221 borname => $bornamefilter,
222 order => $order,
223 showall => $showall,
224 DHTMLcalendar_dateformat => C4::Dates->DHTMLcalendar(),
225 dateduefrom => $input->param( 'dateduefrom' ) || '',
226 datedueto => $input->param( 'datedueto' ) || '',
229 if ($noreport) {
230 # la de dah ... page comes up presto-quicko
231 $template->param( noreport => $noreport );
232 } else {
233 # FIXME : the left joins + where clauses make the following SQL query really slow with large datasets :(
235 # FIX 1: use the table with the least rows as first in the join, second least second, etc
236 # ref: http://www.fiftyfoureleven.com/weblog/web-development/programming-and-scripts/mysql-optimization-tip
238 # FIX 2: ensure there are indexes for columns participating in the WHERE clauses, where feasible/reasonable
241 my $today_dt = DateTime->now(time_zone => C4::Context->tz);
242 $today_dt->truncate(to => 'minute');
243 my $todaysdate = $today_dt->strftime('%Y-%m-%d %H:%M');
245 $bornamefilter =~s/\*/\%/g;
246 $bornamefilter =~s/\?/\_/g;
248 my $strsth="SELECT date_due,
249 borrowers.title as borrowertitle,
250 borrowers.surname,
251 borrowers.firstname,
252 borrowers.streetnumber,
253 borrowers.streettype,
254 borrowers.address,
255 borrowers.address2,
256 borrowers.city,
257 borrowers.zipcode,
258 borrowers.country,
259 borrowers.phone,
260 borrowers.email,
261 issues.itemnumber,
262 issues.issuedate,
263 items.barcode,
264 biblio.title,
265 biblio.author,
266 borrowers.borrowernumber,
267 biblio.biblionumber,
268 borrowers.branchcode,
269 items.itemcallnumber,
270 items.replacementprice,
271 items.enumchron
272 FROM issues
273 LEFT JOIN borrowers ON (issues.borrowernumber=borrowers.borrowernumber )
274 LEFT JOIN items ON (issues.itemnumber=items.itemnumber)
275 LEFT JOIN biblioitems ON (biblioitems.biblioitemnumber=items.biblioitemnumber)
276 LEFT JOIN biblio ON (biblio.biblionumber=items.biblionumber )
277 WHERE 1=1 "; # placeholder, since it is possible that none of the additional
278 # conditions will be selected by user
279 $strsth.=" AND date_due < '" . $todaysdate . "' " unless ($showall);
280 $strsth.=" AND (borrowers.firstname like '".$bornamefilter."%' or borrowers.surname like '".$bornamefilter."%' or borrowers.cardnumber like '".$bornamefilter."%')" if($bornamefilter) ;
281 $strsth.=" AND borrowers.categorycode = '" . $borcatfilter . "' " if $borcatfilter;
282 if( $itemtypefilter ){
283 if( C4::Context->preference('item-level_itypes') ){
284 $strsth.=" AND items.itype = '" . $itemtypefilter . "' ";
285 } else {
286 $strsth.=" AND biblioitems.itemtype = '" . $itemtypefilter . "' ";
289 if ( $borflagsfilter eq 'gonenoaddress' ) {
290 $strsth .= " AND borrowers.gonenoaddress <> 0";
292 elsif ( $borflagsfilter eq 'debarred' ) {
293 $strsth .= " AND borrowers.debarred >= CURDATE()" ;
295 elsif ( $borflagsfilter eq 'lost') {
296 $strsth .= " AND borrowers.lost <> 0";
298 $strsth.=" AND borrowers.branchcode = '" . $branchfilter . "' " if $branchfilter;
299 $strsth.=" AND date_due < '" . $datedueto . "' " if $datedueto;
300 $strsth.=" AND date_due > '" . $dateduefrom . "' " if $dateduefrom;
301 # restrict patrons (borrowers) to those matching the patron attribute filter(s), if any
302 my $bnlist = $have_pattr_filter_data ? join(',',keys %borrowernumber_to_attributes) : '';
303 $strsth =~ s/WHERE 1=1/WHERE 1=1 AND borrowers.borrowernumber IN ($bnlist)/ if $bnlist;
304 $strsth =~ s/WHERE 1=1/WHERE 0=1/ if $have_pattr_filter_data && !$bnlist; # no match if no borrowers matched patron attrs
305 $strsth.=" ORDER BY " . (
306 ($order eq "borrower") ? "surname, firstname, date_due" :
307 ($order eq "borrower desc") ? "surname desc, firstname desc, date_due" :
308 ($order eq "title" or $order eq "title desc") ? "$order, date_due, surname, firstname" :
309 ($order eq "barcode" or $order eq "barcode desc") ? "items.$order, date_due, surname, firstname" :
310 ($order eq "date_due desc") ? "date_due DESC, surname, firstname" :
311 "date_due, surname, firstname" # default sort order
313 $template->param(sql=>$strsth);
314 my $sth=$dbh->prepare($strsth);
315 #warn "overdue.pl : query string ".$strsth;
316 $sth->execute();
318 my @overduedata;
319 while (my $data = $sth->fetchrow_hashref) {
321 # most of the overdue report data is linked to the database schema, i.e. things like borrowernumber and phone
322 # but the patron attributes (patron_attr_value_loop) are unnormalised and varies dynamically from one db to the next
324 my $pattrs = $borrowernumber_to_attributes{$data->{borrowernumber}} || {}; # patron attrs for this borrower
325 # $pattrs is a hash { attrcode => [ [value,displayvalue], [value,displayvalue]... ] }
327 my @patron_attr_value_loop; # template array [ {value=>v1}, {value=>v2} ... } ]
328 for my $pattr_filter (grep { ! $_->{isclone} } @patron_attr_filter_loop) {
329 my @displayvalues = map { $_->[1] } @{ $pattrs->{$pattr_filter->{code}} }; # grab second value from each subarray
330 push @patron_attr_value_loop, { value => join(', ', sort { lc $a cmp lc $b } @displayvalues) };
332 my $dt = dt_from_string($data->{date_due}, 'sql');
334 push @overduedata, {
335 duedate => output_pref($dt),
336 borrowernumber => $data->{borrowernumber},
337 barcode => $data->{barcode},
338 itemnum => $data->{itemnumber},
339 issuedate => format_date($data->{issuedate}),
340 borrowertitle => $data->{borrowertitle},
341 surname => $data->{surname},
342 firstname => $data->{firstname},
343 streetnumber => $data->{streetnumber},
344 streettype => $data->{streettype},
345 address => $data->{address},
346 address2 => $data->{address2},
347 city => $data->{city},
348 zipcode => $data->{zipcode},
349 country => $data->{country},
350 phone => $data->{phone},
351 email => $data->{email},
352 biblionumber => $data->{biblionumber},
353 title => $data->{title},
354 author => $data->{author},
355 branchcode => $data->{branchcode},
356 itemcallnumber => $data->{itemcallnumber},
357 replacementprice => $data->{replacementprice},
358 enumchron => $data->{enumchron},
359 patron_attr_value_loop => \@patron_attr_value_loop,
363 my ($attrorder) = $order =~ /patron_attr_(.*)$/;
364 my $patrorder = '';
365 my $sortorder = 'asc';
366 if (defined $attrorder) {
367 ($sortorder, $patrorder) = split /_/, $attrorder, 2;
369 print STDERR ">>> order is $order, patrorder is $patrorder, sortorder is $sortorder\n" if $debug;
371 if (my @attrtype = grep { $_->{'code'} eq $patrorder } @patron_attr_filter_loop) { # sort by patron attrs perhaps?
372 my $ordinal = $attrtype[0]{ordinal};
373 print STDERR ">>> sort ordinal is $ordinal\n" if $debug;
375 sub patronattr_sorter_asc {
376 lc $a->{patron_attr_value_loop}[$ordinal]{value}
378 lc $b->{patron_attr_value_loop}[$ordinal]{value} }
380 sub patronattr_sorter_des { -patronattr_sorter_asc() }
382 my $sorter = $sortorder eq 'desc' ? \&patronattr_sorter_des : \&patronattr_sorter_asc;
383 @overduedata = sort $sorter @overduedata;
386 if ($op eq 'csv') {
387 binmode(STDOUT, ":encoding(UTF-8)");
388 my $csv = build_csv(\@overduedata);
389 print $input->header(-type => 'application/vnd.sun.xml.calc',
390 -encoding => 'utf-8',
391 -attachment=>"overdues.csv",
392 -filename=>"overdues.csv" );
393 print $csv;
394 exit;
397 # generate parameter list for CSV download link
398 my $new_cgi = CGI->new($input);
399 $new_cgi->delete('op');
400 my $csv_param_string = $new_cgi->query_string();
402 $template->param(
403 csv_param_string => $csv_param_string,
404 todaysdate => output_pref($today_dt),
405 overdueloop => \@overduedata,
406 nnoverdue => scalar(@overduedata),
407 noverdue_is_plural => scalar(@overduedata) != 1,
408 noreport => $noreport,
409 isfiltered => $isfiltered,
410 borflag_gonenoaddress => $borflagsfilter eq 'gonenoaddress',
411 borflag_debarred => $borflagsfilter eq 'debarred',
412 borflag_lost => $borflagsfilter eq 'lost',
417 output_html_with_http_headers $input, $cookie, $template->output;
420 sub build_csv {
421 my $overdues = shift;
423 return "" if scalar(@$overdues) == 0;
425 my @lines = ();
427 # build header ...
428 my @keys = qw /duedate title author borrowertitle firstname surname phone barcode email address address2 zipcode city country
429 branchcode itemcallnumber biblionumber borrowernumber itemnum issuedate replacementprice streetnumber streettype/;
430 my $csv = Text::CSV_XS->new();
431 $csv->combine(@keys);
432 push @lines, $csv->string();
434 # ... and rest of report
435 foreach my $overdue ( @{ $overdues } ) {
436 push @lines, $csv->string() if $csv->combine(map { $overdue->{$_} } @keys);
439 return join("\n", @lines) . "\n";