Bug 25393: UI adjustments
[koha.git] / svc / checkouts
1 #!/usr/bin/perl
2
3 # Copyright 2014 ByWater Solutions
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use CGI;
23 use JSON qw(to_json);
24
25 use C4::Auth qw(check_cookie_auth haspermission);
26 use C4::Circulation qw(GetIssuingCharges CanBookBeRenewed GetRenewCount );
27 use C4::Overdues qw(GetFine);
28 use C4::Context;
29
30 use Koha::AuthorisedValues;
31 use Koha::DateUtils qw( dt_from_string output_pref );
32 use Koha::Items;
33 use Koha::ItemTypes;
34
35 my $input = CGI->new;
36
37 my ( $auth_status, $session ) = check_cookie_auth( $input->cookie('CGISESSID'));
38 if( $auth_status ne 'ok' ) {
39     print CGI::header( '-status' => '401' );
40     exit 0;
41 }
42
43 my $userid   = $session->param('id');
44
45 unless (haspermission($userid, { circulate => 'circulate_remaining_permissions' })
46     || haspermission($userid, { borrowers => 'edit_borrowers' })) {
47     exit 0;
48 }
49
50 my @sort_columns = qw/date_due title itype issuedate branchcode itemcallnumber/;
51
52 my @borrowernumber   = $input->multi_param('borrowernumber');
53 my $offset           = $input->param('iDisplayStart');
54 my $results_per_page = $input->param('iDisplayLength') || -1;
55
56 my $sorting_column = $input->param('iSortCol_0') || q{};
57 $sorting_column = ( $sorting_column && $sort_columns[$sorting_column] ) ? $sort_columns[$sorting_column] : 'issuedate';
58
59 my $sorting_direction = $input->param('sSortDir_0') || q{};
60 $sorting_direction = $sorting_direction eq 'asc' ? 'asc' : 'desc';
61
62 $results_per_page = undef if ( $results_per_page == -1 );
63
64 binmode STDOUT, ":encoding(UTF-8)";
65 print $input->header( -type => 'text/plain', -charset => 'UTF-8' );
66
67 my @parameters;
68 my $sql = '
69     SELECT
70         issues.issue_id,
71         issues.issuedate,
72         issues.date_due,
73         issues.date_due < now() as date_due_overdue,
74         issues.timestamp,
75         issues.auto_renew,
76
77         issues.onsite_checkout,
78
79         biblio.biblionumber,
80         biblio.title,
81         biblio.subtitle,
82         biblio.medium,
83         biblio.part_number,
84         biblio.part_name,
85         biblio.author,
86
87         items.itemnumber,
88         items.barcode,
89         branches2.branchname AS homebranch,
90         items.itemnotes,
91         items.itemnotes_nonpublic,
92         items.itemcallnumber,
93         items.copynumber,
94         items.replacementprice,
95
96         issues.branchcode,
97         branches.branchname,
98
99         items.itype,
100         biblioitems.itemtype,
101
102         items.ccode AS collection,
103
104         borrowers.borrowernumber,
105         borrowers.surname,
106         borrowers.firstname,
107         borrowers.cardnumber,
108
109         items.itemlost,
110         items.damaged,
111         items.location,
112         items.enumchron,
113         items.materials,
114
115         DATEDIFF( issues.issuedate, CURRENT_DATE() ) AS not_issued_today,
116
117         return_claims.id AS return_claim_id,
118         return_claims.notes AS return_claim_notes,
119         return_claims.created_on AS return_claim_created_on,
120         return_claims.updated_on AS return_claim_updated_on
121
122     FROM issues
123         LEFT JOIN items USING ( itemnumber )
124         LEFT JOIN biblio USING ( biblionumber )
125         LEFT JOIN biblioitems USING ( biblionumber )
126         LEFT JOIN borrowers USING ( borrowernumber )
127         LEFT JOIN branches ON ( issues.branchcode = branches.branchcode )
128         LEFT JOIN branches branches2 ON ( items.homebranch = branches2.branchcode )
129         LEFT JOIN return_claims USING ( issue_id )
130     WHERE issues.borrowernumber
131 ';
132
133 if ( @borrowernumber == 1 ) {
134     $sql .= '= ?';
135 }
136 else {
137     $sql .= ' IN (' . join( ',', ('?') x @borrowernumber ) . ') ';
138 }
139 push( @parameters, @borrowernumber );
140
141 $sql .= " ORDER BY $sorting_column $sorting_direction ";
142
143 my $dbh = C4::Context->dbh();
144 my $sth = $dbh->prepare($sql);
145 $sth->execute(@parameters);
146
147 my $item_level_itypes = C4::Context->preference('item-level_itypes');
148 my $claims_returned_lost_value = C4::Context->preference('ClaimReturnedLostValue');
149 my $confirm_parts_required = C4::Context->preference("CircConfirmItemParts");
150
151 my $itemtypes = { map { $_->{itemtype} => $_->{translated_description} } @{ Koha::ItemTypes->search_with_localization->unblessed } };
152
153 my @checkouts_today;
154 my @checkouts_previous;
155 while ( my $c = $sth->fetchrow_hashref() ) {
156     my ($charge) = GetIssuingCharges( $c->{itemnumber}, $c->{borrowernumber} );
157     my $fine = GetFine( $c->{itemnumber}, $c->{borrowernumber} );
158
159     my $checkout_obj = Koha::Checkouts->find( $c->{issue_id} );
160     my ( $can_renew, $can_renew_error, $info ) =
161       CanBookBeRenewed( $checkout_obj->patron, $checkout_obj );
162     my $can_renew_date =
163       $can_renew_error && $can_renew_error eq 'too_soon'
164       ? output_pref(
165         {
166             dt => $info->{soonest_renew_date},
167             as_due_date => 1
168         }
169       )
170       : undef;
171
172     my (
173         $renewals_count,
174         $renewals_allowed,
175         $renewals_remaining,
176         $unseen_count,
177         $unseen_allowed,
178         $unseen_remaining
179     ) =
180       GetRenewCount( $c->{borrowernumber}, $c->{itemnumber} );
181
182     my ( $itemtype, $recordtype, $type_for_stat );
183     $itemtype      = $itemtypes->{ $c->{itype} }    if $c->{itype};
184     $recordtype    = $itemtypes->{ $c->{itemtype} } if $c->{itemtype};
185     $type_for_stat = $item_level_itypes ? $itemtype : $recordtype;
186
187     my $location;
188     if ( $c->{location} ) {
189         my $av = Koha::AuthorisedValues->get_description_by_koha_field(
190             { kohafield => 'items.location', authorised_value => $c->{location} } );
191         $location = $av->{lib} ? $av->{lib} : '';
192     }
193     my $collection;
194     if ( $c->{collection} ) {
195         my $av = Koha::AuthorisedValues->get_description_by_koha_field(
196             { kohafield => 'items.ccode', authorised_value => $c->{collection} } );
197         $collection = $av->{lib} ? $av->{lib} : '';
198     }
199     my $lost;
200     my $claims_returned;
201     if ( $c->{itemlost} ) {
202         my $av = Koha::AuthorisedValues->get_description_by_koha_field(
203             { kohafield => 'items.itemlost', authorised_value => $c->{itemlost} } );
204         $lost            = $av->{lib} ? $av->{lib} : '';
205         $claims_returned = $c->{itemlost} eq $claims_returned_lost_value;
206     }
207     my $damaged;
208     if ( $c->{damaged} ) {
209         my $av = Koha::AuthorisedValues->get_description_by_koha_field(
210             { kohafield => 'items.damaged', authorised_value => $c->{damaged} } );
211         $damaged = $av->{lib} ? $av->{lib} : '';
212     }
213     my $materials;
214     if ( $c->{materials} && $confirm_parts_required ) {
215         my $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => '', kohafield =>'items.materials', authorised_value => $c->{materials} });
216         $materials = $descriptions->{lib} // $c->{materials};
217     }
218     my @subtitles = split(/ \| /, $c->{'subtitle'} // '' );
219
220     my $recalled = 0;
221     if ( C4::Context->preference('UseRecalls') ) {
222         my $item = Koha::Items->find( $c->{itemnumber} );
223         my $recall = undef;
224         $recall = $item->check_recalls if $item->can_be_waiting_recall;
225         if ( defined $recall ) {
226             if ( $recall->item_level ) {
227                 if ( $recall->item_id == $c->{itemnumber} ) {
228                     # item-level recall on this item
229                     $recalled = 1;
230                 } else {
231                     $recalled = 0;
232                 }
233             } else {
234                 # biblio-level recall, but don't want to mark recalled if the recall has been allocated a different item
235                 if ( !$recall->waiting ) {
236                     $recalled = 1;
237                 }
238             }
239         }
240     }
241
242     my $checkout = {
243         DT_RowId             => $c->{itemnumber} . '-' . $c->{borrowernumber},
244         title                => $c->{title},
245         subtitle             => \@subtitles,
246         medium               => $c->{medium} // '',
247         part_number          => $c->{part_number} // '',
248         part_name            => $c->{part_name} // '',
249         author               => $c->{author},
250         barcode              => $c->{barcode},
251         type_for_stat          => $type_for_stat || q{},
252         itemtype_description   => $itemtype || q{},
253         recordtype_description => $recordtype || q{},
254         collection           => $collection,
255         location             => $location,
256         homebranch           => $c->{homebranch},
257         itemnotes            => $c->{itemnotes},
258         itemnotes_nonpublic  => $c->{itemnotes_nonpublic},
259         branchcode           => $c->{branchcode},
260         branchname           => $c->{branchname},
261         itemcallnumber       => $c->{itemcallnumber} || q{},
262         copynumber           => $c->{copynumber} || q{},
263         charge         => $charge,
264         fine           => $fine,
265         price          => $c->{replacementprice} || q{},
266         can_renew      => $can_renew,
267         can_renew_error     => $can_renew_error,
268         can_renew_date      => $can_renew_date,
269         itemnumber          => $c->{itemnumber},
270         borrowernumber      => $c->{borrowernumber},
271         biblionumber        => $c->{biblionumber},
272         issuedate           => $c->{issuedate},
273         date_due            => $c->{date_due},
274         date_due_overdue    => $c->{date_due_overdue} ? JSON::true : JSON::false,
275         timestamp           => $c->{timestamp},
276         auto_renew          => $c->{auto_renew},
277         onsite_checkout     => $c->{onsite_checkout},
278         enumchron           => $c->{enumchron},
279         renewals_count      => $renewals_count,
280         renewals_allowed    => $renewals_allowed || 0,
281         renewals_remaining  => $renewals_remaining,
282         unseen_count        => $unseen_count,
283         unseen_allowed      => $unseen_allowed,
284         unseen_remaining    => $unseen_remaining,
285
286         return_claim_id         => $c->{return_claim_id},
287         return_claim_notes      => $c->{return_claim_notes},
288         return_claim_created_on => $c->{return_claim_created_on},
289         return_claim_updated_on => $c->{return_claim_updated_on},
290         return_claim_created_on_formatted => $c->{return_claim_created_on} ? output_pref({ dt => dt_from_string( $c->{return_claim_created_on} ) }) : undef,
291         return_claim_updated_on_formatted => $c->{return_claim_updated_on} ? output_pref({ dt => dt_from_string( $c->{return_claim_updated_on} ) }) : undef,
292
293         lost    => $lost,
294         claims_returned => $claims_returned,
295         damaged => $damaged,
296         materials => $materials,
297         borrower => {
298             surname    => $c->{surname},
299             firstname  => $c->{firstname},
300             cardnumber => $c->{cardnumber},
301         },
302         issued_today => !$c->{not_issued_today},
303         recalled => $recalled,
304     };
305
306     if ( $c->{not_issued_today} ) {
307         push( @checkouts_previous, $checkout );
308     }
309     else {
310         push( @checkouts_today, $checkout );
311     }
312 }
313
314
315 @checkouts_today = sort { $a->{timestamp} cmp $b->{timestamp} } @checkouts_today;    # latest to earliest
316 @checkouts_today = reverse(@checkouts_today)
317   if ( C4::Context->preference('todaysIssuesDefaultSortOrder') eq 'desc' );      # earliest to latest
318
319 @checkouts_previous =
320   sort { $a->{date_due} cmp $b->{date_due} || $a->{timestamp} cmp $b->{timestamp} }
321   @checkouts_previous;                                                               # latest to earliest
322 @checkouts_previous = reverse(@checkouts_previous)
323   if ( C4::Context->preference('previousIssuesDefaultSortOrder') eq 'desc' );    # earliest to latest
324
325 my @checkouts = ( @checkouts_today, @checkouts_previous );
326
327 my $i = 1;
328 map { $_->{sort_order} = $i++ } @checkouts;
329
330
331 my $data;
332 $data->{'iTotalRecords'}        = scalar @checkouts;
333 $data->{'iTotalDisplayRecords'} = scalar @checkouts;
334 $data->{'sEcho'}                = $input->param('sEcho') || undef;
335 $data->{'aaData'}               = \@checkouts;
336
337 print to_json($data);