View | Details | Raw Unified | Return to bug 21327
Collapse All | Expand All

(-)a/Core/Circulation.pm (-1 / +912 lines)
Line 0 Link Here
0
- 
1
package Core::Circulation;
2
3
use strict;
4
use warnings;
5
6
use Carp;
7
use Data::Dumper;
8
use Try::Tiny;
9
10
use Koha::Items;
11
use Koha::Account;
12
13
use Core::Exceptions;
14
use parent qw(Core::Main Core::Prefs);
15
16
=head
17
  Constructor
18
    object args:
19
      item    : item object
20
      library : library object
21
      patron  : patron object
22
      prefs   : preferences override hashmap
23
    object accessors:
24
      checkout : checkout object
25
      messages : legacy messages hashmap (for ui/SIP)
26
      iteminfo : legacy iteminfo hashmap (for ui/SIP)
27
=cut
28
sub new {
29
  my ($class, $args) = @_;
30
  ($args->{item} and $args->{library}) or die "Missing required arg item or library";
31
32
  my $self = $class->SUPER::new();
33
  $self = { %$self,               # Inherent anything from parent
34
    error    => undef,            # Error return
35
    item     => $args->{item},    # Mandatory
36
    library  => $args->{library}, # Mandatory
37
    patron   => $args->{patron},  # Mandatory for checkouts
38
  };
39
  $self->{library} or die "Should not proceed with circulation without library branch!";
40
41
  # # Default prefs with overrides from args
42
  $self->{prefs} = Core::Prefs->new($args->{prefs}); # TODO: Is this cached? If not, we should
43
  $self->{checkout} = Koha::Checkouts->find({itemnumber => $self->{item}->itemnumber});
44
  $self->{messages} = {};
45
  $self->{iteminfo} = $self->{item}->unblessed;
46
47
  bless $self, $class;
48
49
  return $self;
50
}
51
52
# Dummy override for testing
53
sub testOverride {
54
  print "TEST: Core::Circulation called without overrides";
55
}
56
57
=head
58
  Checkin
59
    optional args: returnDate
60
=cut
61
sub Checkin {
62
  my ($self, $returnDate) = @_;
63
  warn "Checking in item " . $self->{item}->itemnumber . "\n";
64
  my $t0 = Time::HiRes::time();
65
  $returnDate ||= _today();
66
67
  $self->testOverride();
68
69
  # CHECKOUT
70
  if ($self->{checkout}) {
71
    $self->{patron} = $self->{checkout}->patron;
72
    $self->{iteminfo} = { %{$self->{iteminfo}}, %{$self->{checkout}->unblessed} };
73
    $self->{iteminfo}->{overdue} = $self->{checkout}->is_overdue;
74
75
    if ($self->{item}->itemlost) {
76
      $self->{messages}->{WasLost} = 1;
77
      if (Koha::RefundLostItemFeeRules->should_refund({
78
          current_branch      => $self->{library}->branchcode,
79
          item_home_branch    => $self->{item}->homebranch,
80
          item_holding_branch => $self->{item}->holdingbranch,
81
      })) {
82
          $self->fixAccountForLostAndReturned();
83
          $self->{messages}->{Itemlost} = $self->{item}->barcode;
84
          $self->{messages}->{LostItemFeeRefunded} = 1;
85
      }
86
    }
87
    $self->fixOverduesOnReturn();
88
  } else { # NOT CHECKED OUT, JUST FIX STATUSES, TRANSFERS AND RETURN
89
    $self->{error} = Core::Exception::Circulation::CheckinError->new(error => "Unable to checkin\n");
90
    if ($self->{item}->withdrawn) {
91
        $self->{messages}->{Withdrawn} = $self->{item}->barcode;
92
    } else {
93
        $self->{messages}->{NotIssued} = $self->{item}->barcode;
94
    }
95
  }
96
97
  # TRANSFER
98
  my $transfer = $self->{item}->get_transfer;
99
  if ($transfer) {
100
    if ($transfer->tobranch eq $self->{library}->branchcode) {
101
      # ARRIVED AT RIGHT BRANCH
102
      $transfer->datearrived(_today())->store;
103
    } else {
104
      # WRONG BRANCH - Should we fix transfers at once?
105
      $self->{messages}->{WrongTransfer}     = $transfer->tobranch;
106
      $self->{messages}->{WrongTransferItem} = $self->{item}->itemnumber;
107
    }
108
  }
109
110
  # HOLD
111
  my $hold = $self->getPendingHold;
112
  if ($hold) {
113
    if ($hold->branchcode ne $self->{library}->branchcode) {
114
        $self->{messages}->{NeedsTransfer} = $hold->branchcode;
115
    }
116
    # Weird message return yes, but this is how Koha templates expect it
117
    $self->{messages}->{ResFound} = {
118
        %{$hold->unblessed},
119
        ResFound => sub {$hold->is_waiting ? return "Waiting" : return "Reserved"},
120
    };
121
  }
122
123
  # WRONG BRANCH - CREATE TRANSFER IF NOT ONE ALREADY
124
  if ($self->{library}->branchcode ne $self->{item}->homebranch and not $transfer) {
125
      Koha::Item::Transfer->new({
126
          itemnumber => $self->{item}->itemnumber,
127
          frombranch => $self->{library}->branchcode,
128
          tobranch   => $self->{item}->homebranch,
129
          datesent   => _today(),
130
      })->store;
131
      $self->{messages}->{WasTransfered} = 1;
132
      # IF AutomaticItemReturn false - set NeedsTransfer
133
      $self->{prefs}->{AutomaticItemReturn} and $self->{messages}->{NeedsTransfer} = $self->{item}->homebranch;
134
  }
135
136
  # TODO: Rewrite to Core?
137
  C4::Stats::UpdateStats({
138
      branch         => $self->{library}->branchcode,
139
      type           => "return",
140
      itemnumber     => $self->{item}->itemnumber,
141
      itemtype       => $self->{item}->itype,
142
      borrowernumber => $self->{patron} ? $self->{patron}->borrowernumber : undef,
143
      ccode          => $self->{item}->ccode,
144
  });
145
146
  # TODO: Borrower message (SendCirculationAlert) or HANDLE IN PrintSlip ?
147
  $self->sendCirculationAlert();
148
149
  $self->removeOverdueDebarments();
150
151
  # RETURN
152
  if ($self->{checkout}) {
153
      $self->{checkout}->returndate($returnDate)->store;
154
      if ($self->{checkout}->patron->privacy == 2) {
155
          $self->{checkout}->borrowernumber(undef)->store;
156
      } else {
157
          $self->{patron} and $self->{item}->last_returned_by( $self->{patron} );
158
      }
159
      # MERGE TABLES - PLEASE
160
      my $old_checkout = Koha::Old::Checkout->new($self->{checkout}->unblessed)->store;
161
      $self->{checkout}->delete;
162
      # TODO: Needs to decide if $self->{checkout} should be emptied upon checkout or not
163
      # As templates require checkout object we must leave it for now
164
      # undef $self->{checkout};
165
      $self->{messages}->{WasReturned} = 1;
166
  } else {
167
      $self->{error} = Core::Exception::Circulation::CheckinError->new(error => "Unable to checkin\n");
168
      $self->{messages}->{WasReturned}   = 0;
169
      $self->{messages}->{DataCorrupted} = 1;
170
  }
171
172
  # TODO: rewrite all methods to overridable circulation methods
173
  # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
174
  my $returnbranch = $self->getBranchItemRule();
175
  my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $self->{item}->itemnumber );
