Bug 8918: Fix reserve priority in ILS-DI
[koha-equinox.git] / C4 / ILSDI / Services.pm
1 package C4::ILSDI::Services;
2
3 # Copyright 2009 SARL Biblibre
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 use strict;
21 use warnings;
22
23 use C4::Members;
24 use C4::Items;
25 use C4::Circulation;
26 use C4::Branch;
27 use C4::Accounts;
28 use C4::Biblio;
29 use C4::Reserves qw(AddReserve CancelReserve GetReservesFromBiblionumber GetReservesFromBorrowernumber CanBookBeReserved CanItemBeReserved);
30 use C4::Context;
31 use C4::AuthoritiesMarc;
32 use XML::Simple;
33 use HTML::Entities;
34 use CGI;
35 use DateTime;
36 use C4::Auth;
37
38 =head1 NAME
39
40 C4::ILS-DI::Services - ILS-DI Services
41
42 =head1 DESCRIPTION
43
44 Each function in this module represents an ILS-DI service.
45 They all takes a CGI instance as argument and most of them return a 
46 hashref that will be printed by XML::Simple in opac/ilsdi.pl
47
48 =head1 SYNOPSIS
49
50         use C4::ILSDI::Services;
51         use XML::Simple;
52         use CGI;
53
54         my $cgi = new CGI;
55
56         $out = LookupPatron($cgi);
57
58         print CGI::header('text/xml');
59         print XMLout($out,
60                 noattr => 1, 
61                 noescape => 1,
62                 nosort => 1,
63                 xmldecl => '<?xml version="1.0" encoding="UTF-8" ?>',
64                 RootName => 'LookupPatron', 
65                 SuppressEmpty => 1);
66
67 =cut
68
69 =head1 FUNCTIONS
70
71 =head2 GetAvailability
72
73 Given a set of biblionumbers or itemnumbers, returns a list with 
74 availability of the items associated with the identifiers.
75
76 Parameters:
77
78 =head3 id (Required)
79
80 list of either biblionumbers or itemnumbers
81
82 =head3 id_type (Required)
83
84 defines the type of record identifier being used in the request, 
85 possible values:
86
87   - bib
88   - item
89
90 =head3 return_type (Optional)
91
92 requests a particular level of detail in reporting availability, 
93 possible values:
94
95   - bib
96   - item
97
98 =head3 return_fmt (Optional)
99
100 requests a particular format or set of formats in reporting 
101 availability 
102
103 =cut
104
105 sub GetAvailability {
106     my ($cgi) = @_;
107
108     my $out = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n";
109     $out .= "<dlf:collection\n";
110     $out .= "  xmlns:dlf=\"http://diglib.org/ilsdi/1.1\"\n";
111     $out .= "  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n";
112     $out .= "  xsi:schemaLocation=\"http://diglib.org/ilsdi/1.1\n";
113     $out .= "    http://diglib.org/architectures/ilsdi/schemas/1.1/dlfexpanded.xsd\">\n";
114
115     foreach my $id ( split( / /, $cgi->param('id') ) ) {
116         if ( $cgi->param('id_type') eq "item" ) {
117             my ( $biblionumber, $status, $msg, $location ) = _availability($id);
118
119             $out .= "  <dlf:record>\n";
120             $out .= "    <dlf:bibliographic id=\"" . ( $biblionumber || $id ) . "\" />\n";
121             $out .= "    <dlf:items>\n";
122             $out .= "      <dlf:item id=\"" . $id . "\">\n";
123             $out .= "        <dlf:simpleavailability>\n";
124             $out .= "          <dlf:identifier>" . $id . "</dlf:identifier>\n";
125             $out .= "          <dlf:availabilitystatus>" . $status . "</dlf:availabilitystatus>\n";
126             if ($msg)      { $out .= "          <dlf:availabilitymsg>" . $msg . "</dlf:availabilitymsg>\n"; }
127             if ($location) { $out .= "          <dlf:location>" . $location . "</dlf:location>\n"; }
128             $out .= "        </dlf:simpleavailability>\n";
129             $out .= "      </dlf:item>\n";
130             $out .= "    </dlf:items>\n";
131             $out .= "  </dlf:record>\n";
132         } else {
133             my $status;
134             my $msg;
135             my $biblioitem = ( GetBiblioItemByBiblioNumber( $id, undef ) )[0];
136             if ($biblioitem) {
137
138             } else {
139                 $status = "unknown";
140                 $msg    = "Error: could not retrieve availability for this ID";
141             }
142             $out .= "  <dlf:record>\n";
143             $out .= "    <dlf:bibliographic id=\"" . $id . "\" />\n";
144             $out .= "    <dlf:simpleavailability>\n";
145             $out .= "      <dlf:identifier>" . $id . "</dlf:identifier>\n";
146             $out .= "      <dlf:availabilitystatus>" . $status . "</dlf:availabilitystatus>\n";
147             $out .= "      <dlf:availabilitymsg>" . $msg . "</dlf:availabilitymsg>\n";
148             $out .= "    </dlf:simpleavailability>\n";
149             $out .= "  </dlf:record>\n";
150         }
151     }
152     $out .= "</dlf:collection>\n";
153
154     return $out;
155 }
156
157 =head2 GetRecords
158
159 Given a list of biblionumbers, returns a list of record objects that 
160 contain bibliographic information, as well as associated holdings and item
161 information. The caller may request a specific metadata schema for the 
162 record objects to be returned.
163
164 This function behaves similarly to HarvestBibliographicRecords and 
165 HarvestExpandedRecords in Data Aggregation, but allows quick, real time 
166 lookup by bibliographic identifier.
167
168 You can use OAI-PMH ListRecords instead of this service.
169
170 Parameters:
171
172   - id (Required)
173         list of system record identifiers
174   - id_type (Optional)
175         Defines the metadata schema in which the records are returned, 
176         possible values:
177           - MARCXML
178
179 =cut
180
181 sub GetRecords {
182     my ($cgi) = @_;
183
184     # Check if the schema is supported. For now, GetRecords only supports MARCXML
185     if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) {
186         return { code => 'UnsupportedSchema' };
187     }
188
189     my @records;
190
191     # Loop over biblionumbers
192     foreach my $biblionumber ( split( / /, $cgi->param('id') ) ) {
193
194         # Get the biblioitem from the biblionumber
195         my $biblioitem = ( GetBiblioItemByBiblioNumber( $biblionumber, undef ) )[0];
196         if ( not $biblioitem->{'biblionumber'} ) {
197             $biblioitem->{code} = "RecordNotFound";
198         }
199
200         my $embed_items = 1;
201         my $record = GetMarcBiblio($biblionumber, $embed_items);
202         if ($record) {
203             $biblioitem->{marcxml} = $record->as_xml_record();
204         }
205
206         # We don't want MARC to be displayed
207         delete $biblioitem->{'marc'};
208
209         # Get most of the needed data
210         my $biblioitemnumber = $biblioitem->{'biblioitemnumber'};
211         my $reserves         = GetReservesFromBiblionumber({ biblionumber => $biblionumber });
212         my $issues           = GetBiblioIssues($biblionumber);
213         my $items            = GetItemsByBiblioitemnumber($biblioitemnumber);
214
215         # We loop over the items to clean them
216         foreach my $item (@$items) {
217
218             # This hides additionnal XML subfields, we don't need these info
219             delete $item->{'more_subfields_xml'};
220
221             # Display branch names instead of branch codes
222             $item->{'homebranchname'}    = GetBranchName( $item->{'homebranch'} );
223             $item->{'holdingbranchname'} = GetBranchName( $item->{'holdingbranch'} );
224         }
225
226         # Hashref building...
227         $biblioitem->{'items'}->{'item'}       = $items;
228         $biblioitem->{'reserves'}->{'reserve'} = $reserves;
229         $biblioitem->{'issues'}->{'issue'}     = $issues;
230
231         push @records, $biblioitem;
232     }
233
234     return { record => \@records };
235 }
236
237 =head2 GetAuthorityRecords
238
239 Given a list of authority record identifiers, returns a list of record 
240 objects that contain the authority records. The function user may request 
241 a specific metadata schema for the record objects.
242
243 Parameters:
244
245   - id (Required)
246     list of authority record identifiers
247   - schema (Optional)
248     specifies the metadata schema of records to be returned, possible values:
249       - MARCXML
250
251 =cut
252
253 sub GetAuthorityRecords {
254     my ($cgi) = @_;
255
256     # If the user asks for an unsupported schema, return an error code
257     if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) {
258         return { code => 'UnsupportedSchema' };
259     }
260
261     my @records;
262
263     # Let's loop over the authority IDs
264     foreach my $authid ( split( / /, $cgi->param('id') ) ) {
265
266         # Get the record as XML string, or error code
267         push @records, GetAuthorityXML($authid) || { code => 'RecordNotFound' };
268     }
269
270     return { record => \@records };
271 }
272
273 =head2 LookupPatron
274
275 Looks up a patron in the ILS by an identifier, and returns the borrowernumber.
276
277 Parameters:
278
279   - id (Required)
280         an identifier used to look up the patron in Koha
281   - id_type (Optional)
282         the type of the identifier, possible values:
283         - cardnumber
284         - firstname
285         - userid
286         - borrowernumber
287
288 =cut
289
290 sub LookupPatron {
291     my ($cgi) = @_;
292
293     # Get the borrower...
294     my $borrower = GetMember($cgi->param('id_type') => $cgi->param('id'));
295     if ( not $borrower->{'borrowernumber'} ) {
296         return { message => 'PatronNotFound' };
297     }
298
299     # Build the hashref
300     my $patron->{'id'} = $borrower->{'borrowernumber'};
301     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
302
303     # ...and return his ID
304     return $patron;
305 }
306
307 =head2 AuthenticatePatron
308
309 Authenticates a user's login credentials and returns the identifier for 
310 the patron.
311
312 Parameters:
313
314   - username (Required)
315     user's login identifier (userid or cardnumber)
316   - password (Required)
317     user's password
318
319 =cut
320
321 sub AuthenticatePatron {
322     my ($cgi) = @_;
323     my ($status, $cardnumber, $userid) = C4::Auth::checkpw( C4::Context->dbh, $cgi->param('username'), $cgi->param('password') );
324     if ( $status ) {
325         # Get the borrower
326         my $borrower = GetMember( cardnumber => $cardnumber );
327         my $patron->{'id'} = $borrower->{'borrowernumber'};
328         return $patron;
329     }
330     else {
331         return { code => 'PatronNotFound' };
332     }
333 }
334
335 =head2 GetPatronInfo
336
337 Returns specified information about the patron, based on options in the 
338 request. This function can optionally return patron's contact information, 
339 fine information, hold request information, and loan information.
340
341 Parameters:
342
343   - patron_id (Required)
344         the borrowernumber
345   - show_contact (Optional, default 1)
346         whether or not to return patron's contact information in the response
347   - show_fines (Optional, default 0)
348         whether or not to return fine information in the response
349   - show_holds (Optional, default 0)
350         whether or not to return hold request information in the response
351   - show_loans (Optional, default 0)
352         whether or not to return loan information request information in the response 
353
354 =cut
355
356 sub GetPatronInfo {
357     my ($cgi) = @_;
358
359     # Get Member details
360     my $borrowernumber = $cgi->param('patron_id');
361     my $borrower = GetMemberDetails( $borrowernumber );
362     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
363
364     # Cleaning the borrower hashref
365     $borrower->{'charges'}    = $borrower->{'flags'}->{'CHARGES'}->{'amount'};
366     $borrower->{'branchname'} = GetBranchName( $borrower->{'branchcode'} );
367     delete $borrower->{'flags'};
368     delete $borrower->{'userid'};
369     delete $borrower->{'password'};
370
371     # Contact fields management
372     if ( $cgi->param('show_contact') eq "0" ) {
373
374         # Define contact fields
375         my @contactfields = (
376             'email',              'emailpro',           'fax',                 'mobile',          'phone',             'phonepro',
377             'streetnumber',       'zipcode',            'city',                'streettype',      'B_address',         'B_city',
378             'B_email',            'B_phone',            'B_zipcode',           'address',         'address2',          'altcontactaddress1',
379             'altcontactaddress2', 'altcontactaddress3', 'altcontactfirstname', 'altcontactphone', 'altcontactsurname', 'altcontactzipcode'
380         );
381
382         # and delete them
383         foreach my $field (@contactfields) {
384             delete $borrower->{$field};
385         }
386     }
387
388     # Fines management
389     if ( $cgi->param('show_fines') eq "1" ) {
390         my @charges;
391         for ( my $i = 1 ; my @charge = getcharges( $borrowernumber, undef, $i ) ; $i++ ) {
392             push( @charges, @charge );
393         }
394         $borrower->{'fines'}->{'fine'} = \@charges;
395     }
396
397     # Reserves management
398     if ( $cgi->param('show_holds') eq "1" ) {
399
400         # Get borrower's reserves
401         my @reserves = GetReservesFromBorrowernumber( $borrowernumber, undef );
402         foreach my $reserve (@reserves) {
403
404             # Get additional informations
405             my $item = GetBiblioFromItemNumber( $reserve->{'itemnumber'}, undef );
406             my $branchname = GetBranchName( $reserve->{'branchcode'} );
407
408             # Remove unwanted fields
409             delete $item->{'marc'};
410             delete $item->{'marcxml'};
411             delete $item->{'more_subfields_xml'};
412
413             # Add additional fields
414             $reserve->{'item'}       = $item;
415             $reserve->{'branchname'} = $branchname;
416             $reserve->{'title'}      = GetBiblio( $reserve->{'biblionumber'} )->{'title'};
417         }
418         $borrower->{'holds'}->{'hold'} = \@reserves;
419     }
420
421     # Issues management
422     if ( $cgi->param('show_loans') eq "1" ) {
423         my $issues = GetPendingIssues($borrowernumber);
424         foreach my $issue ( @$issues ){
425             $issue->{'issuedate'} = $issue->{'issuedate'}->strftime('%Y-%m-%d %H:%M');
426             $issue->{'date_due'} = $issue->{'date_due'}->strftime('%Y-%m-%d %H:%M');
427         }
428         $borrower->{'loans'}->{'loan'} = $issues;
429     }
430
431     return $borrower;
432 }
433
434 =head2 GetPatronStatus
435
436 Returns a patron's status information.
437
438 Parameters:
439
440   - patron_id (Required)
441         the borrower ID
442
443 =cut
444
445 sub GetPatronStatus {
446     my ($cgi) = @_;
447
448     # Get Member details
449     my $borrowernumber = $cgi->param('patron_id');
450     my $borrower = GetMemberDetails( $borrowernumber );
451     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
452
453     # Return the results
454     return {
455         type   => $$borrower{categorycode},
456         status => 0, # TODO
457         expiry => $$borrower{dateexpiry},
458     };
459 }
460
461 =head2 GetServices
462
463 Returns information about the services available on a particular item for 
464 a particular patron.
465
466 Parameters:
467
468   - patron_id (Required)
469         a borrowernumber
470   - item_id (Required)
471         an itemnumber
472 =cut
473
474 sub GetServices {
475     my ($cgi) = @_;
476
477     # Get the member, or return an error code if not found
478     my $borrowernumber = $cgi->param('patron_id');
479     my $borrower = GetMemberDetails( $borrowernumber );
480     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
481
482     # Get the item, or return an error code if not found
483     my $itemnumber = $cgi->param('item_id');
484     my $item = GetItem( $itemnumber );
485     return { code => 'RecordNotFound' } unless $$item{itemnumber};
486
487     my @availablefor;
488
489     # Reserve level management
490     my $biblionumber = $item->{'biblionumber'};
491     my $canbookbereserved = CanBookBeReserved( $borrower, $biblionumber );
492     if ($canbookbereserved) {
493         push @availablefor, 'title level hold';
494         my $canitembereserved = IsAvailableForItemLevelRequest($itemnumber);
495         if ($canitembereserved) {
496             push @availablefor, 'item level hold';
497         }
498     }
499
500     # Reserve cancellation management
501     my @reserves = GetReservesFromBorrowernumber( $borrowernumber, undef );
502     my @reserveditems;
503     foreach my $reserve (@reserves) {
504         push @reserveditems, $reserve->{'itemnumber'};
505     }
506     if ( grep { $itemnumber eq $_ } @reserveditems ) {
507         push @availablefor, 'hold cancellation';
508     }
509
510     # Renewal management
511     my @renewal = CanBookBeRenewed( $borrowernumber, $itemnumber );
512     if ( $renewal[0] ) {
513         push @availablefor, 'loan renewal';
514     }
515
516     # Issuing management
517     my $barcode = $item->{'barcode'} || '';
518     $barcode = barcodedecode($barcode) if ( $barcode && C4::Context->preference('itemBarcodeInputFilter') );
519     if ($barcode) {
520         my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $borrower, $barcode );
521
522         # TODO push @availablefor, 'loan';
523     }
524
525     my $out;
526     $out->{'AvailableFor'} = \@availablefor;
527
528     return $out;
529 }
530
531 =head2 RenewLoan
532
533 Extends the due date for a borrower's existing issue.
534
535 Parameters:
536
537   - patron_id (Required)
538         a borrowernumber
539   - item_id (Required)
540         an itemnumber
541   - desired_due_date (Required)
542         the date the patron would like the item returned by 
543
544 =cut
545
546 sub RenewLoan {
547     my ($cgi) = @_;
548
549     # Get borrower infos or return an error code
550     my $borrowernumber = $cgi->param('patron_id');
551     my $borrower = GetMemberDetails( $borrowernumber );
552     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
553
554     # Get the item, or return an error code
555     my $itemnumber = $cgi->param('item_id');
556     my $item = GetItem( $itemnumber );
557     return { code => 'RecordNotFound' } unless $$item{itemnumber};
558
559     # Add renewal if possible
560     my @renewal = CanBookBeRenewed( $borrowernumber, $itemnumber );
561     if ( $renewal[0] ) { AddRenewal( $borrowernumber, $itemnumber ); }
562
563     my $issue = GetItemIssue($itemnumber);
564
565     # Hashref building
566     my $out;
567     $out->{'renewals'} = $issue->{'renewals'};
568     $out->{date_due}   = $issue->{date_due}->strftime('%Y-%m-%d %H:%S');
569     $out->{'success'}  = $renewal[0];
570     $out->{'error'}    = $renewal[1];
571
572     return $out;
573 }
574
575 =head2 HoldTitle
576
577 Creates, for a borrower, a biblio-level hold reserve.
578
579 Parameters:
580
581   - patron_id (Required)
582         a borrowernumber
583   - bib_id (Required)
584         a biblionumber
585   - request_location (Required)
586         IP address where the end user request is being placed
587   - pickup_location (Optional)
588         a branch code indicating the location to which to deliver the item for pickup
589   - needed_before_date (Optional)
590         date after which hold request is no longer needed
591   - pickup_expiry_date (Optional)
592         date after which item returned to shelf if item is not picked up 
593
594 =cut
595
596 sub HoldTitle {
597     my ($cgi) = @_;
598
599     # Get the borrower or return an error code
600     my $borrowernumber = $cgi->param('patron_id');
601     my $borrower = GetMemberDetails( $borrowernumber );
602     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
603
604     # Get the biblio record, or return an error code
605     my $biblionumber = $cgi->param('bib_id');
606     my $biblio = GetBiblio( $biblionumber );
607     return { code => 'RecordNotFound' } unless $$biblio{biblionumber};
608     
609     my $title = $$biblio{title};
610
611     # Check if the biblio can be reserved
612     return { code => 'NotHoldable' } unless CanBookBeReserved( $borrowernumber, $biblionumber );
613
614     my $branch;
615
616     # Pickup branch management
617     if ( $cgi->param('pickup_location') ) {
618         $branch = $cgi->param('pickup_location');
619         my $branches = GetBranches;
620         return { code => 'LocationNotFound' } unless $$branches{$branch};
621     } else { # if the request provide no branch, use the borrower's branch
622         $branch = $$borrower{branchcode};
623     }
624
625     # Add the reserve
626     #    $branch,    $borrowernumber, $biblionumber,
627     #    $constraint, $bibitems,  $priority, $resdate, $expdate, $notes,
628     #    $title,      $checkitem, $found
629     my $priority= C4::Reserves::CalculatePriority( $biblionumber );
630     AddReserve( $branch, $borrowernumber, $biblionumber, 'a', undef, $priority, undef, undef, undef, $title, undef, undef );
631
632     # Hashref building
633     my $out;
634     $out->{'title'}           = $title;
635     $out->{'pickup_location'} = GetBranchName($branch);
636
637     # TODO $out->{'date_available'}  = '';
638
639     return $out;
640 }
641
642 =head2 HoldItem
643
644 Creates, for a borrower, an item-level hold request on a specific item of 
645 a bibliographic record in Koha.
646
647 Parameters:
648
649   - patron_id (Required)
650         a borrowernumber
651   - bib_id (Required)
652         a biblionumber
653   - item_id (Required)
654         an itemnumber
655   - pickup_location (Optional)
656         a branch code indicating the location to which to deliver the item for pickup
657   - needed_before_date (Optional)
658         date after which hold request is no longer needed
659   - pickup_expiry_date (Optional)
660         date after which item returned to shelf if item is not picked up 
661
662 =cut
663
664 sub HoldItem {
665     my ($cgi) = @_;
666
667     # Get the borrower or return an error code
668     my $borrowernumber = $cgi->param('patron_id');
669     my $borrower = GetMemberDetails( $borrowernumber );
670     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
671
672     # Get the biblio or return an error code
673     my $biblionumber = $cgi->param('bib_id');
674     my $biblio = GetBiblio($biblionumber);
675     return { code => 'RecordNotFound' } unless $$biblio{biblionumber};
676
677     my $title = $$biblio{title};
678
679     # Get the item or return an error code
680     my $itemnumber = $cgi->param('item_id');
681     my $item = GetItem( $itemnumber );
682     return { code => 'RecordNotFound' } unless $$item{itemnumber};
683
684     # If the biblio does not match the item, return an error code
685     return { code => 'RecordNotFound' } if $$item{biblionumber} ne $$biblio{biblionumber};
686
687     # Check for item disponibility
688     my $canitembereserved = C4::Reserves::CanItemBeReserved( $borrowernumber, $itemnumber );
689     my $canbookbereserved = C4::Reserves::CanBookBeReserved( $borrowernumber, $biblionumber );
690     return { code => 'NotHoldable' } unless $canbookbereserved and $canitembereserved;
691
692     # Pickup branch management
693     my $branch;
694     if ( $cgi->param('pickup_location') ) {
695         $branch = $cgi->param('pickup_location');
696         my $branches = GetBranches();
697         return { code => 'LocationNotFound' } unless $$branches{$branch};
698     } else { # if the request provide no branch, use the borrower's branch
699         $branch = $$borrower{branchcode};
700     }
701
702     # Add the reserve
703     #    $branch,    $borrowernumber, $biblionumber,
704     #    $constraint, $bibitems,  $priority, $resdate, $expdate, $notes,
705     #    $title,      $checkitem, $found
706     my $priority= C4::Reserves::CalculatePriority( $biblionumber );
707     AddReserve( $branch, $borrowernumber, $biblionumber, 'a', undef, $priority, undef, undef, undef, $title, $itemnumber, undef );
708
709     # Hashref building
710     my $out;
711     $out->{'pickup_location'} = GetBranchName($branch);
712
713     # TODO $out->{'date_available'} = '';
714
715     return $out;
716 }
717
718 =head2 CancelHold
719
720 Cancels an active reserve request for the borrower.
721
722 Parameters:
723
724   - patron_id (Required)
725         a borrowernumber
726   - item_id (Required)
727         an itemnumber 
728
729 =cut
730
731 sub CancelHold {
732     my ($cgi) = @_;
733
734     # Get the borrower or return an error code
735     my $borrowernumber = $cgi->param('patron_id');
736     my $borrower = GetMemberDetails( $borrowernumber );
737     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
738
739     # Get the item or return an error code
740     my $itemnumber = $cgi->param('item_id');
741     my $item = GetItem( $itemnumber );
742     return { code => 'RecordNotFound' } unless $$item{itemnumber};
743
744     # Get borrower's reserves
745     my @reserves = GetReservesFromBorrowernumber( $borrowernumber, undef );
746     my @reserveditems;
747
748     # ...and loop over it to build an array of reserved itemnumbers
749     foreach my $reserve (@reserves) {
750         push @reserveditems, $reserve->{'itemnumber'};
751     }
752
753     # if the item was not reserved by the borrower, returns an error code
754     return { code => 'NotCanceled' } unless any { $itemnumber eq $_ } @reserveditems;
755
756     # Cancel the reserve
757     CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
758
759     return { code => 'Canceled' };
760 }
761
762 =head2 _availability
763
764 Returns, for an itemnumber, an array containing availability information.
765
766  my ($biblionumber, $status, $msg, $location) = _availability($id);
767
768 =cut
769
770 sub _availability {
771     my ($itemnumber) = @_;
772     my $item = GetItem( $itemnumber, undef, undef );
773
774     if ( not $item->{'itemnumber'} ) {
775         return ( undef, 'unknown', 'Error: could not retrieve availability for this ID', undef );
776     }
777
778     my $biblionumber = $item->{'biblioitemnumber'};
779     my $location     = GetBranchName( $item->{'holdingbranch'} );
780
781     if ( $item->{'notforloan'} ) {
782         return ( $biblionumber, 'not available', 'Not for loan', $location );
783     } elsif ( $item->{'onloan'} ) {
784         return ( $biblionumber, 'not available', 'Checked out', $location );
785     } elsif ( $item->{'itemlost'} ) {
786         return ( $biblionumber, 'not available', 'Item lost', $location );
787     } elsif ( $item->{'wthdrawn'} ) {
788         return ( $biblionumber, 'not available', 'Item withdrawn', $location );
789     } elsif ( $item->{'damaged'} ) {
790         return ( $biblionumber, 'not available', 'Item damaged', $location );
791     } else {
792         return ( $biblionumber, 'available', undef, $location );
793     }
794 }
795
796 1;