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