176
177
  if (!$is_in_rotating_collection && $self->{messages}->{NotIssued} and !$hold and ($self->{item}->homebranch ne $returnbranch) and not $self->{messages}->{WrongTransfer}) {
178
      if ( $self->{prefs}->{AutomaticItemReturn} or ( $self->{prefs}->{UseBranchTransferLimits}
179
            and ! C4::Circulation::IsBranchTransferAllowed($self->{item}->homebranch, $returnbranch, $self->{item}->{$self->{prefs}->{BranchTransferLimitsType}} )
180
         )) {
181
          # AutomaticItemReturn: transfer to returnbranch
182
          $transfer and $transfer->set({
183
            datearrived => \"NOW()",
184
            comments => undef,
185
          });
186
          Koha::Item::Transfer->new({
187
            itemnumber => $self->{item}->itemnumber,
188
            frombranch => $self->{library}->branchcode,
189
            tobranch   => $returnbranch,
190
            datesent   => _today(),
191
          })->store;
192
          $self->{prefs}->{ReturnToShelvingCart} and $self->cartToShelf();
193
          $self->{messages}->{WasTransfered} = 1;
194
      } else {
195
          $self->{messages}->{NeedsTransfer} = $returnbranch;
196
      }
197
  }
198
199
  # UPDATE ITEM
200
  $self->{item}->set({
201
      holdingbranch => $returnbranch ? $returnbranch : $self->{library}->branchcode,
202
      onloan        => undef,
203
      datelastseen  => _today(),
204
      itemlost      => 0,
205
  })->store;
206
207
  my $dt = Time::HiRes::time() - $t0;
208
  printf STDERR "[%s] Checkin called : %s - elapsed %.3fs\n", ref $self, scalar(gmtime()), $dt;
209
  return $self;
