Bug 32030: ERM - Add vendor to license
[koha-ffzg.git] / Koha / Hold.pm
1 package Koha::Hold;
2
3 # Copyright ByWater Solutions 2014
4 # Copyright 2017 Koha Development team
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 use Modern::Perl;
22
23 use List::MoreUtils qw( any );
24
25 use C4::Context qw(preference);
26 use C4::Letters qw( GetPreparedLetter EnqueueLetter );
27 use C4::Log qw( logaction );
28 use C4::Reserves;
29
30 use Koha::AuthorisedValues;
31 use Koha::DateUtils qw( dt_from_string );
32 use Koha::Patrons;
33 use Koha::Biblios;
34 use Koha::Hold::CancellationRequests;
35 use Koha::Items;
36 use Koha::Libraries;
37 use Koha::Old::Holds;
38 use Koha::Calendar;
39 use Koha::Plugins;
40
41 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
42
43 use Koha::Exceptions;
44 use Koha::Exceptions::Hold;
45
46 use base qw(Koha::Object);
47
48 =head1 NAME
49
50 Koha::Hold - Koha Hold object class
51
52 =head1 API
53
54 =head2 Class methods
55
56 =cut
57
58 =head3 age
59
60 returns the number of days since a hold was placed, optionally
61 using the calendar
62
63 my $age = $hold->age( $use_calendar );
64
65 =cut
66
67 sub age {
68     my ( $self, $use_calendar ) = @_;
69
70     my $today = dt_from_string;
71     my $age;
72
73     if ( $use_calendar ) {
74         my $calendar = Koha::Calendar->new( branchcode => $self->branchcode );
75         $age = $calendar->days_between( dt_from_string( $self->reservedate ), $today );
76     }
77     else {
78         $age = $today->delta_days( dt_from_string( $self->reservedate ) );
79     }
80
81     $age = $age->in_units( 'days' );
82
83     return $age;
84 }
85
86 =head3 suspend_hold
87
88 my $hold = $hold->suspend_hold( $suspend_until );
89
90 =cut
91
92 sub suspend_hold {
93     my ( $self, $date ) = @_;
94
95     $date &&= dt_from_string($date)->truncate( to => 'day' )->datetime;
96
97     if ( $self->is_found ) {    # We can't suspend found holds
98         if ( $self->is_waiting ) {
99             Koha::Exceptions::Hold::CannotSuspendFound->throw( status => 'W' );
100         }
101         elsif ( $self->is_in_transit ) {
102             Koha::Exceptions::Hold::CannotSuspendFound->throw( status => 'T' );
103         }
104         elsif ( $self->is_in_processing ) {
105             Koha::Exceptions::Hold::CannotSuspendFound->throw( status => 'P' );
106         }
107         else {
108             Koha::Exceptions::Hold::CannotSuspendFound->throw(
109                       'Unhandled data exception on found hold (id='
110                     . $self->id
111                     . ', found='
112                     . $self->found
113                     . ')' );
114         }
115     }
116
117     $self->suspend(1);
118     $self->suspend_until($date);
119     $self->store();
120
121     Koha::Plugins->call(
122         'after_hold_action',
123         {
124             action  => 'suspend',
125             payload => { hold => $self->get_from_storage }
126         }
127     );
128
129     logaction( 'HOLDS', 'SUSPEND', $self->reserve_id, $self )
130         if C4::Context->preference('HoldsLog');
131
132     Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
133         {
134             biblio_ids => [ $self->biblionumber ]
135         }
136     ) if C4::Context->preference('RealTimeHoldsQueue');
137
138     return $self;
139 }
140
141 =head3 resume
142
143 my $hold = $hold->resume();
144
145 =cut
146
147 sub resume {
148     my ( $self ) = @_;
149
150     $self->suspend(0);
151     $self->suspend_until( undef );
152
153     $self->store();
154
155     Koha::Plugins->call(
156         'after_hold_action',
157         {
158             action  => 'resume',
159             payload => { hold => $self->get_from_storage }
160         }
161     );
162
163     logaction( 'HOLDS', 'RESUME', $self->reserve_id, $self )
164         if C4::Context->preference('HoldsLog');
165
166     Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
167         {
168             biblio_ids => [ $self->biblionumber ]
169         }
170     ) if C4::Context->preference('RealTimeHoldsQueue');
171
172     return $self;
173 }
174
175 =head3 delete
176
177 $hold->delete();
178
179 =cut
180
181 sub delete {
182     my ( $self ) = @_;
183
184     my $deleted = $self->SUPER::delete($self);
185
186     logaction( 'HOLDS', 'DELETE', $self->reserve_id, $self )
187         if C4::Context->preference('HoldsLog');
188
189     return $deleted;
190 }
191
192 =head3 set_transfer
193
194 =cut
195
196 sub set_transfer {
197     my ( $self ) = @_;
198
199     $self->priority(0);
200     $self->found('T');
201     $self->store();
202
203     return $self;
204 }
205
206 =head3 set_waiting
207
208 =cut
209
210 sub set_waiting {
211     my ( $self, $desk_id ) = @_;
212
213     $self->priority(0);
214
215     my $today = dt_from_string();
216
217     my $values = {
218         found => 'W',
219         ( !$self->waitingdate ? ( waitingdate => $today->ymd ) : () ),
220         desk_id => $desk_id,
221     };
222
223     my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
224     my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
225
226     my $new_expiration_date = $today->clone->add(days => $max_pickup_delay);
227
228     if ( C4::Context->preference("ExcludeHolidaysFromMaxPickUpDelay") ) {
229         my $itemtype = $self->item ? $self->item->effective_itemtype : $self->biblio->itemtype;
230         my $daysmode = Koha::CirculationRules->get_effective_daysmode(
231             {
232                 categorycode => $self->borrower->categorycode,
233                 itemtype     => $itemtype,
234                 branchcode   => $self->branchcode,
235             }
236         );
237         my $calendar = Koha::Calendar->new( branchcode => $self->branchcode, days_mode => $daysmode );
238
239         $new_expiration_date = $calendar->days_forward( dt_from_string(), $max_pickup_delay );
240     }
241
242     # If patron's requested expiration date is prior to the
243     # calculated one, we keep the patron's one.
244     if ( $self->patron_expiration_date ) {
245         my $requested_expiration = dt_from_string( $self->patron_expiration_date );
246
247         my $cmp =
248           $requested_expiration
249           ? DateTime->compare( $requested_expiration, $new_expiration_date )
250           : 0;
251
252         $new_expiration_date =
253           $cmp == -1 ? $requested_expiration : $new_expiration_date;
254     }
255
256     $values->{expirationdate} = $new_expiration_date->ymd;
257
258     $self->set($values)->store();
259
260     return $self;
261 }
262
263 =head3 is_pickup_location_valid
264
265     if ($hold->is_pickup_location_valid({ library_id => $library->id }) ) {
266         ...
267     }
268
269 Returns a I<boolean> representing if the passed pickup location is valid for the hold.
270 It throws a I<Koha::Exceptions::_MissingParameter> if the library_id parameter is not
271 passed.
272
273 =cut
274
275 sub is_pickup_location_valid {
276     my ( $self, $params ) = @_;
277
278     Koha::Exceptions::MissingParameter->throw('The library_id parameter is mandatory')
279         unless $params->{library_id};
280
281     my $pickup_locations;
282
283     if ( $self->itemnumber ) { # item-level
284         $pickup_locations = $self->item->pickup_locations({ patron => $self->patron });
285     }
286     else { # biblio-level
287         $pickup_locations = $self->biblio->pickup_locations({ patron => $self->patron });
288     }
289
290     return any { $_->branchcode eq $params->{library_id} } $pickup_locations->as_list;
291 }
292
293 =head3 set_pickup_location
294
295     $hold->set_pickup_location(
296         {
297             library_id => $library->id,
298           [ force   => 0|1 ]
299         }
300     );
301
302 Updates the hold pickup location. It throws a I<Koha::Exceptions::Hold::InvalidPickupLocation> if
303 the passed pickup location is not valid.
304
305 Note: It is up to the caller to verify if I<AllowHoldPolicyOverride> is set when setting the
306 B<force> parameter.
307
308 =cut
309
310 sub set_pickup_location {
311     my ( $self, $params ) = @_;
312
313     Koha::Exceptions::MissingParameter->throw('The library_id parameter is mandatory')
314         unless $params->{library_id};
315
316     if (
317         $params->{force}
318         || $self->is_pickup_location_valid(
319             { library_id => $params->{library_id} }
320         )
321       )
322     {
323         # all good, set the new pickup location
324         $self->branchcode( $params->{library_id} )->store;
325     }
326     else {
327         Koha::Exceptions::Hold::InvalidPickupLocation->throw;
328     }
329
330     return $self;
331 }
332
333 =head3 set_processing
334
335 $hold->set_processing;
336
337 Mark the hold as in processing.
338
339 =cut
340
341 sub set_processing {
342     my ( $self ) = @_;
343
344     $self->priority(0);
345     $self->found('P');
346     $self->store();
347
348     return $self;
349 }
350
351 =head3 is_found
352
353 Returns true if hold is waiting, in transit or in processing
354
355 =cut
356
357 sub is_found {
358     my ($self) = @_;
359
360     return 0 unless $self->found();
361     return 1 if $self->found() eq 'W';
362     return 1 if $self->found() eq 'T';
363     return 1 if $self->found() eq 'P';
364 }
365
366 =head3 is_waiting
367
368 Returns true if hold is a waiting hold
369
370 =cut
371
372 sub is_waiting {
373     my ($self) = @_;
374
375     my $found = $self->found;
376     return $found && $found eq 'W';
377 }
378
379 =head3 is_in_transit
380
381 Returns true if hold is a in_transit hold
382
383 =cut
384
385 sub is_in_transit {
386     my ($self) = @_;
387
388     return 0 unless $self->found();
389     return $self->found() eq 'T';
390 }
391
392 =head3 is_in_processing
393
394 Returns true if hold is a in_processing hold
395
396 =cut
397
398 sub is_in_processing {
399     my ($self) = @_;
400
401     return 0 unless $self->found();
402     return $self->found() eq 'P';
403 }
404
405 =head3 is_cancelable_from_opac
406
407 Returns true if hold is a cancelable hold
408
409 Holds may be only canceled if they are not found.
410
411 This is used from the OPAC.
412
413 =cut
414
415 sub is_cancelable_from_opac {
416     my ($self) = @_;
417
418     return 1 unless $self->is_found();
419     return 0; # if ->is_in_transit or if ->is_waiting or ->is_in_processing
420 }
421
422 =head3 cancellation_requestable_from_opac
423
424     if ( $hold->cancellation_requestable_from_opac ) { ... }
425
426 Returns a I<boolean> representing if a cancellation request can be placed on the hold
427 from the OPAC. It targets holds that cannot be cancelled from the OPAC (see the
428 B<is_cancelable_from_opac> method above), but for which circulation rules allow
429 requesting cancellation.
430
431 Throws a B<Koha::Exceptions::InvalidStatus> exception with the following I<invalid_status>
432 values:
433
434 =over 4
435
436 =item B<'hold_not_waiting'>: the hold is expected to be waiting and it is not.
437
438 =item B<'no_item_linked'>: the waiting hold doesn't have an item properly linked.
439
440 =back
441
442 =cut
443
444 sub cancellation_requestable_from_opac {
445     my ( $self ) = @_;
446
447     Koha::Exceptions::InvalidStatus->throw( invalid_status => 'hold_not_waiting' )
448       unless $self->is_waiting;
449
450     my $item = $self->item;
451
452     Koha::Exceptions::InvalidStatus->throw( invalid_status => 'no_item_linked' )
453       unless $item;
454
455     my $patron = $self->patron;
456
457     my $controlbranch = $patron->branchcode;
458
459     if ( C4::Context->preference('ReservesControlBranch') eq 'ItemHomeLibrary' ) {
460         $controlbranch = $item->homebranch;
461     }
462
463     return Koha::CirculationRules->get_effective_rule_value(
464         {
465             categorycode => $patron->categorycode,
466             itemtype     => $item->itype,
467             branchcode   => $controlbranch,
468             rule_name    => 'waiting_hold_cancellation',
469         }
470     ) ? 1 : 0;
471 }
472
473 =head3 is_at_destination
474
475 Returns true if hold is waiting
476 and the hold's pickup branch matches
477 the hold item's holding branch
478
479 =cut
480
481 sub is_at_destination {
482     my ($self) = @_;
483
484     return $self->is_waiting() && ( $self->branchcode() eq $self->item()->holdingbranch() );
485 }
486
487 =head3 biblio
488
489 Returns the related Koha::Biblio object for this hold
490
491 =cut
492
493 sub biblio {
494     my ($self) = @_;
495
496     $self->{_biblio} ||= Koha::Biblios->find( $self->biblionumber() );
497
498     return $self->{_biblio};
499 }
500
501 =head3 patron
502
503 Returns the related Koha::Patron object for this hold
504
505 =cut
506
507 sub patron {
508     my ($self) = @_;
509
510     my $patron_rs = $self->_result->patron;
511     return Koha::Patron->_new_from_dbic($patron_rs);
512 }
513
514 =head3 item
515
516 Returns the related Koha::Item object for this Hold
517
518 =cut
519
520 sub item {
521     my ($self) = @_;
522
523     $self->{_item} ||= Koha::Items->find( $self->itemnumber() );
524
525     return $self->{_item};
526 }
527
528 =head3 item_group
529
530 Returns the related Koha::Biblio::ItemGroup object for this Hold
531
532 =cut
533
534 sub item_group {
535     my ($self) = @_;
536
537     my $item_group_rs = $self->_result->item_group;
538     return unless $item_group_rs;
539     return Koha::Biblio::ItemGroup->_new_from_dbic($item_group_rs);
540 }
541
542 =head3 branch
543
544 Returns the related Koha::Library object for this Hold
545
546 =cut
547
548 sub branch {
549     my ($self) = @_;
550
551     $self->{_branch} ||= Koha::Libraries->find( $self->branchcode() );
552
553     return $self->{_branch};
554 }
555
556 =head3 desk
557
558 Returns the related Koha::Desk object for this Hold
559
560 =cut
561
562 sub desk {
563     my $self = shift;
564     my $desk_rs = $self->_result->desk;
565     return unless $desk_rs;
566     return Koha::Desk->_new_from_dbic($desk_rs);
567 }
568
569 =head3 borrower
570
571 Returns the related Koha::Patron object for this Hold
572
573 =cut
574
575 # FIXME Should be renamed with ->patron
576 sub borrower {
577     my ($self) = @_;
578
579     $self->{_borrower} ||= Koha::Patrons->find( $self->borrowernumber() );
580
581     return $self->{_borrower};
582 }
583
584 =head3 is_suspended
585
586 my $bool = $hold->is_suspended();
587
588 =cut
589
590 sub is_suspended {
591     my ( $self ) = @_;
592
593     return $self->suspend();
594 }
595
596 =head3 add_cancellation_request
597
598     my $cancellation_request = $hold->add_cancellation_request({ [ creation_date => $creation_date ] });
599
600 Adds a cancellation request to the hold. Returns the generated
601 I<Koha::Hold::CancellationRequest> object.
602
603 =cut
604
605 sub add_cancellation_request {
606     my ( $self, $params ) = @_;
607
608     my $request = Koha::Hold::CancellationRequest->new(
609         {   hold_id      => $self->id,
610             ( $params->{creation_date} ? ( creation_date => $params->{creation_date} ) : () ),
611         }
612     )->store;
613
614     $request->discard_changes;
615
616     return $request;
617 }
618
619 =head3 cancellation_requests
620
621     my $cancellation_requests = $hold->cancellation_requests;
622
623 Returns related a I<Koha::Hold::CancellationRequests> resultset.
624
625 =cut
626
627 sub cancellation_requests {
628     my ($self) = @_;
629
630     return Koha::Hold::CancellationRequests->search( { hold_id => $self->id } );
631 }
632
633 =head3 cancel
634
635 my $cancel_hold = $hold->cancel(
636     {
637         [ charge_cancel_fee   => 1||0, ]
638         [ cancellation_reason => $cancellation_reason, ]
639         [ skip_holds_queue    => 1||0 ]
640     }
641 );
642
643 Cancel a hold:
644 - The hold will be moved to the old_reserves table with a priority=0
645 - The priority of other holds will be updated
646 - The patron will be charge (see ExpireReservesMaxPickUpDelayCharge) if the charge_cancel_fee parameter is set
647 - The canceled hold will have the cancellation reason added to old_reserves.cancellation_reason if one is passed in
648 - a CANCEL HOLDS log will be done if the pref HoldsLog is on
649
650 =cut
651
652 sub cancel {
653     my ( $self, $params ) = @_;
654
655     my $autofill_next = $params->{autofill} && $self->itemnumber && $self->found && $self->found eq 'W';
656
657     $self->_result->result_source->schema->txn_do(
658         sub {
659             my $patron = $self->patron;
660
661             $self->cancellationdate( dt_from_string->strftime( '%Y-%m-%d %H:%M:%S' ) );
662             $self->priority(0);
663             $self->cancellation_reason( $params->{cancellation_reason} );
664             $self->store();
665
666             if ( $params->{cancellation_reason} ) {
667                 my $letter = C4::Letters::GetPreparedLetter(
668                     module                 => 'reserves',
669                     letter_code            => 'HOLD_CANCELLATION',
670                     message_transport_type => 'email',
671                     branchcode             => $self->borrower->branchcode,
672                     lang                   => $self->borrower->lang,
673                     tables => {
674                         branches    => $self->borrower->branchcode,
675                         borrowers   => $self->borrowernumber,
676                         items       => $self->itemnumber,
677                         biblio      => $self->biblionumber,
678                         biblioitems => $self->biblionumber,
679                         reserves    => $self->unblessed,
680                     }
681                 );
682
683                 if ($letter) {
684                     C4::Letters::EnqueueLetter(
685                         {
686                             letter                   => $letter,
687                             borrowernumber         => $self->borrowernumber,
688                             message_transport_type => 'email',
689                         }
690                     );
691                 }
692             }
693
694             my $old_me = $self->_move_to_old;
695
696             Koha::Plugins->call(
697                 'after_hold_action',
698                 {
699                     action  => 'cancel',
700                     payload => { hold => $old_me->get_from_storage }
701                 }
702             );
703
704             # anonymize if required
705             $old_me->anonymize
706                 if $patron->privacy == 2;
707
708             $self->SUPER::delete(); # Do not add a DELETE log
709             # now fix the priority on the others....
710             C4::Reserves::_FixPriority({ biblionumber => $self->biblionumber });
711
712             # and, if desired, charge a cancel fee
713             my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
714             if ( $charge && $params->{'charge_cancel_fee'} ) {
715                 my $account =
716                   Koha::Account->new( { patron_id => $self->borrowernumber } );
717                 $account->add_debit(
718                     {
719                         amount     => $charge,
720                         user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
721                         interface  => C4::Context->interface,
722                         library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
723                         type       => 'RESERVE_EXPIRED',
724                         item_id    => $self->itemnumber
725                     }
726                 );
727             }
728
729             C4::Log::logaction( 'HOLDS', 'CANCEL', $self->reserve_id, $self )
730                 if C4::Context->preference('HoldsLog');
731
732             Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
733                 {
734                     biblio_ids => [ $old_me->biblionumber ]
735                 }
736             ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
737         }
738     );
739
740     if ($autofill_next) {
741         my ( undef, $next_hold ) = C4::Reserves::CheckReserves( $self->itemnumber );
742         if ($next_hold) {
743             my $is_transfer = $self->branchcode ne $next_hold->{branchcode};
744
745             C4::Reserves::ModReserveAffect( $self->itemnumber, $self->borrowernumber, $is_transfer, $next_hold->{reserve_id}, $self->desk_id, $autofill_next );
746             C4::Items::ModItemTransfer( $self->itemnumber, $self->branchcode, $next_hold->{branchcode}, "Reserve" ) if $is_transfer;
747         }
748     }
749
750     return $self;
751 }
752
753 =head3 fill
754
755     $hold->fill;
756
757 This method marks the hold as filled. It effectively moves it to old_reserves.
758
759 =cut
760
761 sub fill {
762     my ( $self ) = @_;
763     $self->_result->result_source->schema->txn_do(
764         sub {
765             my $patron = $self->patron;
766
767             $self->set(
768                 {
769                     found    => 'F',
770                     priority => 0,
771                 }
772             );
773
774             my $old_me = $self->_move_to_old;
775
776             Koha::Plugins->call(
777                 'after_hold_action',
778                 {
779                     action  => 'fill',
780                     payload => { hold => $old_me->get_from_storage }
781                 }
782             );
783
784             # anonymize if required
785             $old_me->anonymize
786                 if $patron->privacy == 2;
787
788             $self->SUPER::delete(); # Do not add a DELETE log
789
790             # now fix the priority on the others....
791             C4::Reserves::_FixPriority({ biblionumber => $self->biblionumber });
792
793             if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
794                 my $fee = $patron->category->reservefee // 0;
795                 if ( $fee > 0 ) {
796                     $patron->account->add_debit(
797                         {
798                             amount       => $fee,
799                             description  => $self->biblio->title,
800                             user_id      => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
801                             library_id   => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
802                             interface    => C4::Context->interface,
803                             type         => 'RESERVE',
804                             item_id      => $self->itemnumber
805                         }
806                     );
807                 }
808             }
809
810             C4::Log::logaction( 'HOLDS', 'FILL', $self->id, $self )
811                 if C4::Context->preference('HoldsLog');
812
813             Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
814                 {
815                     biblio_ids => [ $old_me->biblionumber ]
816                 }
817             ) if C4::Context->preference('RealTimeHoldsQueue');
818         }
819     );
820     return $self;
821 }
822
823 =head3 store
824
825 Override base store method to set default
826 expirationdate for holds.
827
828 =cut
829
830 sub store {
831     my ($self) = @_;
832
833     Koha::Exceptions::Hold::MissingPickupLocation->throw() unless $self->branchcode;
834
835     if ( !$self->in_storage ) {
836         if ( ! $self->expirationdate && $self->patron_expiration_date ) {
837             $self->expirationdate($self->patron_expiration_date);
838         }
839
840         if (
841             C4::Context->preference('DefaultHoldExpirationdate')
842                 && !$self->expirationdate
843           )
844         {
845             $self->_set_default_expirationdate;
846         }
847     }
848     else {
849
850         my %updated_columns = $self->_result->get_dirty_columns;
851         return $self->SUPER::store unless %updated_columns;
852
853         if ( exists $updated_columns{reservedate} ) {
854             if (
855                 C4::Context->preference('DefaultHoldExpirationdate')
856                 && ! exists $updated_columns{expirationdate}
857               )
858             {
859                 $self->_set_default_expirationdate;
860             }
861         }
862     }
863
864     $self = $self->SUPER::store;
865 }
866
867 sub _set_default_expirationdate {
868     my $self = shift;
869
870     my $period = C4::Context->preference('DefaultHoldExpirationdatePeriod') || 0;
871     my $timeunit =
872       C4::Context->preference('DefaultHoldExpirationdateUnitOfTime') || 'days';
873
874     $self->expirationdate(
875         dt_from_string( $self->reservedate )->add( $timeunit => $period ) );
876 }
877
878 =head3 _move_to_old
879
880 my $is_moved = $hold->_move_to_old;
881
882 Move a hold to the old_reserve table following the same pattern as Koha::Patron->move_to_deleted
883
884 =cut
885
886 sub _move_to_old {
887     my ($self) = @_;
888     my $hold_infos = $self->unblessed;
889     return Koha::Old::Hold->new( $hold_infos )->store;
890 }
891
892 =head3 to_api_mapping
893
894 This method returns the mapping for representing a Koha::Hold object
895 on the API.
896
897 =cut
898
899 sub to_api_mapping {
900     return {
901         reserve_id       => 'hold_id',
902         borrowernumber   => 'patron_id',
903         reservedate      => 'hold_date',
904         biblionumber     => 'biblio_id',
905         branchcode       => 'pickup_library_id',
906         notificationdate => undef,
907         reminderdate     => undef,
908         cancellationdate => 'cancellation_date',
909         reservenotes     => 'notes',
910         found            => 'status',
911         itemnumber       => 'item_id',
912         waitingdate      => 'waiting_date',
913         expirationdate   => 'expiration_date',
914         patron_expiration_date => undef,
915         lowestPriority   => 'lowest_priority',
916         suspend          => 'suspended',
917         suspend_until    => 'suspended_until',
918         itemtype         => 'item_type',
919         item_level_hold  => 'item_level',
920     };
921 }
922
923 =head3 can_update_pickup_location_opac
924
925     my $can_update_pickup_location_opac = $hold->can_update_pickup_location_opac;
926
927 Returns if a hold can change pickup location from opac
928
929 =cut
930
931 sub can_update_pickup_location_opac {
932     my ($self) = @_;
933
934     my @statuses = split /,/, C4::Context->preference("OPACAllowUserToChangeBranch");
935     foreach my $status ( @statuses ){
936         return 1 if ($status eq 'pending' && !$self->is_found && !$self->is_suspended );
937         return 1 if ($status eq 'intransit' && $self->is_in_transit);
938         return 1 if ($status eq 'suspended' && $self->is_suspended);
939     }
940     return 0;
941 }
942
943 =head2 Internal methods
944
945 =head3 _type
946
947 =cut
948
949 sub _type {
950     return 'Reserve';
951 }
952
953 =head1 AUTHORS
954
955 Kyle M Hall <kyle@bywatersolutions.com>
956 Jonathan Druart <jonathan.druart@bugs.koha-community.org>
957 Martin Renvoize <martin.renvoize@ptfs-europe.com>
958
959 =cut
960
961 1;