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

(-)a/Core/Circulation.pm (-1 / +913 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
        # Update transfer, or should we close and create new?
251
        $transfer->frombranch($self->{library}->branchcode)->store;
252
      }
253
    }
254
255
    # TODO: C4::Circulation.pm:1317
256
    # MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve );
257
    #   -> C4::Reserves.pm:1712
258
    #   -> CheckReserves C4::Reserves.pm:626
259
260
    # HOLD
261
    my $hold = $self->getPendingHold();
262
    if ($hold) {
263
        # Right patron - fill hold
264
        if ($hold->borrowernumber == $self->{patron}->borrowernumber) {
265
            $hold->set({found => "F", priority => 0});
266
            # Merge tables - PLEASE!
267
            Koha::Old::Hold->new( $hold->unblessed() )->store();
268
            $hold->delete();
269
        } else {
270
            # TODO: Checkout should be aborted here?
271
            $self->{error} = Core::Exception::Circulation::CheckoutError->new("Unable to checkout - hold for another patron!");
272
        }
273
    }
274
275
    # CIRCULATION RULES
276
    my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule({
277
        categorycode => $self->{patron}->{categorycode},
278
        itemtype     => $self->{item}->itype,
279
        branchcode   => $self->{library}->branchcode,
280
    });
281
282
    # CHECKOUT
283
    # TODO: rewrite calculate dateDue to overridable module
284
    unless ($dateDue) {
285
        my $itype = $self->{item}->effective_itemtype;
286
        $dateDue = C4::Circulation::CalcDateDue( $issueDate, $itype, $self->{library}->branchcode, $self->{patron}->unblessed, 1 );
287
288
    }
289
    $dateDue->truncate( to => "minute" );
290
291
    # TODO: auto_renew and onsite_checkout
292
    $self->{checkout} = Koha::Checkout->new({
293
        borrowernumber => $self->{patron}->borrowernumber,
294
        itemnumber     => $self->{item}->itemnumber,
295
        issuedate      => $issueDate,
296
        date_due       => $dateDue,
297
        branchcode     => $self->{library}->branchcode,
298
    })->store;
299
300
    # Why was totalissues never set?
301
    my $totalissues = $self->{item}->biblioitem->totalissues // 0;
302
    $self->{item}->biblioitem->totalissues($totalissues + 1);
303
304
    # LOST ITEM REFUND
305
    if ($self->{item}->itemlost) {
306
        # REFUND LOST ITEM FEE
307
        if (Koha::RefundLostItemFeeRules->should_refund({
308
            current_branch      => $self->{library}->branchcode,
309
            item_home_branch    => $self->{item}->homebranch,
310
            item_holding_branch => $self->{item}->holdingbranch,
311
        })) {
312
            $self->fixAccountForLostAndReturned();
313
        }
314
    }
315
316
    # UPDATE ITEM
317
    $self->{item}->set({
318
        issues        => $self->{item}->issues + 1,
319
        holdingbranch => $self->{library}->branchcode,
320
        itemlost      => 0,
321
        onloan        => $dateDue->ymd(),
322
        datelastborrowed => _today(),
323
        datelastseen  => _today(),
324
    })->store;
325
326
    C4::Stats::UpdateStats({
327
        branch         => $self->{library}->branchcode,
328
        type           => "issue",
329
        other          => ( $sipMode ? "SIP-$sipMode" : "" ),
330
        itemnumber     => $self->{item}->itemnumber,
331
        itemtype       => $self->{item}->itype,
332
        location       => $self->{item}->location,
333
        borrowernumber => $self->{patron}->borrowernumber,
334
        ccode          => $self->{item}->ccode,
335
    });
336
337
    # TODO: Borrower message (SendCirculationAlert) HANDLE IN PrintSlip ?
338
    $self->sendCirculationAlert();
339
340
    $self->removeOverdueDebarments();
341
  }
342
  my $dt = Time::HiRes::time() - $t0;
343
  printf STDERR "[%s] Checkout called : %s - elapsed %.3fs\n", ref $self, scalar(gmtime()), $dt;
344
  return $self;