210
}
211
212
# IN -> $self, $dateDue[, $cancelReserve, $issueDate, $sipMode]
213
sub Checkout {
214
  my ($self, $dateDue, $cancelReserve, $issueDate, $sipMode, $params) = @_; # Surely most params can be skipped here
215
  $self->{patron} and $self->{library} or Core::Exception::Circulation::CheckinError->throw(error => "No checkout without patron and/or library!\n");
216
217
  # extra params
218
  my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
219
  my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
220
  my $auto_renew = $params && $params->{auto_renew};
221
222
  my $t0 = Time::HiRes::time();
223
  warn "Checking out item " . $self->{item}->barcode . " for patron " . $self->{patron}->borrowernumber . "\n";
224
  $issueDate ||= _today();
225
226
  # PREFS THAT APPLY
227
  # AllowReturnToBranch
228
  my $circControlBranch = $self->getCircControlBranch();
229
230
  # ALREADY CHECKED OUT?
231
  if ($self->{checkout}) {
232
      if ($self->{checkout}->patron->borrowernumber eq $self->{patron}->borrowernumber) {
233
          if ($self->canItemBeRenewed() and not $switch_onsite_checkout) {
234
            $self->Renew( $dateDue );
235
          }
236
      } else {
237
          # Wrong checkout, return first?
238
          if ($self->canItemBeReturned() and not $switch_onsite_checkout) {
239
            $self->Checkin();
240
          }
241
      }
242
  } else {
243
244
    # TRANSFERS AND HOLDS
245
    my $transfer = $self->{item}->get_transfer;
246
    if ($transfer) {
247
      if ($transfer->tobranch eq $self->{library}->branchcode) {
248
        $transfer->datearrived(_today())->store; # Finish transfer
249
      } else {
250
        $transfer->tobranch($self->{library}->branchcode)->store; # Update transfer
251
      }
252
    }
253
254
    # TODO: C4::Circulation.pm:1317
255
    # MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve );
256
    #   -> C4::Reserves.pm:1712
257
    #   -> CheckReserves C4::Reserves.pm:626
258
259
    # HOLD
260
    my $hold = $self->getPendingHold();
261
    if ($hold) {
262
        # Right patron - fill hold
263
        if ($hold->borrowernumber == $self->{patron}->borrowernumber) {
264
            $hold->set({found => "F", priority => 0});
265
            # Merge tables - PLEASE!
266
            Koha::Old::Hold->new( $hold->unblessed() )->store();
267
            $hold->delete();
268
        } else {
269
            # TODO: Checkout should be aborted here?
270
            $self->{error} = Core::Exception::Circulation::CheckoutError->new("Unable to checkout - hold for another patron!");
271
        }
272
    }
273
274
    # CIRCULATION RULES
275
    my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule({
276
        categorycode => $self->{patron}->{categorycode},
277
        itemtype     => $self->{item}->itype,
278
        branchcode   => $self->{library}->branchcode,
279
    });
280
281
    # CHECKOUT
282
    # TODO: rewrite calculate dateDue to overridable module
283
    unless ($dateDue) {
284
        my $itype = $self->{item}->effective_itemtype;
285
        $dateDue = C4::Circulation::CalcDateDue( $issueDate, $itype, $self->{library}->branchcode, $self->{patron}->unblessed, 1 );
286
287
    }
288
    $dateDue->truncate( to => "minute" );
289
290
    # TODO: auto_renew and onsite_checkout
291
    $self->{checkout} = Koha::Checkout->new({
292
        borrowernumber => $self->{patron}->borrowernumber,
293
        itemnumber     => $self->{item}->itemnumber,
294
        issuedate      => $issueDate,
295
        date_due       => $dateDue,
296
        branchcode     => $self->{library}->branchcode,
297
    })->store;
298
299
    # Why was totalissues never set?
300
    my $totalissues = $self->{item}->biblioitem->totalissues // 0;
301
    $self->{item}->biblioitem->totalissues($totalissues + 1);
302
303
    # LOST ITEM REFUND
304
    if ($self->{item}->itemlost) {
305
        # REFUND LOST ITEM FEE
306
        if (Koha::RefundLostItemFeeRules->should_refund({
307
            current_branch      => $self->{library}->branchcode,
308
            item_home_branch    => $self->{item}->homebranch,
309
            item_holding_branch => $self->{item}->holdingbranch,
310
        })) {
311
            $self->fixAccountForLostAndReturned();
312
        }
313
    }
314
315
    # UPDATE ITEM
316
    $self->{item}->set({
317
        issues        => $self->{item}->issues + 1,
318
        holdingbranch => $self->{library}->branchcode,
319
        itemlost      => 0,
320
        onloan        => $dateDue->ymd(),
321
        datelastborrowed => _today(),
322
        datelastseen  => _today(),
323
    })->store;
324
325
    C4::Stats::UpdateStats({
326
        branch         => $self->{library}->branchcode,
327
        type           => "issue",
328
        other          => ( $sipMode ? "SIP-$sipMode" : "" ),
329
        itemnumber     => $self->{item}->itemnumber,
330
        itemtype       => $self->{item}->itype,
331
        location       => $self->{item}->location,
332
        borrowernumber => $self->{patron}->borrowernumber,
333
        ccode          => $self->{item}->ccode,
334
    });
335
336
    # TODO: Borrower message (SendCirculationAlert) HANDLE IN PrintSlip ?
337
    $self->sendCirculationAlert();
338
339
    $self->removeOverdueDebarments();
340
  }
341
  my $dt = Time::HiRes::time() - $t0;
342
  printf STDERR "[%s] Checkout called : %s - elapsed %.3fs\n", ref $self, scalar(gmtime()), $dt;
343
  return $self;
344
345
}
346
347
sub Renew {
348
  my ($self, $dateDue, $lastRenewedDate) = @_; # Probably can skip most of these
349
  $self->{patron} and $self->{library} or die "Do we really need patron and library?";
350
  warn "Renewing item " . $self->{item}->itemnumber . " for patron " . $self->{patron}->borrowernumber . "\n";
351
  my $t0 = Time::HiRes::time();
352
353
  $self->{checkout} or die "No checkout, cannot renew";
354
  $lastRenewedDate ||= _today();
355
356
  # PREFS THAT APPLY
357
  # CalculateFinesOnReturn
358
  # RenewalPeriodBase
359
  # RenewalSendNotice
360
  # CircControl
361
362
  $self->fixOverduesOnReturn();
363
  $self->canItemBeRenewed() or return $self;
364
365
  # default to checkout branch
366
  my $circControlBranch = $self->getCircControlBranch();
367
368
  # If the due date wasn't specified, calculate it by adding the
369
  # book's loan length to today's date or the current due date
370
  # based on the value of the RenewalPeriodBase syspref.
371
  if ( defined $dateDue && ref $dateDue ne "DateTime" ) {
372
    carp "Invalid date passed to Renew.";
373
    return;
374
    }
375
  # TODO: Adjust to use calendar
376
  unless ($dateDue) {
377
    my $itemType = $self->{item}->effective_itemtype;
378
    if ($self->{prefs}->{RenewalPeriodBase} eq "date_due") {
379
        $dateDue = _mysqldate2dt($self->{checkout}->date_due);
380
    } else {
381
        $dateDue = _today();
382
    }
383
    $dateDue = C4::Circulation::CalcDateDue($dateDue, $itemType, $circControlBranch, $self->{patron}->unblessed, 1);
384
  }
385
386
  # UPDATE CHECKOUT AND ITEM
387
  my $checkoutRenewals = $self->{checkout}->renewals || 0;
388
  my $itemRenewals     = $self->{item}->renewals || 0;
389
  $self->{checkout}->set({
390
      date_due => _dt2mysqldate($dateDue),
391
      lastreneweddate => $lastRenewedDate,
392
      renewals => $checkoutRenewals + 1,
393
  })->store;
394
395
  $self->{item}->set({
396
      renewals => $itemRenewals + 1,
397
      onloan   => _dt2mysqldate($dateDue),
398
  })->store;
399
400
  # TODO: Renewal notice?
401
  $self->removeOverdueDebarments();
402
403
  # STATS
404
  C4::Circulation::UpdateStats({
405
    branch         => $self->{checkout}->branchcode,
406
    type           => "renew",
407
    amount         => undef,
408
    itemnumber     => $self->{item}->itemnumber,
409
    itemtype       => $self->{item}->itype,
410
    location       => $self->{item}->location,
411
    borrowernumber => $self->{patron}->borrowernumber,
412
    ccode          => $self->{item}->ccode,
413
  });
414
  my $dt = Time::HiRes::time() - $t0;
415
  printf STDERR "[%s] Renew called : %s - elapsed %.3fs\n", ref $self, scalar(gmtime()), $dt;
416
  return $self;
417
}
418
419
=head
420
  getBranchItemRule
