Bug 18837: (follow-up) Add additional tests
[koha.git] / circ / overdue.pl
blob0b6f0f802a5f3b70e792e15760002a1d4249801e
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
10 # under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
14 # Koha is distributed in the hope that it will be useful, but
15 # WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use Modern::Perl;
23 use C4::Context;
24 use C4::Output;
25 use CGI qw(-oldstyle_urls -utf8);
26 use C4::Auth;
27 use C4::Debug;
28 use Text::CSV_XS;
29 use Koha::DateUtils;
30 use DateTime;
31 use DateTime::Format::MySQL;
33 my $input = new CGI;
34 my $showall = $input->param('showall');
35 my $bornamefilter = $input->param('borname') || '';
36 my $borcatfilter = $input->param('borcat') || '';
37 my $itemtypefilter = $input->param('itemtype') || '';
38 my $borflagsfilter = $input->param('borflag') || '';
39 my $branchfilter = $input->param('branch') || '';
40 my $homebranchfilter = $input->param('homebranch') || '';
41 my $holdingbranchfilter = $input->param('holdingbranch') || '';
42 my $op = $input->param('op') || '';
44 my ($dateduefrom, $datedueto);
45 if ( $dateduefrom = $input->param('dateduefrom') ) {
46 $dateduefrom = dt_from_string( $dateduefrom );
48 if ( $datedueto = $input->param('datedueto') ) {
49 $datedueto = dt_from_string( $datedueto )->set_hour(23)->set_minute(59);
52 my $isfiltered = $op =~ /apply/i && $op =~ /filter/i;
53 my $noreport = C4::Context->preference('FilterBeforeOverdueReport') && ! $isfiltered && $op ne "csv";
55 my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
57 template_name => "circ/overdue.tt",
58 query => $input,
59 type => "intranet",
60 authnotrequired => 0,
61 flagsrequired => { circulate => "overdues_report" },
62 debug => 1,
66 our $logged_in_user = Koha::Patrons->find( $loggedinuser ) or die "Not logged in";
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,
93 # Filtering by Patron Attributes
94 # @patron_attr_filter_loop is non empty if there are any patron attribute filters
95 # %cgi_attrcode_to_attrvalues contains the patron attribute filter values, as returned by the CGI
96 # %borrowernumber_to_attributes is populated by those borrowernumbers matching the patron attribute filters
98 my %cgi_attrcode_to_attrvalues; # ( patron_attribute_code => [ zero or more attribute filter values from the CGI ] )
99 for my $attrcode (grep { /^patron_attr_filter_/ } $input->multi_param) {
100 if (my @attrvalues = grep { length($_) > 0 } $input->multi_param($attrcode)) {
101 $attrcode =~ s/^patron_attr_filter_//;
102 $cgi_attrcode_to_attrvalues{$attrcode} = \@attrvalues;
103 print STDERR ">>>param($attrcode)[@{[scalar @attrvalues]}] = '@attrvalues'\n" if $debug;
106 my $have_pattr_filter_data = keys(%cgi_attrcode_to_attrvalues) > 0;
108 my @patron_attr_filter_loop; # array of [ domid cgivalue ismany isclone ordinal code description repeatable authorised_value_category ]
110 my $sth = $dbh->prepare('SELECT code,description,repeatable,authorised_value_category
111 FROM borrower_attribute_types
112 WHERE staff_searchable <> 0
113 ORDER BY description');
114 $sth->execute();
115 my $ordinal = 0;
116 while (my $row = $sth->fetchrow_hashref) {
117 $row->{ordinal} = $ordinal;
118 my $code = $row->{code};
119 my $cgivalues = $cgi_attrcode_to_attrvalues{$code} || [ '' ];
120 my $isclone = 0;
121 $row->{ismany} = @$cgivalues > 1;
122 my $serial = 0;
123 for (@$cgivalues) {
124 $row->{domid} = $ordinal * 1000 + $serial;
125 $row->{cgivalue} = $_;
126 $row->{isclone} = $isclone;
127 push @patron_attr_filter_loop, { %$row }; # careful: must store a *deep copy* of the modified row
128 } continue { $isclone = 1, ++$serial }
129 } continue { ++$ordinal }
131 my %borrowernumber_to_attributes; # hash of { borrowernumber => { attrcode => [ [val,display], [val,display], ... ] } }
132 # i.e. val differs from display when attr is an authorised value
133 if (@patron_attr_filter_loop) {
134 # MAYBE FIXME: currently, *all* borrower_attributes are loaded into %borrowernumber_to_attributes
135 # then filtered and honed down to match the patron attribute filters. If this is
136 # too resource intensive, MySQL can be used to do the filtering, i.e. rewire the
137 # SQL below to select only those attribute values that match the filters.
139 my $sql = q(SELECT borrowernumber AS bn, b.code, attribute AS val, category AS avcategory, lib AS avdescription
140 FROM borrower_attributes b
141 JOIN borrower_attribute_types bt ON (b.code = bt.code)
142 LEFT JOIN authorised_values a ON (a.category = bt.authorised_value_category AND a.authorised_value = b.attribute));
143 my $sth = $dbh->prepare($sql);
144 $sth->execute();
145 while (my $row = $sth->fetchrow_hashref) {
146 my $pattrs = $borrowernumber_to_attributes{$row->{bn}} ||= { };
147 push @{ $pattrs->{$row->{code}} }, [
148 $row->{val},
149 defined $row->{avdescription} ? $row->{avdescription} : $row->{val},
153 for my $bn (keys %borrowernumber_to_attributes) {
154 my $pattrs = $borrowernumber_to_attributes{$bn};
155 my $keep = 1;
156 for my $code (keys %cgi_attrcode_to_attrvalues) {
157 # discard patrons that do not match (case insensitive) at least one of each attribute filter value
158 my $discard = 1;
159 for my $attrval (map { lc $_ } @{ $cgi_attrcode_to_attrvalues{$code} }) {
160 ## if (grep { $attrval eq lc($_->[0]) } @{ $pattrs->{$code} })
161 if (grep { $attrval eq lc($_->[1]) } @{ $pattrs->{$code} }) {
162 $discard = 0;
163 last;
166 if ($discard) {
167 $keep = 0;
168 last;
171 if ($debug) {
172 my $showkeep = $keep ? 'keep' : 'do NOT keep';
173 print STDERR ">>> patron $bn: $showkeep attributes: ";
174 for (sort keys %$pattrs) { my @a=map { "$_->[0]/$_->[1] " } @{$pattrs->{$_}}; print STDERR "attrcode $_ = [@a] " }
175 print STDERR "\n";
177 delete $borrowernumber_to_attributes{$bn} if !$keep;
182 $template->param(
183 patron_attr_header_loop => [ map { { header => $_->{description} } } grep { ! $_->{isclone} } @patron_attr_filter_loop ],
184 branchfilter => $branchfilter,
185 homebranchfilter => $homebranchfilter,
186 holdingbranchfilter => $holdingbranchfilter,
187 borcatloop=> \@borcatloop,
188 itemtypeloop => \@itemtypeloop,
189 patron_attr_filter_loop => \@patron_attr_filter_loop,
190 borname => $bornamefilter,
191 showall => $showall,
192 dateduefrom => $dateduefrom,
193 datedueto => $datedueto,
196 if ($noreport) {
197 # la de dah ... page comes up presto-quicko
198 $template->param( noreport => $noreport );
199 } else {
200 # FIXME : the left joins + where clauses make the following SQL query really slow with large datasets :(
202 # FIX 1: use the table with the least rows as first in the join, second least second, etc
203 # ref: http://www.fiftyfoureleven.com/weblog/web-development/programming-and-scripts/mysql-optimization-tip
205 # FIX 2: ensure there are indexes for columns participating in the WHERE clauses, where feasible/reasonable
208 my $today_dt = DateTime->now(time_zone => C4::Context->tz);
209 $today_dt->truncate(to => 'minute');
210 my $todaysdate = $today_dt->strftime('%Y-%m-%d %H:%M');
212 $bornamefilter =~s/\*/\%/g;
213 $bornamefilter =~s/\?/\_/g;
215 my $strsth="SELECT date_due,
216 borrowers.title as borrowertitle,
217 borrowers.surname,
218 borrowers.firstname,
219 borrowers.streetnumber,
220 borrowers.streettype,
221 borrowers.address,
222 borrowers.address2,
223 borrowers.city,
224 borrowers.zipcode,
225 borrowers.country,
226 borrowers.phone,
227 borrowers.email,
228 borrowers.cardnumber,
229 borrowers.borrowernumber,
230 borrowers.branchcode,
231 issues.itemnumber,
232 issues.issuedate,
233 items.barcode,
234 items.homebranch,
235 items.holdingbranch,
236 biblio.title,
237 biblio.author,
238 biblio.biblionumber,
239 items.itemcallnumber,
240 items.replacementprice,
241 items.enumchron,
242 items.itemnotes_nonpublic
243 FROM issues
244 LEFT JOIN borrowers ON (issues.borrowernumber=borrowers.borrowernumber )
245 LEFT JOIN items ON (issues.itemnumber=items.itemnumber)
246 LEFT JOIN biblioitems ON (biblioitems.biblioitemnumber=items.biblioitemnumber)
247 LEFT JOIN biblio ON (biblio.biblionumber=items.biblionumber )
248 WHERE 1=1 "; # placeholder, since it is possible that none of the additional
249 # conditions will be selected by user
250 $strsth.=" AND date_due < '" . $todaysdate . "' " unless ($showall);
251 $strsth.=" AND (borrowers.firstname like '".$bornamefilter."%' or borrowers.surname like '".$bornamefilter."%' or borrowers.cardnumber like '".$bornamefilter."%')" if($bornamefilter) ;
252 $strsth.=" AND borrowers.categorycode = '" . $borcatfilter . "' " if $borcatfilter;
253 if( $itemtypefilter ){
254 if( C4::Context->preference('item-level_itypes') ){
255 $strsth.=" AND items.itype = '" . $itemtypefilter . "' ";
256 } else {
257 $strsth.=" AND biblioitems.itemtype = '" . $itemtypefilter . "' ";
260 if ( $borflagsfilter eq 'gonenoaddress' ) {
261 $strsth .= " AND borrowers.gonenoaddress <> 0";
263 elsif ( $borflagsfilter eq 'debarred' ) {
264 $strsth .= " AND borrowers.debarred >= CURDATE()" ;
266 elsif ( $borflagsfilter eq 'lost') {
267 $strsth .= " AND borrowers.lost <> 0";
269 $strsth.=" AND borrowers.branchcode = '" . $branchfilter . "' " if $branchfilter;
270 $strsth.=" AND items.homebranch = '" . $homebranchfilter . "' " if $homebranchfilter;
271 $strsth.=" AND items.holdingbranch = '" . $holdingbranchfilter . "' " if $holdingbranchfilter;
272 $strsth.=" AND date_due >= ?" if $dateduefrom;
273 $strsth.=" AND date_due <= ?" if $datedueto;
274 # restrict patrons (borrowers) to those matching the patron attribute filter(s), if any
275 my $bnlist = $have_pattr_filter_data ? join(',',keys %borrowernumber_to_attributes) : '';
276 $strsth =~ s/WHERE 1=1/WHERE 1=1 AND borrowers.borrowernumber IN ($bnlist)/ if $bnlist;
277 $strsth =~ s/WHERE 1=1/WHERE 0=1/ if $have_pattr_filter_data && !$bnlist; # no match if no borrowers matched patron attrs
278 $strsth.=" ORDER BY date_due, surname, firstname";
279 $template->param(sql=>$strsth);
280 my $sth=$dbh->prepare($strsth);
281 $sth->execute(
282 ($dateduefrom ? DateTime::Format::MySQL->format_datetime($dateduefrom) : ()),
283 ($datedueto ? DateTime::Format::MySQL->format_datetime($datedueto) : ()),
286 my @overduedata;
287 while (my $data = $sth->fetchrow_hashref) {
289 # most of the overdue report data is linked to the database schema, i.e. things like borrowernumber and phone
290 # but the patron attributes (patron_attr_value_loop) are unnormalised and varies dynamically from one db to the next
292 my $pattrs = $borrowernumber_to_attributes{$data->{borrowernumber}} || {}; # patron attrs for this borrower
293 # $pattrs is a hash { attrcode => [ [value,displayvalue], [value,displayvalue]... ] }
295 my @patron_attr_value_loop; # template array [ {value=>v1}, {value=>v2} ... } ]
296 for my $pattr_filter (grep { ! $_->{isclone} } @patron_attr_filter_loop) {
297 my @displayvalues = map { $_->[1] } @{ $pattrs->{$pattr_filter->{code}} }; # grab second value from each subarray
298 push @patron_attr_value_loop, { value => join(', ', sort { lc $a cmp lc $b } @displayvalues) };
301 push @overduedata, {
302 patron => scalar Koha::Patrons->find( $data->{borrowernumber} ),
303 duedate => $data->{date_due},
304 borrowernumber => $data->{borrowernumber},
305 cardnumber => $data->{cardnumber},
306 borrowertitle => $data->{borrowertitle},
307 surname => $data->{surname},
308 firstname => $data->{firstname},
309 streetnumber => $data->{streetnumber},
310 streettype => $data->{streettype},
311 address => $data->{address},
312 address2 => $data->{address2},
313 city => $data->{city},
314 zipcode => $data->{zipcode},
315 country => $data->{country},
316 phone => $data->{phone},
317 email => $data->{email},
318 branchcode => $data->{branchcode},
319 barcode => $data->{barcode},
320 itemnum => $data->{itemnumber},
321 issuedate => output_pref({ dt => dt_from_string( $data->{issuedate} ), dateonly => 1 }),
322 biblionumber => $data->{biblionumber},
323 title => $data->{title},
324 author => $data->{author},
325 homebranchcode => $data->{homebranchcode},
326 holdingbranchcode => $data->{holdingbranchcode},
327 itemcallnumber => $data->{itemcallnumber},
328 replacementprice => $data->{replacementprice},
329 itemnotes_nonpublic => $data->{itemnotes_nonpublic},
330 enumchron => $data->{enumchron},
331 patron_attr_value_loop => \@patron_attr_value_loop,
335 if ($op eq 'csv') {
336 binmode(STDOUT, ":encoding(UTF-8)");
337 my $csv = build_csv(\@overduedata);
338 print $input->header(-type => 'application/vnd.sun.xml.calc',
339 -encoding => 'utf-8',
340 -attachment=>"overdues.csv",
341 -filename=>"overdues.csv" );
342 print $csv;
343 exit;
346 # generate parameter list for CSV download link
347 my $new_cgi = CGI->new($input);
348 $new_cgi->delete('op');
349 my $csv_param_string = $new_cgi->query_string();
351 $template->param(
352 csv_param_string => $csv_param_string,
353 todaysdate => output_pref($today_dt),
354 overdueloop => \@overduedata,
355 nnoverdue => scalar(@overduedata),
356 noverdue_is_plural => scalar(@overduedata) != 1,
357 noreport => $noreport,
358 isfiltered => $isfiltered,
359 borflag_gonenoaddress => $borflagsfilter eq 'gonenoaddress',
360 borflag_debarred => $borflagsfilter eq 'debarred',
361 borflag_lost => $borflagsfilter eq 'lost',
366 output_html_with_http_headers $input, $cookie, $template->output;
369 sub build_csv {
370 my $overdues = shift;
372 return "" if scalar(@$overdues) == 0;
374 my @lines = ();
376 # build header ...
377 my @keys =
378 qw ( duedate title author borrowertitle firstname surname phone barcode email address address2 zipcode city country
379 branchcode itemcallnumber biblionumber borrowernumber itemnum issuedate replacementprice itemnotes_nonpublic streetnumber streettype);
380 my $csv = Text::CSV_XS->new();
381 $csv->combine(@keys);
382 push @lines, $csv->string();
384 my @private_keys = qw( borrowertitle firstname surname phone email address address2 zipcode city country streetnumber streettype );
385 # ... and rest of report
386 foreach my $overdue ( @{ $overdues } ) {
387 unless ( $logged_in_user->can_see_patron_infos( $overdue->{patron} ) ) {
388 $overdue->{$_} = undef for @private_keys;
390 push @lines, $csv->string() if $csv->combine(map { $overdue->{$_} } @keys);
393 return join("\n", @lines) . "\n";