345
346
}
347
348
sub Renew {
349
  my ($self, $dateDue, $lastRenewedDate) = @_; # Probably can skip most of these
350
  $self->{patron} and $self->{library} or die "Do we really need patron and library?";
351
  warn "Renewing item " . $self->{item}->itemnumber . " for patron " . $self->{patron}->borrowernumber . "\n";
352
  my $t0 = Time::HiRes::time();
353
354
  $self->{checkout} or die "No checkout, cannot renew";
355
  $lastRenewedDate ||= _today();
356
357
  # PREFS THAT APPLY
358
  # CalculateFinesOnReturn
359
  # RenewalPeriodBase
360
  # RenewalSendNotice
361
  # CircControl
362
363
  $self->fixOverduesOnReturn();
364
  $self->canItemBeRenewed() or return $self;
365
366
  # default to checkout branch
367
  my $circControlBranch = $self->getCircControlBranch();
368
369
  # If the due date wasn't specified, calculate it by adding the
370
  # book's loan length to today's date or the current due date
371
  # based on the value of the RenewalPeriodBase syspref.
372
  if ( defined $dateDue && ref $dateDue ne "DateTime" ) {
373
    carp "Invalid date passed to Renew.";
374
    return;
375
    }
376
  # TODO: Adjust to use calendar
377
  unless ($dateDue) {
378
    my $itemType = $self->{item}->effective_itemtype;
379
    if ($self->{prefs}->{RenewalPeriodBase} eq "date_due") {
380
        $dateDue = _mysqldate2dt($self->{checkout}->date_due);
381
    } else {
382
        $dateDue = _today();
383
    }
384
    $dateDue = C4::Circulation::CalcDateDue($dateDue, $itemType, $circControlBranch, $self->{patron}->unblessed, 1);
385
  }
386
387
  # UPDATE CHECKOUT AND ITEM
388
  my $checkoutRenewals = $self->{checkout}->renewals || 0;
389
  my $itemRenewals     = $self->{item}->renewals || 0;
390
  $self->{checkout}->set({
391
      date_due => _dt2mysqldate($dateDue),
392
      lastreneweddate => $lastRenewedDate,
393
      renewals => $checkoutRenewals + 1,
394
  })->store;
395
396
  $self->{item}->set({
397
      renewals => $itemRenewals + 1,
398
      onloan   => _dt2mysqldate($dateDue),
399
  })->store;
400
401
  # TODO: Renewal notice?
402
  $self->removeOverdueDebarments();
403
404
  # STATS
405
  C4::Circulation::UpdateStats({
406
    branch         => $self->{checkout}->branchcode,
407
    type           => "renew",
408
    amount         => undef,
409
    itemnumber     => $self->{item}->itemnumber,
410
    itemtype       => $self->{item}->itype,
411
    location       => $self->{item}->location,
412
    borrowernumber => $self->{patron}->borrowernumber,
413
    ccode          => $self->{item}->ccode,
414
  });
415
  my $dt = Time::HiRes::time() - $t0;
416
  printf STDERR "[%s] Renew called : %s - elapsed %.3fs\n", ref $self, scalar(gmtime()), $dt;
417
  return $self;
418
}
419
420
=head
421
  getBranchItemRule
422
  use legacy C4::Circulation::GetBranchItemRule
423
=cut
424
sub getBranchItemRule {
425
    my $self = shift;
426
    use C4::Circulation;
427
    my $hbr = C4::Circulation::GetBranchItemRule($self->{item}->homebranch, $self->{item}->itype)->{'returnbranch'} || "homebranch";
428
    return $self->{item}->{$hbr} || $self->{item}->homebranch ;
429
}
430
431
=head2 $self->cartToShelf
432
433
  Set the current shelving location of the item record
434
  to its stored permanent shelving location.  This is
435
  primarily used to indicate when an item whose current
436
  location is a special processing ('PROC') or shelving cart
437
  ('CART') location is back in the stacks.
438
439
=cut
440
441
sub cartToShelf {
442
    my $self = shift;
443
    if ($self->{item}->location eq "CART") {
444
      $self->{item}->set({ location => $self->{item}->permanent_location })->store;
445
    }
446
    return $self;
447
}
448
449
=head2 $self->getCircControlBranch
450
451
  Return the library code to be used to determine which circulation