421
  use legacy C4::Circulation::GetBranchItemRule
422
=cut
423
sub getBranchItemRule {
424
    my $self = shift;
425
    use C4::Circulation;
426
    my $hbr = C4::Circulation::GetBranchItemRule($self->{item}->homebranch, $self->{item}->itype)->{'returnbranch'} || "homebranch";
427
    return $self->{item}->{$hbr} || $self->{item}->homebranch ;
428
}
429
430
=head2 $self->cartToShelf
431
432
  Set the current shelving location of the item record
433
  to its stored permanent shelving location.  This is
434
  primarily used to indicate when an item whose current
435
  location is a special processing ('PROC') or shelving cart
436
  ('CART') location is back in the stacks.
437
438
=cut
439
440
sub cartToShelf {
441
    my $self = shift;
442
    if ($self->{item}->location eq "CART") {
443
      $self->{item}->set({ location => $self->{item}->permanent_location })->store;
444
    }
445
    return $self;
446
}
447
448
=head2 $self->getCircControlBranch
449
450
  Return the library code to be used to determine which circulation
451
  policy applies to a transaction.  Looks up the CircControl and
452
  HomeOrHoldingBranch system preferences.
453
454
=cut
455
456
sub getCircControlBranch {
457
  my $self = shift;
458
  my $branch;
459
460
  if ($self->{prefs}->{CircControl} eq "PickupLibrary" and $self->{checkout}) {
461
        $branch = $self->{checkout}->branchcode;   # Why C4::Context->userenv->{'branch'} in C4::Circulation?
462
  } elsif ($self->{prefs}->{CircControl} eq "PatronLibrary") {
463
      $branch = $self->{patron}->branchcode;
464
  } else {
465
      my $branchfield = $self->{prefs}->{HomeOrHoldingBranch} || "homebranch";
466
      $branch = $self->{item}->{$branchfield} || $self->{item}->{homebranch};
467
  }
468
    return $branch;
469
}
470
471
=head $self->fixAccountForLostAndReturned
472
  If item lost, refund and mark as lost returned
