Bug 6048: printoverdues.sh bash odities
[koha.git] / circ / overdue.pl
blob2bdafe1a0bb969fd2e80ae86d12fc02ec08bb2ef
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 Date::Calc qw/Today/;
32 use Text::CSV_XS;
34 my $input = new CGI;
35 my $order = $input->param('order') || '';
36 my $showall = $input->param('showall');
37 my $bornamefilter = $input->param('borname') || '';
38 my $borcatfilter = $input->param('borcat') || '';
39 my $itemtypefilter = $input->param('itemtype') || '';
40 my $borflagsfilter = $input->param('borflag') || '';
41 my $branchfilter = $input->param('branch') || '';
42 my $op = $input->param('op') || '';
43 my $dateduefrom = format_date_in_iso($input->param( 'dateduefrom' )) || '';
44 my $datedueto = format_date_in_iso($input->param( 'datedueto' )) || '';
45 my $isfiltered = $op =~ /apply/i && $op =~ /filter/i;
46 my $noreport = C4::Context->preference('FilterBeforeOverdueReport') && ! $isfiltered && $op ne "csv";
48 my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
50 template_name => "circ/overdue.tmpl",
51 query => $input,
52 type => "intranet",
53 authnotrequired => 0,
54 flagsrequired => { reports => 1, circulate => "circulate_remaining_permissions" },
55 debug => 1,
59 my $dbh = C4::Context->dbh;
61 my $req;
62 $req = $dbh->prepare( "select categorycode, description from categories order by description");
63 $req->execute;
64 my @borcatloop;
65 while (my ($catcode, $description) =$req->fetchrow) {
66 push @borcatloop, {
67 value => $catcode,
68 selected => $catcode eq $borcatfilter ? 1 : 0,
69 catname => $description,
73 $req = $dbh->prepare( "select itemtype, description from itemtypes order by description");
74 $req->execute;
75 my @itemtypeloop;
76 while (my ($itemtype, $description) =$req->fetchrow) {
77 push @itemtypeloop, {
78 value => $itemtype,
79 selected => $itemtype eq $itemtypefilter ? 1 : 0,
80 itemtypename => $description,
83 my $onlymine=C4::Context->preference('IndependantBranches') &&
84 C4::Context->userenv &&
85 C4::Context->userenv->{flags} % 2 !=1 &&
86 C4::Context->userenv->{branch};
88 $branchfilter = C4::Context->userenv->{'branch'} if ($onlymine && !$branchfilter);
90 # Filtering by Patron Attributes
91 # @patron_attr_filter_loop is non empty if there are any patron attribute filters
92 # %cgi_attrcode_to_attrvalues contains the patron attribute filter values, as returned by the CGI
93 # %borrowernumber_to_attributes is populated by those borrowernumbers matching the patron attribute filters
95 my %cgi_attrcode_to_attrvalues; # ( patron_attribute_code => [ zero or more attribute filter values from the CGI ] )
96 for my $attrcode (grep { /^patron_attr_filter_/ } $input->param) {
97 if (my @attrvalues = grep { length($_) > 0 } $input->param($attrcode)) {
98 $attrcode =~ s/^patron_attr_filter_//;
99 $cgi_attrcode_to_attrvalues{$attrcode} = \@attrvalues;
100 print STDERR ">>>param($attrcode)[@{[scalar @attrvalues]}] = '@attrvalues'\n" if $debug;
103 my $have_pattr_filter_data = keys(%cgi_attrcode_to_attrvalues) > 0;
105 my @patron_attr_filter_loop; # array of [ domid cgivalue ismany isclone ordinal code description repeatable authorised_value_category ]
106 my @patron_attr_order_loop; # array of { label => $patron_attr_label, value => $patron_attr_order }
108 my @sort_roots = qw(borrower title barcode date_due);
109 push @sort_roots, map {$_ . " desc"} @sort_roots;
110 my @order_loop = ({selected => $order ? 0 : 1}); # initial blank row
111 foreach (@sort_roots) {
112 my $tmpl_name = $_;
113 $tmpl_name =~ s/\s/_/g;
114 push @order_loop, {
115 selected => $order eq $_ ? 1 : 0,
116 ordervalue => $_,
117 'order_' . $tmpl_name => 1,
121 my $sth = $dbh->prepare('SELECT code,description,repeatable,authorised_value_category
122 FROM borrower_attribute_types
123 WHERE staff_searchable <> 0
124 ORDER BY description');
125 $sth->execute();
126 my $ordinal = 0;
127 while (my $row = $sth->fetchrow_hashref) {
128 $row->{ordinal} = $ordinal;
129 my $code = $row->{code};
130 my $cgivalues = $cgi_attrcode_to_attrvalues{$code} || [ '' ];
131 my $isclone = 0;
132 $row->{ismany} = @$cgivalues > 1;
133 my $serial = 0;
134 for (@$cgivalues) {
135 $row->{domid} = $ordinal * 1000 + $serial;
136 $row->{cgivalue} = $_;
137 $row->{isclone} = $isclone;
138 push @patron_attr_filter_loop, { %$row }; # careful: must store a *deep copy* of the modified row
139 } continue { $isclone = 1, ++$serial }
140 foreach my $sortorder ('asc', 'desc') {
141 my $ordervalue = "patron_attr_${sortorder}_${code}";
142 push @order_loop, {
143 selected => $order eq $ordervalue ? 1 : 0,
144 ordervalue => $ordervalue,
145 label => $row->{description},
146 $sortorder => 1,
149 } continue { ++$ordinal }
150 for (@patron_attr_order_loop) { $_->{selected} = 1 if $order eq $_->{value} }
152 $template->param(ORDER_LOOP => \@order_loop);
154 my %borrowernumber_to_attributes; # hash of { borrowernumber => { attrcode => [ [val,display], [val,display], ... ] } }
155 # i.e. val differs from display when attr is an authorised value
156 if (@patron_attr_filter_loop) {
157 # MAYBE FIXME: currently, *all* borrower_attributes are loaded into %borrowernumber_to_attributes
158 # then filtered and honed down to match the patron attribute filters. If this is
159 # too resource intensive, MySQL can be used to do the filtering, i.e. rewire the
160 # SQL below to select only those attribute values that match the filters.
162 my $sql = q(SELECT borrowernumber AS bn, b.code, attribute AS val, category AS avcategory, lib AS avdescription
163 FROM borrower_attributes b
164 JOIN borrower_attribute_types bt ON (b.code = bt.code)
165 LEFT JOIN authorised_values a ON (a.category = bt.authorised_value_category AND a.authorised_value = b.attribute));
166 my $sth = $dbh->prepare($sql);
167 $sth->execute();
168 while (my $row = $sth->fetchrow_hashref) {
169 my $pattrs = $borrowernumber_to_attributes{$row->{bn}} ||= { };
170 push @{ $pattrs->{$row->{code}} }, [
171 $row->{val},
172 defined $row->{avdescription} ? $row->{avdescription} : $row->{val},
176 for my $bn (keys %borrowernumber_to_attributes) {
177 my $pattrs = $borrowernumber_to_attributes{$bn};
178 my $keep = 1;
179 for my $code (keys %cgi_attrcode_to_attrvalues) {
180 # discard patrons that do not match (case insensitive) at least one of each attribute filter value
181 my $discard = 1;
182 for my $attrval (map { lc $_ } @{ $cgi_attrcode_to_attrvalues{$code} }) {
183 ## if (grep { $attrval eq lc($_->[0]) } @{ $pattrs->{$code} })
184 if (grep { $attrval eq lc($_->[1]) } @{ $pattrs->{$code} }) {
185 $discard = 0;
186 last;
189 if ($discard) {
190 $keep = 0;
191 last;
194 if ($debug) {
195 my $showkeep = $keep ? 'keep' : 'do NOT keep';
196 print STDERR ">>> patron $bn: $showkeep attributes: ";
197 for (sort keys %$pattrs) { my @a=map { "$_->[0]/$_->[1] " } @{$pattrs->{$_}}; print STDERR "attrcode $_ = [@a] " }
198 print STDERR "\n";
200 delete $borrowernumber_to_attributes{$bn} if !$keep;
205 $template->param(
206 patron_attr_header_loop => [ map { { header => $_->{description} } } grep { ! $_->{isclone} } @patron_attr_filter_loop ],
207 branchloop => GetBranchesLoop($branchfilter, $onlymine),
208 branchfilter => $branchfilter,
209 borcatloop=> \@borcatloop,
210 itemtypeloop => \@itemtypeloop,
211 patron_attr_filter_loop => \@patron_attr_filter_loop,
212 borname => $bornamefilter,
213 order => $order,
214 showall => $showall,
215 DHTMLcalendar_dateformat => C4::Dates->DHTMLcalendar(),
216 dateduefrom => $input->param( 'dateduefrom' ) || '',
217 datedueto => $input->param( 'datedueto' ) || '',
220 if ($noreport) {
221 # la de dah ... page comes up presto-quicko
222 $template->param( noreport => $noreport );
223 } else {
224 # FIXME : the left joins + where clauses make the following SQL query really slow with large datasets :(
226 # FIX 1: use the table with the least rows as first in the join, second least second, etc
227 # ref: http://www.fiftyfoureleven.com/weblog/web-development/programming-and-scripts/mysql-optimization-tip
229 # FIX 2: ensure there are indexes for columns participating in the WHERE clauses, where feasible/reasonable
232 my $todaysdate = sprintf("%-04.4d-%-02.2d-%02.2d", Today());
234 $bornamefilter =~s/\*/\%/g;
235 $bornamefilter =~s/\?/\_/g;
237 my $strsth="SELECT date_due,
238 borrowers.title as borrowertitle,
239 concat(surname,' ', firstname) as borrower,
240 borrowers.streetnumber,
241 borrowers.streettype,
242 borrowers.address,
243 borrowers.address2,
244 borrowers.city,
245 borrowers.zipcode,
246 borrowers.country,
247 borrowers.phone,
248 borrowers.email,
249 issues.itemnumber,
250 items.barcode,
251 biblio.title,
252 biblio.author,
253 borrowers.borrowernumber,
254 biblio.biblionumber,
255 borrowers.branchcode,
256 items.itemcallnumber,
257 items.replacementprice
258 FROM issues
259 LEFT JOIN borrowers ON (issues.borrowernumber=borrowers.borrowernumber )
260 LEFT JOIN items ON (issues.itemnumber=items.itemnumber)
261 LEFT JOIN biblioitems ON (biblioitems.biblioitemnumber=items.biblioitemnumber)
262 LEFT JOIN biblio ON (biblio.biblionumber=items.biblionumber )
263 WHERE 1=1 "; # placeholder, since it is possible that none of the additional
264 # conditions will be selected by user
265 $strsth.=" AND date_due < '" . $todaysdate . "' " unless ($showall);
266 $strsth.=" AND (borrowers.firstname like '".$bornamefilter."%' or borrowers.surname like '".$bornamefilter."%' or borrowers.cardnumber like '".$bornamefilter."%')" if($bornamefilter) ;
267 $strsth.=" AND borrowers.categorycode = '" . $borcatfilter . "' " if $borcatfilter;
268 $strsth.=" AND biblioitems.itemtype = '" . $itemtypefilter . "' " if $itemtypefilter;
269 $strsth.=" AND borrowers.flags = '" . $borflagsfilter . "' " if $borflagsfilter;
270 $strsth.=" AND borrowers.branchcode = '" . $branchfilter . "' " if $branchfilter;
271 $strsth.=" AND date_due < '" . $datedueto . "' " if $datedueto;
272 $strsth.=" AND date_due > '" . $dateduefrom . "' " if $dateduefrom;
273 # restrict patrons (borrowers) to those matching the patron attribute filter(s), if any
274 my $bnlist = $have_pattr_filter_data ? join(',',keys %borrowernumber_to_attributes) : '';
275 $strsth =~ s/WHERE 1=1/WHERE 1=1 AND borrowers.borrowernumber IN ($bnlist)/ if $bnlist;
276 $strsth =~ s/WHERE 1=1/WHERE 0=1/ if $have_pattr_filter_data && !$bnlist; # no match if no borrowers matched patron attrs
277 $strsth.=" ORDER BY " . (
278 ($order eq "borrower" or $order eq "borrower desc") ? "$order, date_due" :
279 ($order eq "title" or $order eq "title desc") ? "$order, date_due, borrower" :
280 ($order eq "barcode" or $order eq "barcode desc") ? "items.$order, date_due, borrower" :
281 ($order eq "date_due desc") ? "date_due DESC, borrower" :
282 "date_due, borrower" # default sort order
284 $template->param(sql=>$strsth);
285 my $sth=$dbh->prepare($strsth);
286 #warn "overdue.pl : query string ".$strsth;
287 $sth->execute();
289 my @overduedata;
290 while (my $data = $sth->fetchrow_hashref) {
292 # most of the overdue report data is linked to the database schema, i.e. things like borrowernumber and phone
293 # but the patron attributes (patron_attr_value_loop) are unnormalised and varies dynamically from one db to the next
295 my $pattrs = $borrowernumber_to_attributes{$data->{borrowernumber}} || {}; # patron attrs for this borrower
296 # $pattrs is a hash { attrcode => [ [value,displayvalue], [value,displayvalue]... ] }
298 my @patron_attr_value_loop; # template array [ {value=>v1}, {value=>v2} ... } ]
299 for my $pattr_filter (grep { ! $_->{isclone} } @patron_attr_filter_loop) {
300 my @displayvalues = map { $_->[1] } @{ $pattrs->{$pattr_filter->{code}} }; # grab second value from each subarray
301 push @patron_attr_value_loop, { value => join(', ', sort { lc $a cmp lc $b } @displayvalues) };
304 push @overduedata, {
305 duedate => format_date($data->{date_due}),
306 borrowernumber => $data->{borrowernumber},
307 barcode => $data->{barcode},
308 itemnum => $data->{itemnumber},
309 borrowertitle => $data->{borrowertitle},
310 name => $data->{borrower},
311 streetnumber => $data->{streetnumber},
312 streettype => $data->{streettype},
313 address => $data->{address},
314 address2 => $data->{address2},
315 city => $data->{city},
316 zipcode => $data->{zipcode},
317 country => $data->{country},
318 phone => $data->{phone},
319 email => $data->{email},
320 biblionumber => $data->{biblionumber},
321 title => $data->{title},
322 author => $data->{author},
323 branchcode => $data->{branchcode},
324 itemcallnumber => $data->{itemcallnumber},
325 replacementprice => $data->{replacementprice},
326 patron_attr_value_loop => \@patron_attr_value_loop,
330 my ($attrorder) = $order =~ /patron_attr_(.*)$/;
331 my $patrorder = '';
332 my $sortorder = 'asc';
333 if (defined $attrorder) {
334 ($sortorder, $patrorder) = split /_/, $attrorder, 2;
336 print STDERR ">>> order is $order, patrorder is $patrorder, sortorder is $sortorder\n" if $debug;
338 if (my @attrtype = grep { $_->{'code'} eq $patrorder } @patron_attr_filter_loop) { # sort by patron attrs perhaps?
339 my $ordinal = $attrtype[0]{ordinal};
340 print STDERR ">>> sort ordinal is $ordinal\n" if $debug;
342 sub patronattr_sorter_asc {
343 lc $a->{patron_attr_value_loop}[$ordinal]{value}
345 lc $b->{patron_attr_value_loop}[$ordinal]{value} }
347 sub patronattr_sorter_des { -patronattr_sorter_asc() }
349 my $sorter = $sortorder eq 'desc' ? \&patronattr_sorter_des : \&patronattr_sorter_asc;
350 @overduedata = sort $sorter @overduedata;
353 if ($op eq 'csv') {
354 binmode(STDOUT, ":encoding(UTF-8)");
355 my $csv = build_csv(\@overduedata);
356 print $input->header(-type => 'application/vnd.sun.xml.calc',
357 -encoding => 'utf-8',
358 -attachment=>"overdues.csv",
359 -filename=>"overdues.csv" );
360 print $csv;
361 exit;
364 # generate parameter list for CSV download link
365 my $new_cgi = CGI->new($input);
366 $new_cgi->delete('op');
367 my $csv_param_string = $new_cgi->query_string();
369 $template->param(
370 csv_param_string => $csv_param_string,
371 todaysdate => format_date($todaysdate),
372 overdueloop => \@overduedata,
373 nnoverdue => scalar(@overduedata),
374 noverdue_is_plural => scalar(@overduedata) != 1,
375 noreport => $noreport,
376 isfiltered => $isfiltered,
377 borflag_gonenoaddress => $borflagsfilter eq 'gonenoaddress',
378 borflag_debarred => $borflagsfilter eq 'debarred',
379 borflag_lost => $borflagsfilter eq 'lost',
384 output_html_with_http_headers $input, $cookie, $template->output;
387 sub build_csv {
388 my $overdues = shift;
390 return "" if scalar(@$overdues) == 0;
392 my @lines = ();
394 # build header ...
395 my @keys = grep { $_ ne 'patron_attr_value_loop' } sort keys %{ $overdues->[0] };
396 my $csv = Text::CSV_XS->new();
397 $csv->combine(@keys);
398 push @lines, $csv->string();
400 # ... and rest of report
401 foreach my $overdue ( @{ $overdues } ) {
402 push @lines, $csv->string() if $csv->combine(map { $overdue->{$_} } @keys);
405 return join("\n", @lines) . "\n";