452
  policy applies to a transaction.  Looks up the CircControl and
453
  HomeOrHoldingBranch system preferences.
454
455
=cut
456
457
sub getCircControlBranch {
458
  my $self = shift;
459
  my $branch;
460
461
  if ($self->{prefs}->{CircControl} eq "PickupLibrary" and $self->{checkout}) {
462
        $branch = $self->{checkout}->branchcode;   # Why C4::Context->userenv->{'branch'} in C4::Circulation?
463
  } elsif ($self->{prefs}->{CircControl} eq "PatronLibrary") {
464
      $branch = $self->{patron}->branchcode;
465
  } else {
466
      my $branchfield = $self->{prefs}->{HomeOrHoldingBranch} || "homebranch";
467
      $branch = $self->{item}->{$branchfield} || $self->{item}->{homebranch};
468
  }
469
    return $branch;
470
}
471
472
=head $self->fixAccountForLostAndReturned
473
  If item lost, refund and mark as lost returned
474
=cut
475
sub fixAccountForLostAndReturned {
476
    my $self = shift;
477
    my $acc = Koha::Account::Lines->search(
478
        {
479
            itemnumber  => $self->{item}->itemnumber,
480
            accounttype => { -in => [ "L", "Rep" ] },
481
        },
482
        {
483
            order_by => { -desc => [ "date", "accountno" ] }
484
        }
485
    )->next();
486
    return unless $acc;
487
488
    $acc->accounttype("LR");
489
    $acc->store();
490
491
    my $account = Koha::Account->new( { patron_id => $acc->borrowernumber } );
492
    my $credit_id = $account->pay(
493
        {
494
            amount       => $acc->amount,
495
            description  => "Item Returned " . $self->{item}->barcode,
496
            account_type => "CR",
497
            offset_type  => "Lost Item Return",
498
            accounlines  => [$acc],
499
500
        }
501
    );
502
503
    return $credit_id;
504
}
505
506
=head
507
  ref C4::Circulation.pm L2369
508
    From what I could decipher, this sets fine accounttype to F in normal cases, rest is handled in cronjobs
509
    TODO:
510
    if params exemptfine -> add line with credit and type "Forgiven"
511
    if params dropbox    -> add line with credit and type "Dropbox"
512
513
=cut
514
sub fixOverduesOnReturn {
515
    my $self = shift;
516
517
    $self->{patron} and $self->{item} or croak "Missing patron or item";
518
519
    my $accountline = Koha::Account::Lines->search(
520
        {
521
            borrowernumber => $self->{patron}->borrowernumber,
522
            itemnumber     => $self->{item}->itemnumber,
523
            -or            => [
524
                accounttype => "FU",
525
                accounttype => "O",
526
            ],
527
        }
528
    )->next();
529
    $accountline and $accountline->accounttype("F")->store;
530
    return $self;
531
}
532
533
=head
534
    ref C4::Circulation L676