473
=cut
474
sub fixAccountForLostAndReturned {
475
    my $self = shift;
476
    my $acc = Koha::Account::Lines->search(
477
        {
478
            itemnumber  => $self->{item}->itemnumber,
479
            accounttype => { -in => [ "L", "Rep" ] },
480
        },
481
        {
482
            order_by => { -desc => [ "date", "accountno" ] }
483
        }
484
    )->next();
485
    return unless $acc;
486
487
    $acc->accounttype("LR");
488
    $acc->store();
489
490
    my $account = Koha::Account->new( { patron_id => $acc->borrowernumber } );
491
    my $credit_id = $account->pay(
492
        {
493
            amount       => $acc->amount,
494
            description  => "Item Returned " . $self->{item}->barcode,
495
            account_type => "CR",
496
            offset_type  => "Lost Item Return",
497
            accounlines  => [$acc],
498
499
        }
500
    );
501
502
    return $credit_id;
503
}
504
505
=head
506
  ref C4::Circulation.pm L2369
507
    From what I could decipher, this sets fine accounttype to F in normal cases, rest is handled in cronjobs
508
    TODO:
509
    if params exemptfine -> add line with credit and type "Forgiven"
510
    if params dropbox    -> add line with credit and type "Dropbox"
511
512
=cut
513
sub fixOverduesOnReturn {
514
    my $self = shift;
515
516
    $self->{patron} and $self->{item} or croak "Missing patron or item";
517
518
    my $accountline = Koha::Account::Lines->search(
519
        {
520
            borrowernumber => $self->{patron}->borrowernumber,
521
            itemnumber     => $self->{item}->itemnumber,
522
            -or            => [
523
                accounttype => "FU",
524
                accounttype => "O",
525
            ],
526
        }
527
    )->next();
528
    $accountline and $accountline->accounttype("F")->store;
529
    return $self;
530
}
531
532
=head
533
    ref C4::Circulation L676
