Bug 9823: Refactor return from GetReservesFromBiblionumber
[koha.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, $constraint, $bibitems,  $priority, $notes, $title, $checkitem,  $found
627     AddReserve( $branch, $borrowernumber, $biblionumber, 'a', undef, 0, undef, $title, undef, undef );
628
629     # Hashref building
630     my $out;
631     $out->{'title'}           = $title;
632     $out->{'pickup_location'} = GetBranchName($branch);
633
634     # TODO $out->{'date_available'}  = '';
635
636     return $out;
637 }
638
639 =head2 HoldItem
640
641 Creates, for a borrower, an item-level hold request on a specific item of 
642 a bibliographic record in Koha.
643
644 Parameters:
645
646   - patron_id (Required)
647         a borrowernumber
648   - bib_id (Required)
649         a biblionumber
650   - item_id (Required)
651         an itemnumber
652   - pickup_location (Optional)
653         a branch code indicating the location to which to deliver the item for pickup
654   - needed_before_date (Optional)
655         date after which hold request is no longer needed
656   - pickup_expiry_date (Optional)
657         date after which item returned to shelf if item is not picked up 
658
659 =cut
660
661 sub HoldItem {
662     my ($cgi) = @_;
663
664     # Get the borrower or return an error code
665     my $borrowernumber = $cgi->param('patron_id');
666     my $borrower = GetMemberDetails( $borrowernumber );
667     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
668
669     # Get the biblio or return an error code
670     my $biblionumber = $cgi->param('bib_id');
671     my $biblio = GetBiblio($biblionumber);
672     return { code => 'RecordNotFound' } unless $$biblio{biblionumber};
673
674     my $title = $$biblio{title};
675
676     # Get the item or return an error code
677     my $itemnumber = $cgi->param('item_id');
678     my $item = GetItem( $itemnumber );
679     return { code => 'RecordNotFound' } unless $$item{itemnumber};
680
681     # If the biblio does not match the item, return an error code
682     return { code => 'RecordNotFound' } if $$item{biblionumber} ne $$biblio{biblionumber};
683
684     # Check for item disponibility
685     my $canitembereserved = C4::Reserves::CanItemBeReserved( $borrowernumber, $itemnumber );
686     my $canbookbereserved = C4::Reserves::CanBookBeReserved( $borrowernumber, $biblionumber );
687     return { code => 'NotHoldable' } unless $canbookbereserved and $canitembereserved;
688
689     my $branch;
690
691     # Pickup branch management
692     if ( $cgi->param('pickup_location') ) {
693         $branch = $cgi->param('pickup_location');
694         my $branches = GetBranches();
695         return { code => 'LocationNotFound' } unless $$branches{$branch};
696     } else { # if the request provide no branch, use the borrower's branch
697         $branch = $$borrower{branchcode};
698     }
699
700     my $rank;
701     my $found;
702
703     # Get rank and found
704     $rank = '0' unless C4::Context->preference('ReservesNeedReturns');
705     if ( $item->{'holdingbranch'} eq $branch ) {
706         $found = 'W' unless C4::Context->preference('ReservesNeedReturns');
707     }
708
709     # Add the reserve
710     # $branch,$borrowernumber,$biblionumber,$constraint,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found
711     AddReserve( $branch, $borrowernumber, $biblionumber, 'a', undef, $rank, '', '', '', $title, $itemnumber, $found );
712
713     # Hashref building
714     my $out;
715     $out->{'pickup_location'} = GetBranchName($branch);
716
717     # TODO $out->{'date_available'} = '';
718
719     return $out;
720 }
721
722 =head2 CancelHold
723
724 Cancels an active reserve request for the borrower.
725
726 Parameters:
727
728   - patron_id (Required)
729         a borrowernumber
730   - item_id (Required)
731         an itemnumber 
732
733 =cut
734
735 sub CancelHold {
736     my ($cgi) = @_;
737
738     # Get the borrower or return an error code
739     my $borrowernumber = $cgi->param('patron_id');
740     my $borrower = GetMemberDetails( $borrowernumber );
741     return { code => 'PatronNotFound' } unless $$borrower{borrowernumber};
742
743     # Get the item or return an error code
744     my $itemnumber = $cgi->param('item_id');
745     my $item = GetItem( $itemnumber );
746     return { code => 'RecordNotFound' } unless $$item{itemnumber};
747
748     # Get borrower's reserves
749     my @reserves = GetReservesFromBorrowernumber( $borrowernumber, undef );
750     my @reserveditems;
751
752     # ...and loop over it to build an array of reserved itemnumbers
753     foreach my $reserve (@reserves) {
754         push @reserveditems, $reserve->{'itemnumber'};
755     }
756
757     # if the item was not reserved by the borrower, returns an error code
758     return { code => 'NotCanceled' } unless any { $itemnumber eq $_ } @reserveditems;
759
760     # Cancel the reserve
761     CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
762
763     return { code => 'Canceled' };
764 }
765
766 =head2 _availability
767
768 Returns, for an itemnumber, an array containing availability information.
769
770  my ($biblionumber, $status, $msg, $location) = _availability($id);
771
772 =cut
773
774 sub _availability {
775     my ($itemnumber) = @_;
776     my $item = GetItem( $itemnumber, undef, undef );
777
778     if ( not $item->{'itemnumber'} ) {
779         return ( undef, 'unknown', 'Error: could not retrieve availability for this ID', undef );
780     }
781
782     my $biblionumber = $item->{'biblioitemnumber'};
783     my $location     = GetBranchName( $item->{'holdingbranch'} );
784
785     if ( $item->{'notforloan'} ) {
786         return ( $biblionumber, 'not available', 'Not for loan', $location );
787     } elsif ( $item->{'onloan'} ) {
788         return ( $biblionumber, 'not available', 'Checked out', $location );
789     } elsif ( $item->{'itemlost'} ) {
790         return ( $biblionumber, 'not available', 'Item lost', $location );
791     } elsif ( $item->{'wthdrawn'} ) {
792         return ( $biblionumber, 'not available', 'Item withdrawn', $location );
793     } elsif ( $item->{'damaged'} ) {
794         return ( $biblionumber, 'not available', 'Item damaged', $location );
795     } else {
796         return ( $biblionumber, 'available', undef, $location );
797     }
798 }
799
800 1;