535
536
=cut
537
sub canItemBeIssued {
538
    my ($self, $dateDue, $ignoreReserves) = @_;
539
    # my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_;
540
    my $needsconfirmation;    # filled with problems that needs confirmations
541
    my $issuingimpossible;    # filled with problems that causes the issue to be IMPOSSIBLE
542
    my $alerts;               # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
543
    my $messages;             # filled with information messages-> that should be displayed.
544
545
    # my $onsite_checkout     = $params->{onsite_checkout}     || 0;
546
    # my $override_high_holds = $params->{override_high_holds} || 0;
547
548
    # PREFS
549
    # noissuescharge
550
    # AllowFineOverride
551
    # AllFinesNeedOverride
552
    # NoIssuesChargeGuarantees
553
    # This should be returned before calling Core::Circulation
554
    $self->{item}->itemnumber or return ($self, {UNKNOWN_BARCODE => 1});
555
    # TODO: should dateDue be automatically calculated?
556
    # Makes no sense to ask for dateDue
557
#     unless ($dateDue) {
558
#         my $itype = $self->{item}->effective_itemtype;
559
#         $dateDue = C4::Circulation::CalcDateDue( _today(), $itype, $self->{library}->branchcode, $self->{patron}->unblessed, 1 );
560
561
#     }
562
#     $dateDue->truncate( to => "minute" );
563
564
    # PATRON FLAGS
565
    $self->{patron}->gonenoaddress  and $issuingimpossible->{GNA}       = 1;
566
    $self->{patron}->lost           and $issuingimpossible->{CARD_LOST} = 1;
567
    $self->{patron}->debarred       and $issuingimpossible->{DEBARRED}  = 1;
568
    $self->{patron}->is_expired     and $issuingimpossible->{EXPIRED}   = 1;
569
570
    # DEBTS
571
    # Note: Koha::Account is different than other Koha Objects
572
    my $debt = Koha::Account->new({patron_id => $self->{patron}->borrowernumber})->balance;
573
    if ($debt > 0) {
574
        # TODO: if $self->{prefs}->{IssuingInProcess}
575
        if ($debt > $self->{prefs}->{noissuescharge} and not $self->{prefs}->{AllowFineOverride}) {
576
            $issuingimpossible->{DEBT} = sprintf( "%.2f", $debt );
577
        }
578
    } elsif ($self->{prefs}->{NoIssuesChargeGuarantees}) {
579
        my $guarantees_charges;
580
        foreach my $g ( @$self->{patron}->guarantees ) {
581
            $guarantees_charges += Koha::Account->new({patron_id => $g->borrowernumber})->balance;
582
        }
583
        if ( $guarantees_charges > $self->{prefs}->{NoIssuesChargeGuarantees} and not $self->{prefs}->{AllowFineOverride}) {
584
            $issuingimpossible->{DEBT_GUARANTEES} = sprintf( "%.2f", $$guarantees_charges );
585
        }
586
    }
587
588
    # DEBARRED
589
    if ( my $debarred_date = $self->{patron}->is_debarred ) {
590
        if ($debarred_date eq "9999-12-31") {
591
            $issuingimpossible->{USERBLOCKEDNOENDDATE} = $debarred_date;
592
        } else {
593
            $issuingimpossible->{USERBLOCKEDWITHENDDATE} = $debarred_date;
594
        }
595
    } elsif ( my $num_overdues = $self->{patron}->has_overdues ) {
596
        if ( C4::Context->preference("OverduesBlockCirc") eq "block") {
597
            $issuingimpossible->{USERBLOCKEDOVERDUE} = $num_overdues;
598
        }
599
        elsif ( C4::Context->preference("OverduesBlockCirc") eq "confirmation") {
600
            $needsconfirmation->{USERBLOCKEDOVERDUE} = $num_overdues;
601
        }
602
    }
603
604
    # ALREADY CHECKED OUT
605
    if ($self->{checkout}) {
606
        # SAME PATRON
607
        if ($self->{checkout}->borrowernumber eq $self->{patron}->borrowernumber ) {
608
            if ( $self->{checkout}->onsite_checkout and $self->{prefs}->{SwitchOnSiteCheckouts}) {
609
                $messages->{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1;
610
            } else {
611
                my ($ok, $error) = $self->canItemBeRenewed();
612
                if ($ok) {
613
                    if ( $error eq "onsite_checkout" ) {
614
                        $issuingimpossible->{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1;
615
                    } else {
616
                        $issuingimpossible->{NO_MORE_RENEWALS} = 1;
617
                    }
618
                } else {
619
                    $needsconfirmation->{RENEW_ISSUE} = 1;
620
                }
621
            }
622
        # DIFFERENT PATRON
623
        } else {
624
            my ( $ok, $message ) = $self->canItemBeReturned();
625
            if ( $ok ) {
626
                $needsconfirmation->{ISSUED_TO_ANOTHER} = 1;
627
                # THESE ARE JUST SILLY - should be removed
628
                $needsconfirmation->{issued_firstname} = $self->{checkout}->patron->firstname;
629
                $needsconfirmation->{issued_surname} = $self->{checkout}->patron->surname;
630
                $needsconfirmation->{issued_cardnumber} = $self->{checkout}->patron->cardnumber;
631
                $needsconfirmation->{issued_borrowernumber} = $self->{checkout}->patron->borrowernumber;
632
            } else {
633
                $issuingimpossible->{RETURN_IMPOSSIBLE} = 1;
634
                $issuingimpossible->{branch_to_return} = $message;
635
            }
636
        }
637
    }
638
639
    # TODO: Too Many checkouts
640
    # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS
641
    #
642
    # my $switch_onsite_checkout = (
643
    #       C4::Context->preference('SwitchOnSiteCheckouts')
644
    #   and $issue
645
    #   and $issue->onsite_checkout
646
    #   and $issue->borrowernumber == $borrower->{'borrowernumber'} ? 1 : 0 );
647
    # my $toomany = TooMany( $borrower, $item->{biblionumber}, $item, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } );
648
    # # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book
649
    # if ( $toomany && not exists $needsconfirmation->{RENEW_ISSUE} ) {
650
    #     if ( $toomany->{max_allowed} == 0 ) {
651
    #         $needsconfirmation->{PATRON_CANT} = 1;
652
    #     }
653
    #     if ( C4::Context->preference("AllowTooManyOverride") ) {
654
    #         $needsconfirmation->{TOO_MANY} = $toomany->{reason};
655
    #         $needsconfirmation->{current_loan_count} = $toomany->{count};
656
    #         $needsconfirmation->{max_loans_allowed} = $toomany->{max_allowed};
657
    #     } else {
658
    #         $issuingimpossible->{TOO_MANY} = $toomany->{reason};
659
    #         $issuingimpossible->{current_loan_count} = $toomany->{count};
660
    #         $issuingimpossible->{max_loans_allowed} = $toomany->{max_allowed};
661
    #     }
662
    # }
663
664
    # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
665
    if ($self->{patron}->wants_check_for_previous_checkout and $self->{patron}->do_check_for_previous_checkout($self->{item})) {
666
        $needsconfirmation->{PREVISSUE} = 1
667
    }
668
669
    # ITEM BASED RESTRICTIONS
670
    # TODO:
671
    # Age restriction
672
    # pref item-level_itypes
673
    # independentBranches
674
    # rental charges
675
    # decreaseLoanHighHolds
676
677
    $self->{item}->withdrawn > 0 and $issuingimpossible->{WTHDRAWN} = 1;
678
    if ($self->{item}->restricted and $self->{item}->restricted > 0) {
679
        $issuingimpossible->{RESTRICTED} = 1;
680
    }
681
682
    if ($self->{item}->notforloan) {
683
        if ($self->{prefs}->{AllowNotForLoanOverride}) {
684
            $issuingimpossible->{NOT_FOR_LOAN} = 1;
685
            $issuingimpossible->{item_notforloan} = $self->{item}->notforloan; # WHY THIS?
686
        } else {
687
            $needsconfirmation->{NOT_FOR_LOAN_FORCING} = 1;
688
            $needsconfirmation->{item_notforloan} = $self->{item}->notforloan; # WHY THIS?
689
        }
690
    }
691
692
    if ( $self->{item}->itemlost and $self->{prefs}->{IssueLostItem} ne "nothing" ) {
693
        my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $self->{item}->itemlost });
694
        my $code = $av->count ? $av->next->lib : "";
695
        ( $self->{prefs}->{IssueLostItem} eq "confirm" ) and $needsconfirmation->{ITEM_LOST} = $code;
696
        ( $self->{prefs}->{IssueLostItem} eq "alert"   ) and $alerts->{ITEM_LOST} = $code;            # TODO: alert only used once, should be removed
697
    }
698
699
    unless ( $ignoreReserves ) {
700
        my $hold = $self->getPendingHold();
701
        if ($hold) {
702
            if ($hold->is_waiting) {
703
                $needsconfirmation->{RESERVE_WAITING} = 1;
704
                $needsconfirmation->{'reswaitingdate'} = $hold->waitingdate;
705
            } else {
706
                $needsconfirmation->{RESERVED} = 1;
707
            }
708
709
            $needsconfirmation->{'resfirstname'} = $hold->patron->firstname;
710
            $needsconfirmation->{'ressurname'} = $hold->patron->surname;
711
            $needsconfirmation->{'rescardnumber'} = $hold->patron->cardnumber;
712
            $needsconfirmation->{'resborrowernumber'} = $hold->patron->borrowernumber;
713
            $needsconfirmation->{'resbranchcode'} = $hold->patron->branchcode;
714
            $needsconfirmation->{'resreservedate'} = $hold->reservedate;
715
        }
716
    }
717
718
    # TODO: consider if this is neccessary
719
    if ( not $self->{prefs}->{AllowMultipleIssuesOnABiblio} and not $issuingimpossible->{NO_MORE_RENEWALS} and not $needsconfirmation->{RENEW_ISSUE}) {
720
        # don't do the multiple loans per bib check if we've
721
        # already determined that we've got a loan on the same item
722
        unless ($self->{item}->biblio->subscriptions->count) {
723
            my $checkouts = Koha::Checkouts->search({
724
                borrowernumber => $self->{patron}->borrowernumber,
725
                biblionumber   => $self->{item}->biblionumber,
726
            }, {
727
                join => 'item',
728
            });
729
            if ( $checkouts->count ) {
730
                $needsconfirmation->{BIBLIO_ALREADY_ISSUED} = 1;
731
            }
732
        }
733
    }
734
735
    return ( $issuingimpossible, $needsconfirmation, $alerts, $messages );
736
}
737
738
=head
739
  $self->canItemBeRenewed
