Bug 18356: Extend GetNextDate.t, add GetFictiveIssueNumber.t (unit=year)
[koha.git] / circ / overdue.pl
blob68abc8452711b7df9dd105d7c321f403c07d9074
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 my $dbh = C4::Context->dbh;
68 my $req;
69 $req = $dbh->prepare( "select categorycode, description from categories order by description");
70 $req->execute;
71 my @borcatloop;
72 while (my ($catcode, $description) =$req->fetchrow) {
73 push @borcatloop, {
74 value => $catcode,
75 selected => $catcode eq $borcatfilter ? 1 : 0,
76 catname => $description,
80 $req = $dbh->prepare( "select itemtype, description from itemtypes order by description");
81 $req->execute;
82 my @itemtypeloop;
83 while (my ($itemtype, $description) =$req->fetchrow) {
84 push @itemtypeloop, {
85 value => $itemtype,
86 selected => $itemtype eq $itemtypefilter ? 1 : 0,
87 itemtypename => $description,
91 # Filtering by Patron Attributes
92 # @patron_attr_filter_loop is non empty if there are any patron attribute filters
93 # %cgi_attrcode_to_attrvalues contains the patron attribute filter values, as returned by the CGI
94 # %borrowernumber_to_attributes is populated by those borrowernumbers matching the patron attribute filters
96 my %cgi_attrcode_to_attrvalues; # ( patron_attribute_code => [ zero or more attribute filter values from the CGI ] )
97 for my $attrcode (grep { /^patron_attr_filter_/ } $input->multi_param) {
98 if (my @attrvalues = grep { length($_) > 0 } $input->multi_param($attrcode)) {
99 $attrcode =~ s/^patron_attr_filter_//;
100 $cgi_attrcode_to_attrvalues{$attrcode} = \@attrvalues;
101 print STDERR ">>>param($attrcode)[@{[scalar @attrvalues]}] = '@attrvalues'\n" if $debug;
104 my $have_pattr_filter_data = keys(%cgi_attrcode_to_attrvalues) > 0;
106 my @patron_attr_filter_loop; # array of [ domid cgivalue ismany isclone ordinal code description repeatable authorised_value_category ]
108 my $sth = $dbh->prepare('SELECT code,description,repeatable,authorised_value_category
109 FROM borrower_attribute_types
110 WHERE staff_searchable <> 0
111 ORDER BY description');
112 $sth->execute();
113 my $ordinal = 0;
114 while (my $row = $sth->fetchrow_hashref) {
115 $row->{ordinal} = $ordinal;
116 my $code = $row->{code};
117 my $cgivalues = $cgi_attrcode_to_attrvalues{$code} || [ '' ];
118 my $isclone = 0;
119 $row->{ismany} = @$cgivalues > 1;
120 my $serial = 0;
121 for (@$cgivalues) {
122 $row->{domid} = $ordinal * 1000 + $serial;
123 $row->{cgivalue} = $_;
124 $row->{isclone} = $isclone;
125 push @patron_attr_filter_loop, { %$row }; # careful: must store a *deep copy* of the modified row
126 } continue { $isclone = 1, ++$serial }
127 } continue { ++$ordinal }
129 my %borrowernumber_to_attributes; # hash of { borrowernumber => { attrcode => [ [val,display], [val,display], ... ] } }
130 # i.e. val differs from display when attr is an authorised value
131 if (@patron_attr_filter_loop) {
132 # MAYBE FIXME: currently, *all* borrower_attributes are loaded into %borrowernumber_to_attributes
133 # then filtered and honed down to match the patron attribute filters. If this is
134 # too resource intensive, MySQL can be used to do the filtering, i.e. rewire the
135 # SQL below to select only those attribute values that match the filters.
137 my $sql = q(SELECT borrowernumber AS bn, b.code, attribute AS val, category AS avcategory, lib AS avdescription
138 FROM borrower_attributes b
139 JOIN borrower_attribute_types bt ON (b.code = bt.code)
140 LEFT JOIN authorised_values a ON (a.category = bt.authorised_value_category AND a.authorised_value = b.attribute));
141 my $sth = $dbh->prepare($sql);
142 $sth->execute();
143 while (my $row = $sth->fetchrow_hashref) {
144 my $pattrs = $borrowernumber_to_attributes{$row->{bn}} ||= { };
145 push @{ $pattrs->{$row->{code}} }, [
146 $row->{val},
147 defined $row->{avdescription} ? $row->{avdescription} : $row->{val},
151 for my $bn (keys %borrowernumber_to_attributes) {
152 my $pattrs = $borrowernumber_to_attributes{$bn};
153 my $keep = 1;
154 for my $code (keys %cgi_attrcode_to_attrvalues) {
155 # discard patrons that do not match (case insensitive) at least one of each attribute filter value
156 my $discard = 1;
157 for my $attrval (map { lc $_ } @{ $cgi_attrcode_to_attrvalues{$code} }) {
158 ## if (grep { $attrval eq lc($_->[0]) } @{ $pattrs->{$code} })
159 if (grep { $attrval eq lc($_->[1]) } @{ $pattrs->{$code} }) {
160 $discard = 0;
161 last;
164 if ($discard) {
165 $keep = 0;
166 last;
169 if ($debug) {
170 my $showkeep = $keep ? 'keep' : 'do NOT keep';
171 print STDERR ">>> patron $bn: $showkeep attributes: ";
172 for (sort keys %$pattrs) { my @a=map { "$_->[0]/$_->[1] " } @{$pattrs->{$_}}; print STDERR "attrcode $_ = [@a] " }
173 print STDERR "\n";
175 delete $borrowernumber_to_attributes{$bn} if !$keep;
180 $template->param(
181 patron_attr_header_loop => [ map { { header => $_->{description} } } grep { ! $_->{isclone} } @patron_attr_filter_loop ],
182 branchfilter => $branchfilter,
183 homebranchfilter => $homebranchfilter,
184 holdingbranchfilter => $homebranchfilter,
185 borcatloop=> \@borcatloop,
186 itemtypeloop => \@itemtypeloop,
187 patron_attr_filter_loop => \@patron_attr_filter_loop,
188 borname => $bornamefilter,
189 showall => $showall,
190 dateduefrom => $dateduefrom,
191 datedueto => $datedueto,
194 if ($noreport) {
195 # la de dah ... page comes up presto-quicko
196 $template->param( noreport => $noreport );
197 } else {
198 # FIXME : the left joins + where clauses make the following SQL query really slow with large datasets :(
200 # FIX 1: use the table with the least rows as first in the join, second least second, etc
201 # ref: http://www.fiftyfoureleven.com/weblog/web-development/programming-and-scripts/mysql-optimization-tip
203 # FIX 2: ensure there are indexes for columns participating in the WHERE clauses, where feasible/reasonable
206 my $today_dt = DateTime->now(time_zone => C4::Context->tz);
207 $today_dt->truncate(to => 'minute');
208 my $todaysdate = $today_dt->strftime('%Y-%m-%d %H:%M');
210 $bornamefilter =~s/\*/\%/g;
211 $bornamefilter =~s/\?/\_/g;
213 my $strsth="SELECT date_due,
214 borrowers.title as borrowertitle,
215 borrowers.surname,
216 borrowers.firstname,
217 borrowers.streetnumber,
218 borrowers.streettype,
219 borrowers.address,
220 borrowers.address2,
221 borrowers.city,
222 borrowers.zipcode,
223 borrowers.country,
224 borrowers.phone,
225 borrowers.email,
226 borrowers.cardnumber,
227 issues.itemnumber,
228 issues.issuedate,
229 items.barcode,
230 items.homebranch,
231 items.holdingbranch,
232 biblio.title,
233 biblio.author,
234 borrowers.borrowernumber,
235 biblio.biblionumber,
236 borrowers.branchcode,
237 items.itemcallnumber,
238 items.replacementprice,
239 items.enumchron
240 FROM issues
241 LEFT JOIN borrowers ON (issues.borrowernumber=borrowers.borrowernumber )
242 LEFT JOIN items ON (issues.itemnumber=items.itemnumber)
243 LEFT JOIN biblioitems ON (biblioitems.biblioitemnumber=items.biblioitemnumber)
244 LEFT JOIN biblio ON (biblio.biblionumber=items.biblionumber )
245 WHERE 1=1 "; # placeholder, since it is possible that none of the additional
246 # conditions will be selected by user
247 $strsth.=" AND date_due < '" . $todaysdate . "' " unless ($showall);
248 $strsth.=" AND (borrowers.firstname like '".$bornamefilter."%' or borrowers.surname like '".$bornamefilter."%' or borrowers.cardnumber like '".$bornamefilter."%')" if($bornamefilter) ;
249 $strsth.=" AND borrowers.categorycode = '" . $borcatfilter . "' " if $borcatfilter;
250 if( $itemtypefilter ){
251 if( C4::Context->preference('item-level_itypes') ){
252 $strsth.=" AND items.itype = '" . $itemtypefilter . "' ";
253 } else {
254 $strsth.=" AND biblioitems.itemtype = '" . $itemtypefilter . "' ";
257 if ( $borflagsfilter eq 'gonenoaddress' ) {
258 $strsth .= " AND borrowers.gonenoaddress <> 0";
260 elsif ( $borflagsfilter eq 'debarred' ) {
261 $strsth .= " AND borrowers.debarred >= CURDATE()" ;
263 elsif ( $borflagsfilter eq 'lost') {
264 $strsth .= " AND borrowers.lost <> 0";
266 $strsth.=" AND borrowers.branchcode = '" . $branchfilter . "' " if $branchfilter;
267 $strsth.=" AND items.homebranch = '" . $homebranchfilter . "' " if $homebranchfilter;
268 $strsth.=" AND items.holdingbranch = '" . $holdingbranchfilter . "' " if $holdingbranchfilter;
269 $strsth.=" AND date_due >= ?" if $dateduefrom;
270 $strsth.=" AND date_due <= ?" if $datedueto;
271 # restrict patrons (borrowers) to those matching the patron attribute filter(s), if any
272 my $bnlist = $have_pattr_filter_data ? join(',',keys %borrowernumber_to_attributes) : '';
273 $strsth =~ s/WHERE 1=1/WHERE 1=1 AND borrowers.borrowernumber IN ($bnlist)/ if $bnlist;
274 $strsth =~ s/WHERE 1=1/WHERE 0=1/ if $have_pattr_filter_data && !$bnlist; # no match if no borrowers matched patron attrs
275 $strsth.=" ORDER BY date_due, surname, firstname";
276 $template->param(sql=>$strsth);
277 my $sth=$dbh->prepare($strsth);
278 $sth->execute(
279 ($dateduefrom ? DateTime::Format::MySQL->format_datetime($dateduefrom) : ()),
280 ($datedueto ? DateTime::Format::MySQL->format_datetime($datedueto) : ()),
283 my @overduedata;
284 while (my $data = $sth->fetchrow_hashref) {
286 # most of the overdue report data is linked to the database schema, i.e. things like borrowernumber and phone
287 # but the patron attributes (patron_attr_value_loop) are unnormalised and varies dynamically from one db to the next
289 my $pattrs = $borrowernumber_to_attributes{$data->{borrowernumber}} || {}; # patron attrs for this borrower
290 # $pattrs is a hash { attrcode => [ [value,displayvalue], [value,displayvalue]... ] }
292 my @patron_attr_value_loop; # template array [ {value=>v1}, {value=>v2} ... } ]
293 for my $pattr_filter (grep { ! $_->{isclone} } @patron_attr_filter_loop) {
294 my @displayvalues = map { $_->[1] } @{ $pattrs->{$pattr_filter->{code}} }; # grab second value from each subarray
295 push @patron_attr_value_loop, { value => join(', ', sort { lc $a cmp lc $b } @displayvalues) };
297 my $dt = dt_from_string($data->{date_due}, 'sql');
299 push @overduedata, {
300 duedate => output_pref($dt),
301 borrowernumber => $data->{borrowernumber},
302 barcode => $data->{barcode},
303 cardnumber => $data->{cardnumber},
304 itemnum => $data->{itemnumber},
305 issuedate => output_pref({ dt => dt_from_string( $data->{issuedate} ), dateonly => 1 }),
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 biblionumber => $data->{biblionumber},
319 title => $data->{title},
320 author => $data->{author},
321 branchcode => $data->{branchcode},
322 homebranchcode => $data->{homebranchcode},
323 holdingbranchcode => $data->{holdingbranchcode},
324 itemcallnumber => $data->{itemcallnumber},
325 replacementprice => $data->{replacementprice},
326 enumchron => $data->{enumchron},
327 patron_attr_value_loop => \@patron_attr_value_loop,
331 if ($op eq 'csv') {
332 binmode(STDOUT, ":encoding(UTF-8)");
333 my $csv = build_csv(\@overduedata);
334 print $input->header(-type => 'application/vnd.sun.xml.calc',
335 -encoding => 'utf-8',
336 -attachment=>"overdues.csv",
337 -filename=>"overdues.csv" );
338 print $csv;
339 exit;
342 # generate parameter list for CSV download link
343 my $new_cgi = CGI->new($input);
344 $new_cgi->delete('op');
345 my $csv_param_string = $new_cgi->query_string();
347 $template->param(
348 csv_param_string => $csv_param_string,
349 todaysdate => output_pref($today_dt),
350 overdueloop => \@overduedata,
351 nnoverdue => scalar(@overduedata),
352 noverdue_is_plural => scalar(@overduedata) != 1,
353 noreport => $noreport,
354 isfiltered => $isfiltered,
355 borflag_gonenoaddress => $borflagsfilter eq 'gonenoaddress',
356 borflag_debarred => $borflagsfilter eq 'debarred',
357 borflag_lost => $borflagsfilter eq 'lost',
362 output_html_with_http_headers $input, $cookie, $template->output;
365 sub build_csv {
366 my $overdues = shift;
368 return "" if scalar(@$overdues) == 0;
370 my @lines = ();
372 # build header ...
373 my @keys = qw /duedate title author borrowertitle firstname surname phone barcode email address address2 zipcode city country
374 branchcode itemcallnumber biblionumber borrowernumber itemnum issuedate replacementprice streetnumber streettype/;
375 my $csv = Text::CSV_XS->new();
376 $csv->combine(@keys);
377 push @lines, $csv->string();
379 # ... and rest of report
380 foreach my $overdue ( @{ $overdues } ) {
381 push @lines, $csv->string() if $csv->combine(map { $overdue->{$_} } @keys);
384 return join("\n", @lines) . "\n";