534
535
=cut
536
sub canItemBeIssued {
537
    my ($self, $dateDue, $ignoreReserves) = @_;
538
    # my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_;
539
    my $needsconfirmation;    # filled with problems that needs confirmations
540
    my $issuingimpossible;    # filled with problems that causes the issue to be IMPOSSIBLE
541
    my $alerts;               # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
542
    my $messages;             # filled with information messages-> that should be displayed.
543
544
    # my $onsite_checkout     = $params->{onsite_checkout}     || 0;
545
    # my $override_high_holds = $params->{override_high_holds} || 0;
546
547
    # PREFS
548
    # noissuescharge
549
    # AllowFineOverride
550
    # AllFinesNeedOverride
551
    # NoIssuesChargeGuarantees
552
    # This should be returned before calling Core::Circulation
553
    $self->{item}->itemnumber or return ($self, {UNKNOWN_BARCODE => 1});
554
    # TODO: should dateDue be automatically calculated?
555
    # Makes no sense to ask for dateDue
556
#     unless ($dateDue) {
557
#         my $itype = $self->{item}->effective_itemtype;
558
#         $dateDue = C4::Circulation::CalcDateDue( _today(), $itype, $self->{library}->branchcode, $self->{patron}->unblessed, 1 );
559
560
#     }
561
#     $dateDue->truncate( to => "minute" );
562
563
    # PATRON FLAGS
564
    $self->{patron}->gonenoaddress  and $issuingimpossible->{GNA}       = 1;
565
    $self->{patron}->lost           and $issuingimpossible->{CARD_LOST} = 1;
566
    $self->{patron}->debarred       and $issuingimpossible->{DEBARRED}  = 1;
567
    $self->{patron}->is_expired     and $issuingimpossible->{EXPIRED}   = 1;
568
569
    # DEBTS
570
    # Note: Koha::Account is different than other Koha Objects
571
    my $debt = Koha::Account->new({patron_id => $self->{patron}->borrowernumber})->balance;
572
    if ($debt > 0) {
573
        # TODO: if $self->{prefs}->{IssuingInProcess}
574
        if ($debt > $self->{prefs}->{noissuescharge} and not $self->{prefs}->{AllowFineOverride}) {
575
            $issuingimpossible->{DEBT} = sprintf( "%.2f", $debt );
576
        }
577
    } elsif ($self->{prefs}->{NoIssuesChargeGuarantees}) {
578
        my $guarantees_charges;
579
        foreach my $g ( @$self->{patron}->guarantees ) {
580
            $guarantees_charges += Koha::Account->new({patron_id => $g->borrowernumber})->balance;
581
        }
582
        if ( $guarantees_charges > $self->{prefs}->{NoIssuesChargeGuarantees} and not $self->{prefs}->{AllowFineOverride}) {
583
            $issuingimpossible->{DEBT_GUARANTEES} = sprintf( "%.2f", $$guarantees_charges );
584
        }
585
    }
586
587
    # DEBARRED
588
    if ( my $debarred_date = $self->{patron}->is_debarred ) {
589
        if ($debarred_date eq "9999-12-31") {
590
            $issuingimpossible->{USERBLOCKEDNOENDDATE} = $debarred_date;
591
        } else {
592
            $issuingimpossible->{USERBLOCKEDWITHENDDATE} = $debarred_date;
593
        }
594
    } elsif ( my $num_overdues = $self->{patron}->has_overdues ) {
595
        if ( C4::Context->preference("OverduesBlockCirc") eq "block") {
596
            $issuingimpossible->{USERBLOCKEDOVERDUE} = $num_overdues;
597
        }
598
        elsif ( C4::Context->preference("OverduesBlockCirc") eq "confirmation") {
599
            $needsconfirmation->{USERBLOCKEDOVERDUE} = $num_overdues;
600
        }
601
    }
602
603
    # ALREADY CHECKED OUT
604
    if ($self->{checkout}) {
605
        # SAME PATRON
606
        if ($self->{checkout}->borrowernumber eq $self->{patron}->borrowernumber ) {
607
            if ( $self->{checkout}->onsite_checkout and $self->{prefs}->{SwitchOnSiteCheckouts}) {
608
                $messages->{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1;
609
            } else {
610
                my ($ok, $error) = $self->canItemBeRenewed();
611
                if ($ok) {
612
                    if ( $error eq "onsite_checkout" ) {
613
                        $issuingimpossible->{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1;
614
                    } else {
615
                        $issuingimpossible->{NO_MORE_RENEWALS} = 1;
616
                    }
617
                } else {
618
                    $needsconfirmation->{RENEW_ISSUE} = 1;
619
                }
620
            }
621
        # DIFFERENT PATRON
622
        } else {
623
            my ( $ok, $message ) = $self->canItemBeReturned();
624
            if ( $ok ) {
625
                $needsconfirmation->{ISSUED_TO_ANOTHER} = 1;
626
                # THESE ARE JUST SILLY - should be removed
627
                $needsconfirmation->{issued_firstname} = $self->{checkout}->patron->firstname;
628
                $needsconfirmation->{issued_surname} = $self->{checkout}->patron->surname;
629
                $needsconfirmation->{issued_cardnumber} = $self->{checkout}->patron->cardnumber;
630
                $needsconfirmation->{issued_borrowernumber} = $self->{checkout}->patron->borrowernumber;
631
            } else {
632
                $issuingimpossible->{RETURN_IMPOSSIBLE} = 1;
633
                $issuingimpossible->{branch_to_return} = $message;
634
            }
635
        }
636
    }
637
638
    # TODO: Too Many checkouts
639
    # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS
640
    #
641
    # my $switch_onsite_checkout = (
642
    #       C4::Context->preference('SwitchOnSiteCheckouts')
643
    #   and $issue
644
    #   and $issue->onsite_checkout
645
    #   and $issue->borrowernumber == $borrower->{'borrowernumber'} ? 1 : 0 );
646
    # my $toomany = TooMany( $borrower, $item->{biblionumber}, $item, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } );
647
    # # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book
648
    # if ( $toomany && not exists $needsconfirmation->{RENEW_ISSUE} ) {
649
    #     if ( $toomany->{max_allowed} == 0 ) {
650
    #         $needsconfirmation->{PATRON_CANT} = 1;
651
    #     }
652
    #     if ( C4::Context->preference("AllowTooManyOverride") ) {
653
    #         $needsconfirmation->{TOO_MANY} = $toomany->{reason};
654
    #         $needsconfirmation->{current_loan_count} = $toomany->{count};
655
    #         $needsconfirmation->{max_loans_allowed} = $toomany->{max_allowed};
656
    #     } else {
657
    #         $issuingimpossible->{TOO_MANY} = $toomany->{reason};
658
    #         $issuingimpossible->{current_loan_count} = $toomany->{count};
659
    #         $issuingimpossible->{max_loans_allowed} = $toomany->{max_allowed};
660
    #     }
661
    # }
662
663
    # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
664
    if ($self->{patron}->wants_check_for_previous_checkout and $self->{patron}->do_check_for_previous_checkout($self->{item})) {
665
        $needsconfirmation->{PREVISSUE} = 1
666
    }
667
668
    # ITEM BASED RESTRICTIONS
669
    # TODO:
670
    # Age restriction
671
    # pref item-level_itypes
672
    # independentBranches
673
    # rental charges
674
    # decreaseLoanHighHolds
675
676
    $self->{item}->withdrawn > 0 and $issuingimpossible->{WTHDRAWN} = 1;
677
    if ($self->{item}->restricted and $self->{item}->restricted > 0) {
678
        $issuingimpossible->{RESTRICTED} = 1;
679
    }
680
681
    if ($self->{item}->notforloan) {
682
        if ($self->{prefs}->{AllowNotForLoanOverride}) {
683
            $issuingimpossible->{NOT_FOR_LOAN} = 1;
684
            $issuingimpossible->{item_notforloan} = $self->{item}->notforloan; # WHY THIS?
685
        } else {
686
            $needsconfirmation->{NOT_FOR_LOAN_FORCING} = 1;
687
            $needsconfirmation->{item_notforloan} = $self->{item}->notforloan; # WHY THIS?
688
        }
689
    }
690
691
    if ( $self->{item}->itemlost and $self->{prefs}->{IssueLostItem} ne "nothing" ) {
692
        my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $self->{item}->itemlost });
693
        my $code = $av->count ? $av->next->lib : "";
694
        ( $self->{prefs}->{IssueLostItem} eq "confirm" ) and $needsconfirmation->{ITEM_LOST} = $code;
695
        ( $self->{prefs}->{IssueLostItem} eq "alert"   ) and $alerts->{ITEM_LOST} = $code;            # TODO: alert only used once, should be removed
696
    }
697
698
    unless ( $ignoreReserves ) {
699
        my $hold = $self->getPendingHold();
700
        if ($hold) {
701
            if ($hold->is_waiting) {
702
                $needsconfirmation->{RESERVE_WAITING} = 1;
703
                $needsconfirmation->{'reswaitingdate'} = $hold->waitingdate;
704
            } else {
705
                $needsconfirmation->{RESERVED} = 1;
706
            }
707
708
            $needsconfirmation->{'resfirstname'} = $hold->patron->firstname;
709
            $needsconfirmation->{'ressurname'} = $hold->patron->surname;
710
            $needsconfirmation->{'rescardnumber'} = $hold->patron->cardnumber;
711
            $needsconfirmation->{'resborrowernumber'} = $hold->patron->borrowernumber;
712
            $needsconfirmation->{'resbranchcode'} = $hold->patron->branchcode;
713
            $needsconfirmation->{'resreservedate'} = $hold->reservedate;
714
        }
715
    }
716
717
    # TODO: consider if this is neccessary
718
    if ( not $self->{prefs}->{AllowMultipleIssuesOnABiblio} and not $issuingimpossible->{NO_MORE_RENEWALS} and not $needsconfirmation->{RENEW_ISSUE}) {
719
        # don't do the multiple loans per bib check if we've
720
        # already determined that we've got a loan on the same item
721
        unless ($self->{item}->biblio->subscriptions->count) {
722
            my $checkouts = Koha::Checkouts->search({
723
                borrowernumber => $self->{patron}->borrowernumber,
724
                biblionumber   => $self->{item}->biblionumber,
725
            }, {
726
                join => 'item',
727
            });
728
            if ( $checkouts->count ) {
729
                $needsconfirmation->{BIBLIO_ALREADY_ISSUED} = 1;
730
            }
731
        }
732
    }
733
734
    return ( $issuingimpossible, $needsconfirmation, $alerts, $messages );
735
}
736
737
=head
738
  $self->canItemBeRenewed