740
    if no holds binds item, allow renewal
741
    args:
742
      override => bool
743
    return:
744
      bool => can/cannot be renewed
745
      msg  => legacy error msg string
746
=cut
747
748
sub canItemBeRenewed {
749
    my ($self, $override) = @_;
750
    $self->{checkout} or return ( 0, "no_checkout" );
751
752
    # PREFS THAT MIGHT APPLY
753
    # OverduesBlockRenewing
754
    # RestrictionBlockRenewing
755
756
    # Too few items to fill holds means item cannot be renewed
757
    if (not $self->canItemFillHold and not $override) {
758
        return ( 0, "on_hold" );
759
    }
760
761
    # CIRCULATION RULES
762
    my $circ_rule = Koha::IssuingRules->get_effective_issuing_rule(
763
        {   categorycode => $self->{patron}->categorycode,
764
            itemtype     => $self->{item}->itype,
765
            branchcode   => $self->{checkout}->branchcode,
766
        }
767
    );
768
769
    my $renewals = $self->{checkout}->renewals || 0;
770
    $circ_rule or return ( 0, "too_many" ); # MISSING CIRC RULE - too_many in original, but should perhaps be better worded
771
    $circ_rule->renewalsallowed <= $renewals or return ( 0, "too_many" );
772
773
774
    $self->{patron}->is_debarred and return ( 0, "restriction" );
775
    ($self->{patron}->has_overdues or $self->{checkout}->is_overdue) and return ( 0, "overdue" );
776
777
    # TODO: AUTORENEW - USE CASE?
778
779
    # NO RENEWAL BEFORE
780
    if ( defined $circ_rule->norenewalbefore and $circ_rule->norenewalbefore ne "" ) {
781
        my $dt = _mysqldate2dt($self->{checkout}->date_due);
782
        my $soonestrenewal = $dt->subtract( $circ_rule->lengthunit => $circ_rule->norenewalbefore );
783
        if ( $soonestrenewal > DateTime->now() ) {
784
            return ( 0, "too_soon" );
785
        }
786
    }
787
    return (1, undef);
788
}
789
790
sub canItemBeReturned {
791
  my $self = shift;
792
  my $allowreturntobranch = $self->{prefs}->{AllowReturnToBranch};
793
794
  # assume return is allowed to start
795
  my $allowed = 1;
796
  my $message;
797
798
  # identify all cases where return is forbidden
799
  if ($allowreturntobranch eq "homebranch" && $self->{library}->branchcode ne $self->{item}->homebranch) {
800
     $allowed = 0;
801
     $message = $self->{item}->homebranch;
802
  } elsif ($allowreturntobranch eq "holdingbranch" && $self->{library}->branchcode ne $self->{item}->holdingbranch) {
803
     $allowed = 0;
804
     $message = $self->{item}->holdingbranch;
805
  } elsif ($allowreturntobranch eq "homeorholdingbranch" && $self->{library}->branchcode ne $self->{item}->homebranch
806
      && $self->{library}->branchcode ne $self->{item}->holdingbranch) {
807
     $allowed = 0;
808
     $message = $self->{item}->homebranch; # FIXME: choice of homebranch is arbitrary
809
  }
810
811
  return ($allowed, $message);
812
}
813
814
=head
815
  Check if item can fill a pending hold
