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