Bug 29562: Adjust CanItemBeReserved and checkHighHolds to take objects
[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::Biblio qw( GetMarcBiblio );
28 use C4::Reserves qw( CanBookBeReserved IsAvailableForItemLevelRequest CalculatePriority AddReserve CanItemBeReserved );
29 use C4::Context;
30 use C4::Auth;
31 use CGI qw ( -utf8 );
32 use DateTime;
33 use C4::Auth;
34 use Koha::DateUtils qw( dt_from_string );
35 use C4::AuthoritiesMarc qw( GetAuthorityXML );
36
37 use Koha::Biblios;
38 use Koha::Checkouts;
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 $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 $checkouts = Koha::Checkouts->search(
232             { biblionumber => $biblionumber },
233             {
234                 join => 'item',
235                 '+select' => ['item.barcode'],
236                 '+as'     => ['barcode'],
237             }
238         )->unblessed;
239         foreach my $checkout (@$checkouts) {
240             delete $checkout->{'borrowernumber'};
241         }
242         my @items            = $biblio->items->as_list;
243
244         $biblioitem->{items}->{item} = [];
245
246         # We loop over the items to clean them
247         foreach my $item (@items) {
248             my %item = %{ $item->unblessed };
249
250             # This hides additionnal XML subfields, we don't need these info
251             delete $item{'more_subfields_xml'};
252
253             # Display branch names instead of branch codes
254             my $home_library    = $item->home_branch;
255             my $holding_library = $item->holding_branch;
256             $item{'homebranchname'}    = $home_library    ? $home_library->branchname    : '';
257             $item{'holdingbranchname'} = $holding_library ? $holding_library->branchname : '';
258
259             if ($item->location) {
260                 my $authorised_value = Koha::AuthorisedValues->find_by_koha_field({ kohafield => 'items.location', authorised_value => $item->location });
261                 if ($authorised_value) {
262                     $item{location_description} = $authorised_value->opac_description;
263                 }
264             }
265
266             if ($item->itype) {
267                 my $itemtype = Koha::ItemTypes->find($item->itype);
268                 if ($itemtype) {
269                     $item{itype_description} = $itemtype->description;
270                 }
271             }
272
273             my $transfer = $item->get_transfer;
274             if ($transfer) {
275                 $item{transfer} = {
276                     datesent => $transfer->datesent,
277                     frombranch => $transfer->frombranch,
278                     tobranch => $transfer->tobranch,
279                 };
280             }
281
282             push @{ $biblioitem->{items}->{item} }, \%item;
283         }
284
285         # Holds
286         my $holds = $biblio->current_holds->unblessed;
287         foreach my $hold (@$holds) {
288             delete $hold->{'borrowernumber'};
289         }
290
291         # Hashref building...
292         $biblioitem->{'reserves'}->{'reserve'} = $holds;
293         $biblioitem->{'issues'}->{'issue'}     = $checkouts;
294
295         push @records, $biblioitem;
296     }
297
298     return { record => \@records };
299 }
300
301 =head2 GetAuthorityRecords
302
303 Given a list of authority record identifiers, returns a list of record
304 objects that contain the authority records. The function user may request
305 a specific metadata schema for the record objects.
306
307 Parameters:
308
309   - id (Required)
310     list of authority record identifiers
311   - schema (Optional)
312     specifies the metadata schema of records to be returned, possible values:
313       - MARCXML
314
315 =cut
316
317 sub GetAuthorityRecords {
318     my ($cgi) = @_;
319
320     # If the user asks for an unsupported schema, return an error code
321     if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) {
322         return { code => 'UnsupportedSchema' };
323     }
324
325     my @records;
326
327     # Let's loop over the authority IDs
328     foreach my $authid ( split( / /, $cgi->param('id') ) ) {
329
330         # Get the record as XML string, or error code
331         push @records, GetAuthorityXML($authid) || { code => 'RecordNotFound' };
332     }
333
334     return { record => \@records };
335 }
336
337 =head2 LookupPatron
338
339 Looks up a patron in the ILS by an identifier, and returns the borrowernumber.
340
341 Parameters:
342
343   - id (Required)
344     an identifier used to look up the patron in Koha
345   - id_type (Optional)
346     the type of the identifier, possible values:
347     - cardnumber
348     - userid
349         - email
350     - borrowernumber
351     - firstname
352         - surname
353
354 =cut
355
356 sub LookupPatron {
357     my ($cgi) = @_;
358
359     my $id      = $cgi->param('id');
360     if(!$id) {
361         return { message => 'PatronNotFound' };
362     }
363
364     my $patrons;
365     my $passed_id_type = $cgi->param('id_type');
366     if($passed_id_type) {
367         $patrons = Koha::Patrons->search( { $passed_id_type => $id } );
368     } else {
369         foreach my $id_type ('cardnumber', 'userid', 'email', 'borrowernumber',
370                      'surname', 'firstname') {
371             $patrons = Koha::Patrons->search( { $id_type => $id } );
372             last if($patrons->count);
373         }
374     }
375     unless ( $patrons->count ) {
376         return { message => 'PatronNotFound' };
377     }
378
379     return { id => $patrons->next->borrowernumber };
380 }
381
382 =head2 AuthenticatePatron
383
384 Authenticates a user's login credentials and returns the identifier for
385 the patron.
386
387 Parameters:
388
389   - username (Required)
390     user's login identifier (userid or cardnumber)
391   - password (Required)
392     user's password
393
394 =cut
395
396 sub AuthenticatePatron {
397     my ($cgi) = @_;
398     my $username = $cgi->param('username');
399     my $password = $cgi->param('password');
400     my ($status, $cardnumber, $userid) = C4::Auth::checkpw( C4::Context->dbh, $username, $password );
401     if ( $status ) {
402         # Track the login
403         C4::Auth::track_login_daily( $userid );
404         # Get the borrower
405         my $patron = Koha::Patrons->find( { userid => $userid } );
406         return { id => $patron->borrowernumber };
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             push @checkouts, $issue
533         }
534         $borrower->{'loans'}->{'loan'} = \@checkouts;
535     }
536
537     my $show_attributes = $cgi->param('show_attributes');
538     if ( $show_attributes && $show_attributes eq "1" ) {
539         # FIXME Regression expected here, we do not retrieve the same field as previously
540         # Waiting for answer on bug 14257 comment 15
541         $borrower->{'attributes'} = [
542             map {
543                 $_->type->opac_display
544                   ? {
545                     %{ $_->unblessed },
546                     %{ $_->type->unblessed },
547                     value             => $_->attribute,   # Backward compatibility
548                     value_description => $_->description, # Awkward retro-compability...
549                   }
550                   : ()
551             } $patron->extended_attributes->search
552         ];
553     }
554
555     # Add is expired information
556     $borrower->{'is_expired'} = $patron->is_expired ? 1 : 0;
557
558     return $borrower;
559 }
560
561 =head2 GetPatronStatus
562
563 Returns a patron's status information.
564
565 Parameters:
566
567   - patron_id (Required)
568     the borrower ID
569
570 =cut
571
572 sub GetPatronStatus {
573     my ($cgi) = @_;
574
575     # Get Member details
576     my $borrowernumber = $cgi->param('patron_id');
577     my $patron = Koha::Patrons->find( $borrowernumber );
578     return { code => 'PatronNotFound' } unless $patron;
579
580     # Return the results
581     return {
582         type   => $patron->categorycode,
583         status => 0, # TODO
584         expiry => $patron->dateexpiry,
585     };
586 }
587
588 =head2 GetServices
589
590 Returns information about the services available on a particular item for
591 a particular patron.
592
593 Parameters:
594
595   - patron_id (Required)
596     a borrowernumber
597   - item_id (Required)
598     an itemnumber
599
600 =cut
601
602 sub GetServices {
603     my ($cgi) = @_;
604
605     # Get the member, or return an error code if not found
606     my $borrowernumber = $cgi->param('patron_id');
607     my $patron = Koha::Patrons->find( $borrowernumber );
608     return { code => 'PatronNotFound' } unless $patron;
609
610     my $borrower = $patron->unblessed;
611     # Get the item, or return an error code if not found
612     my $itemnumber = $cgi->param('item_id');
613     my $item = Koha::Items->find($itemnumber);
614     return { code => 'RecordNotFound' } unless $item;
615
616     my @availablefor;
617
618     # Reserve level management
619     my $biblionumber = $item->biblionumber;
620     my $canbookbereserved = CanBookBeReserved( $borrower, $biblionumber );
621     if ($canbookbereserved->{status} eq 'OK') {
622         push @availablefor, 'title level hold';
623         my $canitembereserved = IsAvailableForItemLevelRequest($item, $patron);
624         if ($canitembereserved) {
625             push @availablefor, 'item level hold';
626         }
627     }
628
629     # Reserve cancellation management
630     my $holds = $patron->holds;
631     my @reserveditems;
632     while ( my $hold = $holds->next ) { # FIXME This could be improved
633         push @reserveditems, $hold->itemnumber;
634     }
635     if ( grep { $itemnumber eq $_ } @reserveditems ) {
636         push @availablefor, 'hold cancellation';
637     }
638
639     # Renewal management
640     my @renewal = CanBookBeRenewed( $borrowernumber, $itemnumber );
641     if ( $renewal[0] ) {
642         push @availablefor, 'loan renewal';
643     }
644
645     # Issuing management
646     my $barcode = $item->barcode || '';
647     $barcode = barcodedecode($barcode) if $barcode;
648     if ($barcode) {
649         my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $barcode );
650
651         # TODO push @availablefor, 'loan';
652     }
653
654     my $out;
655     $out->{'AvailableFor'} = \@availablefor;
656
657     return $out;
658 }
659
660 =head2 RenewLoan
661
662 Extends the due date for a borrower's existing issue.
663
664 Parameters:
665
666   - patron_id (Required)
667     a borrowernumber
668   - item_id (Required)
669     an itemnumber
670   - desired_due_date (Required)
671     the date the patron would like the item returned by
672
673 =cut
674
675 sub RenewLoan {
676     my ($cgi) = @_;
677
678     # Get borrower infos or return an error code
679     my $borrowernumber = $cgi->param('patron_id');
680     my $patron = Koha::Patrons->find( $borrowernumber );
681     return { code => 'PatronNotFound' } unless $patron;
682
683     # Get the item, or return an error code
684     my $itemnumber = $cgi->param('item_id');
685     my $item = Koha::Items->find($itemnumber);
686     return { code => 'RecordNotFound' } unless $item;
687
688     # Add renewal if possible
689     my @renewal = CanBookBeRenewed( $borrowernumber, $itemnumber );
690     if ( $renewal[0] ) { AddRenewal( $borrowernumber, $itemnumber, undef, undef, undef, undef, 0 ); }
691
692     my $issue = $item->checkout;
693     return unless $issue; # FIXME should be handled
694
695     # Hashref building
696     my $out;
697     $out->{'renewals'} = $issue->renewals;
698     $out->{date_due}   = dt_from_string($issue->date_due)->strftime('%Y-%m-%d %H:%M');
699     $out->{'success'}  = $renewal[0];
700     $out->{'error'}    = $renewal[1];
701
702     return $out;
703 }
704
705 =head2 HoldTitle
706
707 Creates, for a borrower, a biblio-level hold reserve.
708
709 Parameters:
710
711   - patron_id (Required)
712     a borrowernumber
713   - bib_id (Required)
714     a biblionumber
715   - request_location (Required)
716     IP address where the end user request is being placed
717   - pickup_location (Optional)
718     a branch code indicating the location to which to deliver the item for pickup
719   - start_date (Optional)
720     date after which hold request is no longer needed if the document has not been made available
721   - expiry_date (Optional)
722     date after which item returned to shelf if item is not picked up
723
724 =cut
725
726 sub HoldTitle {
727     my ($cgi) = @_;
728
729     # Get the borrower or return an error code
730     my $borrowernumber = $cgi->param('patron_id');
731     my $patron = Koha::Patrons->find( $borrowernumber );
732     return { code => 'PatronNotFound' } unless $patron;
733
734
735     # If borrower is restricted return an error code
736     return { code => 'PatronRestricted' } if $patron->is_debarred;
737
738     # Check for patron expired, category and syspref settings
739     return { code => 'PatronExpired' } if ($patron->category->effective_BlockExpiredPatronOpacActions && $patron->is_expired);
740
741     # Get the biblio record, or return an error code
742     my $biblionumber = $cgi->param('bib_id');
743     my $biblio = Koha::Biblios->find( $biblionumber );
744     return { code => 'RecordNotFound' } unless $biblio;
745
746     my @hostitems = get_hostitemnumbers_of($biblionumber);
747     my @itemnumbers;
748     if (@hostitems){
749         push(@itemnumbers, @hostitems);
750     }
751
752     my $items = Koha::Items->search({ -or => { biblionumber => $biblionumber, itemnumber => { in => \@itemnumbers } } });
753
754     unless ( $items->count ) {
755         return { code => 'NoItems' };
756     }
757
758     my $title = $biblio ? $biblio->title : '';
759
760     # Check if the biblio can be reserved
761     my $code = CanBookBeReserved( $borrowernumber, $biblionumber )->{status};
762     return { code => $code } unless ( $code eq 'OK' );
763
764     my $branch;
765
766     # Pickup branch management
767     if ( $cgi->param('pickup_location') ) {
768         $branch = $cgi->param('pickup_location');
769         return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch);
770     } else { # if the request provide no branch, use the borrower's branch
771         $branch = $patron->branchcode;
772     }
773
774     my $destination = Koha::Libraries->find($branch);
775     return { code => 'libraryNotPickupLocation' } unless $destination->pickup_location;
776     return { code => 'cannotBeTransferred' } unless $biblio->can_be_transferred({ to => $destination });
777
778     my $resdate;
779     if ( $cgi->param('start_date') ) {
780         $resdate = $cgi->param('start_date');
781     }
782
783     my $expdate;
784     if ( $cgi->param('expiry_date') ) {
785         $expdate = $cgi->param('expiry_date');
786     }
787
788     # Add the reserve
789     #    $branch,    $borrowernumber, $biblionumber,
790     #    $constraint, $bibitems,  $priority, $resdate, $expdate, $notes,
791     #    $title,      $checkitem, $found
792     my $priority= C4::Reserves::CalculatePriority( $biblionumber );
793     AddReserve(
794         {
795             branchcode       => $branch,
796             borrowernumber   => $borrowernumber,
797             biblionumber     => $biblionumber,
798             priority         => $priority,
799             reservation_date => $resdate,
800             expiration_date  => $expdate,
801             title            => $title,
802         }
803     );
804
805     # Hashref building
806     my $out;
807     $out->{'title'}           = $title;
808     my $library = Koha::Libraries->find( $branch );
809     $out->{'pickup_location'} = $library ? $library->branchname : '';
810
811     # TODO $out->{'date_available'}  = '';
812
813     return $out;
814 }
815
816 =head2 HoldItem
817
818 Creates, for a borrower, an item-level hold request on a specific item of
819 a bibliographic record in Koha.
820
821 Parameters:
822
823   - patron_id (Required)
824     a borrowernumber
825   - bib_id (Required)
826     a biblionumber
827   - item_id (Required)
828     an itemnumber
829   - pickup_location (Optional)
830     a branch code indicating the location to which to deliver the item for pickup
831   - start_date (Optional)
832     date after which hold request is no longer needed if the item has not been made available
833   - expiry_date (Optional)
834     date after which item returned to shelf if item is not picked up
835
836 =cut
837
838 sub HoldItem {
839     my ($cgi) = @_;
840
841     # Get the borrower or return an error code
842     my $borrowernumber = $cgi->param('patron_id');
843     my $patron = Koha::Patrons->find( $borrowernumber );
844     return { code => 'PatronNotFound' } unless $patron;
845
846     # If borrower is restricted return an error code
847     return { code => 'PatronRestricted' } if $patron->is_debarred;
848
849     # Check for patron expired, category and syspref settings
850     return { code => 'PatronExpired' } if ($patron->category->effective_BlockExpiredPatronOpacActions && $patron->is_expired);
851
852     # Get the biblio or return an error code
853     my $biblionumber = $cgi->param('bib_id');
854     my $biblio = Koha::Biblios->find( $biblionumber );
855     return { code => 'RecordNotFound' } unless $biblio;
856
857     my $title = $biblio ? $biblio->title : '';
858
859     # Get the item or return an error code
860     my $itemnumber = $cgi->param('item_id');
861     my $item = Koha::Items->find($itemnumber);
862     return { code => 'RecordNotFound' } unless $item;
863
864     # If the biblio does not match the item, return an error code
865     return { code => 'RecordNotFound' } if $item->biblionumber ne $biblio->biblionumber;
866
867     # Pickup branch management
868     my $branch;
869     if ( $cgi->param('pickup_location') ) {
870         $branch = $cgi->param('pickup_location');
871         return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch);
872     } else { # if the request provide no branch, use the borrower's branch
873         $branch = $patron->branchcode;
874     }
875
876     # Check for item disponibility
877     my $canitembereserved = C4::Reserves::CanItemBeReserved( $patron, $item, $branch )->{status};
878     return { code => $canitembereserved } unless $canitembereserved eq 'OK';
879
880     my $resdate;
881     if ( $cgi->param('start_date') ) {
882         $resdate = $cgi->param('start_date');
883     }
884
885     my $expdate;
886     if ( $cgi->param('expiry_date') ) {
887         $expdate = $cgi->param('expiry_date');
888     }
889
890     # Add the reserve
891     my $priority = C4::Reserves::CalculatePriority($biblionumber);
892     AddReserve(
893         {
894             branchcode       => $branch,
895             borrowernumber   => $borrowernumber,
896             biblionumber     => $biblionumber,
897             priority         => $priority,
898             reservation_date => $resdate,
899             expiration_date  => $expdate,
900             title            => $title,
901             itemnumber       => $itemnumber,
902         }
903     );
904
905     # Hashref building
906     my $out;
907     my $library = Koha::Libraries->find( $branch );
908     $out->{'pickup_location'} = $library ? $library->branchname : '';
909
910     # TODO $out->{'date_available'} = '';
911
912     return $out;
913 }
914
915 =head2 CancelHold
916
917 Cancels an active reserve request for the borrower.
918
919 Parameters:
920
921   - patron_id (Required)
922         a borrowernumber
923   - item_id (Required)
924         a reserve_id
925
926 =cut
927
928 sub CancelHold {
929     my ($cgi) = @_;
930
931     # Get the borrower or return an error code
932     my $borrowernumber = $cgi->param('patron_id');
933     my $patron = Koha::Patrons->find( $borrowernumber );
934     return { code => 'PatronNotFound' } unless $patron;
935
936     # Get the reserve or return an error code
937     my $reserve_id = $cgi->param('item_id');
938     my $hold = Koha::Holds->find( $reserve_id );
939     return { code => 'RecordNotFound' } unless $hold;
940     return { code => 'RecordNotFound' } unless ($hold->borrowernumber == $borrowernumber);
941
942     $hold->cancel;
943
944     return { code => 'Canceled' };
945 }
946
947 =head2 _availability
948
949 Returns, for an itemnumber, an array containing availability information.
950
951  my ($biblionumber, $status, $msg, $location) = _availability($id);
952
953 =cut
954
955 sub _availability {
956     my ($itemnumber) = @_;
957     my $item = Koha::Items->find($itemnumber);
958
959     unless ( $item ) {
960         return ( undef, 'unknown', 'Error: could not retrieve availability for this ID', undef );
961     }
962
963     my $biblionumber = $item->biblioitemnumber;
964     my $library = Koha::Libraries->find( $item->holdingbranch );
965     my $location = $library ? $library->branchname : '';
966     my $itemcallnumber = $item->itemcallnumber;
967
968     if ( $item->notforloan ) {
969         return ( $biblionumber, 'not available', 'Not for loan', $location, $itemcallnumber );
970     } elsif ( $item->onloan ) {
971         return ( $biblionumber, 'not available', 'Checked out', $location, $itemcallnumber );
972     } elsif ( $item->itemlost ) {
973         return ( $biblionumber, 'not available', 'Item lost', $location, $itemcallnumber );
974     } elsif ( $item->withdrawn ) {
975         return ( $biblionumber, 'not available', 'Item withdrawn', $location, $itemcallnumber );
976     } elsif ( $item->damaged ) {
977         return ( $biblionumber, 'not available', 'Item damaged', $location, $itemcallnumber );
978     } else {
979         return ( $biblionumber, 'available', undef, $location, $itemcallnumber );
980     }
981 }
982
983 1;