816
    reservable means:
817
      - item not found (Waiting or in Transfer)
818
      - item not lost, withdrawn
819
      - item notforloan not code > 0
820
      - item not damaged (unless AllowHoldsOnDamagedItems)
821
      - number of reservable items is greater than number of pending holds
822
      - something something onshelfholds
823
824
    ref: C4::Reserves::CanItemBeReserved L282
825
=cut
826
sub canItemFillHold {
827
    my $self = shift;
828
829
    # PREFS
830
    #   AllowRenewalIfOtherItemsAvailable
831
    #   item-level_itypes
832
    #   IndependentBranches
833
    #   canreservefromotherbranches
834
    #   AllowHoldsOnDamagedItems
835
836
    my $reservableItems = Koha::Items->search({
837
        "me.biblionumber" => $self->{item}->biblionumber,
838
        itemlost => 0,
839
        withdrawn => 0,
840
        notforloan => { "<=" => 0 },
841
        "reserves.found" => { "!=" => undef },
842
      },
843
      { join => "reserves" });
844
845
    if ($reservableItems->count < $self->getPendingHoldsByBiblio->count) {
846
      return 0;
847
    } else {
848
      return 1;
849
    }
850
}
851
=head
852
  return next hold, item or biblio level
853
=cut
854
sub getPendingHold {
855
    my $self = shift;
856
    my $itemholds   = $self->{item}->current_holds;
857
    my $biblioholds = Koha::Holds->search({biblionumber => $self->{item}->biblionumber, found => undef, suspend => 0}, { order_by => { -asc => "priority" }});
858
    my $hold = $itemholds->count ? $itemholds->next : $biblioholds->count ? $biblioholds->next : undef;
859
    return $hold;
860
}
861
862
=head
863
  return pending holds on biblio level