739
    if no holds binds item, allow renewal
740
    args:
741
      override => bool
742
    return:
743
      bool => can/cannot be renewed
744
      msg  => legacy error msg string
745
=cut
746
747
sub canItemBeRenewed {
748
    my ($self, $override) = @_;
749
    $self->{checkout} or return ( 0, "no_checkout" );
750
751
    # PREFS THAT MIGHT APPLY
752
    # OverduesBlockRenewing
753
    # RestrictionBlockRenewing
754
755
    # Too few items to fill holds means item cannot be renewed
756
    if (not $self->canItemFillHold and not $override) {
757
        return ( 0, "on_hold" );
758
    }
759
760
    # CIRCULATION RULES
761
    my $circ_rule = Koha::IssuingRules->get_effective_issuing_rule(
762
        {   categorycode => $self->{patron}->categorycode,
763
            itemtype     => $self->{item}->itype,
764
            branchcode   => $self->{checkout}->branchcode,
765
        }
766
    );
767
768
    my $renewals = $self->{checkout}->renewals || 0;
769
    $circ_rule or return ( 0, "too_many" ); # MISSING CIRC RULE - too_many in original, but should perhaps be better worded
770
    $circ_rule->renewalsallowed <= $renewals or return ( 0, "too_many" );
771
772
773
    $self->{patron}->is_debarred and return ( 0, "restriction" );
774
    ($self->{patron}->has_overdues or $self->{checkout}->is_overdue) and return ( 0, "overdue" );
775
776
    # TODO: AUTORENEW - USE CASE?
777
778
    # NO RENEWAL BEFORE
779
    if ( defined $circ_rule->norenewalbefore and $circ_rule->norenewalbefore ne "" ) {
780
        my $dt = _mysqldate2dt($self->{checkout}->date_due);
781
        my $soonestrenewal = $dt->subtract( $circ_rule->lengthunit => $circ_rule->norenewalbefore );
782
        if ( $soonestrenewal > DateTime->now() ) {
783
            return ( 0, "too_soon" );
784
        }
785
    }
786
    return (1, undef);
787
}
788
789
sub canItemBeReturned {
790
  my $self = shift;
791
  my $allowreturntobranch = $self->{prefs}->{AllowReturnToBranch};
792
793
  # assume return is allowed to start
794
  my $allowed = 1;
795
  my $message;
796
797
  # identify all cases where return is forbidden
798
  if ($allowreturntobranch eq "homebranch" && $self->{library}->branchcode ne $self->{item}->homebranch) {
799
     $allowed = 0;
800
     $message = $self->{item}->homebranch;
801
  } elsif ($allowreturntobranch eq "holdingbranch" && $self->{library}->branchcode ne $self->{item}->holdingbranch) {
802
     $allowed = 0;
803
     $message = $self->{item}->holdingbranch;
804
  } elsif ($allowreturntobranch eq "homeorholdingbranch" && $self->{library}->branchcode ne $self->{item}->homebranch
805
      && $self->{library}->branchcode ne $self->{item}->holdingbranch) {
806
     $allowed = 0;
807
     $message = $self->{item}->homebranch; # FIXME: choice of homebranch is arbitrary
808
  }
809
810
  return ($allowed, $message);
811
}
812
813
=head
814
  Check if item can fill a pending hold
