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