864
=cut
865
sub getPendingHoldsByBiblio {
866
      my $self = shift;
867
      return Koha::Holds->search({biblionumber => $self->{item}->biblionumber, found => undef, suspend => 0}, { order_by => { -asc => "priority" }});
868
}
869
870
sub _mysqldate2dt {
871
    my $datestring = shift;
872
    my $strp = DateTime::Format::Strptime->new( pattern => "%Y-%m-%d %H:%M:%S" );
873
    return $strp->parse_datetime( $datestring );
874
}
875
876
sub _dt2mysqldate {
877
    my $dt = shift;
878
    if (ref $dt ne "DateTime") {
879
        return $dt;
880
    } else {
881
        return $dt->strftime("%F %T");
882
    }
883
}
884
885
sub _ymd2dt {
886
    my $ymdstring = shift;
887
    my $strp = DateTime::Format::Strptime->new( pattern => "%Y-%m-%d" );
888
    return $strp->parse_datetime( $ymdstring );
889
}
890
891
sub _today {
892
    return DateTime->now()->ymd;
893
}
894
895
sub sendCirculationAlert {
896
  # TODO
897
}
898
899
=head
900
  Remove any OVERDUES related debarment if the borrower has no overdues
901
=cut
902
sub removeOverdueDebarments {
903
  my $self = shift;
904
  return unless $self->{patron};
905
  if ( $self->{patron}->is_debarred
906
    && ! $self->{patron}->has_overdues
907
    && @{ Koha::Patron::Debarments::GetDebarments({ borrowernumber => $self->{patron}->borrowernumber, type => "OVERDUES" }) }
908
  ) {
909
      Koha::Patron::Debarments::DelUniqueDebarment({ borrowernumber => $self->{patron}->borrowernumber, type => "OVERDUES" });
910
  }
911
}
912
913
1;

Return to bug 21327