815
    reservable means:
816
      - item not found (Waiting or in Transfer)
817
      - item not lost, withdrawn
818
      - item notforloan not code > 0
819
      - item not damaged (unless AllowHoldsOnDamagedItems)
820
      - number of reservable items is greater than number of pending holds
821
      - something something onshelfholds
822
823
    ref: C4::Reserves::CanItemBeReserved L282
824
=cut
825
sub canItemFillHold {
826
    my $self = shift;
827
828
    # PREFS
829
    #   AllowRenewalIfOtherItemsAvailable
830
    #   item-level_itypes
831
    #   IndependentBranches
832
    #   canreservefromotherbranches
833
    #   AllowHoldsOnDamagedItems
834
835
    my $reservableItems = Koha::Items->search({
836
        "me.biblionumber" => $self->{item}->biblionumber,
837
        itemlost => 0,
838
        withdrawn => 0,
839
        notforloan => { "<=" => 0 },
840
        "reserves.found" => { "!=" => undef },
841
      },
842
      { join => "reserves" });
843
844
    if ($reservableItems->count < $self->getPendingHoldsByBiblio->count) {
845
      return 0;
846
    } else {
847
      return 1;
848
    }
849
}
850
=head
851
  return next hold, item or biblio level
852
=cut
853
sub getPendingHold {
854
    my $self = shift;
855
    my $itemholds   = $self->{item}->current_holds;
856
    my $biblioholds = Koha::Holds->search({biblionumber => $self->{item}->biblionumber, found => undef, suspend => 0}, { order_by => { -asc => "priority" }});
857
    my $hold = $itemholds->count ? $itemholds->next : $biblioholds->count ? $biblioholds->next : undef;
858
    return $hold;
859
}
860
861
=head
862
  return pending holds on biblio level
863
=cut
864
sub getPendingHoldsByBiblio {
865
      my $self = shift;
866
      return Koha::Holds->search({biblionumber => $self->{item}->biblionumber, found => undef, suspend => 0}, { order_by => { -asc => "priority" }});
867
}
868
869
sub _mysqldate2dt {
870
    my $datestring = shift;
871
    my $strp = DateTime::Format::Strptime->new( pattern => "%Y-%m-%d %H:%M:%S" );
872
    return $strp->parse_datetime( $datestring );
873
}
874
875
sub _dt2mysqldate {
876
    my $dt = shift;
877
    if (ref $dt ne "DateTime") {
878
        return $dt;
879
    } else {
880
        return $dt->strftime("%F %T");
881
    }
882
}
883
884
sub _ymd2dt {
885
    my $ymdstring = shift;
886
    my $strp = DateTime::Format::Strptime->new( pattern => "%Y-%m-%d" );
887
    return $strp->parse_datetime( $ymdstring );
888
}
889
890
sub _today {
891
    return DateTime->now()->ymd;
892
}
893
894
sub sendCirculationAlert {
895
  # TODO
896
}
897
898
=head
899
  Remove any OVERDUES related debarment if the borrower has no overdues
900
=cut
901
sub removeOverdueDebarments {
902
  my $self = shift;
903
  return unless $self->{patron};
904
  if ( $self->{patron}->is_debarred
905
    && ! $self->{patron}->has_overdues
906
    && @{ Koha::Patron::Debarments::GetDebarments({ borrowernumber => $self->{patron}->borrowernumber, type => "OVERDUES" }) }
907
  ) {
908
      Koha::Patron::Debarments::DelUniqueDebarment({ borrowernumber => $self->{patron}->borrowernumber, type => "OVERDUES" });
909
  }
910
}
911
912
1;

Return to bug 21327