You can return items now BUT we need to get the branch to do this properly
[koha.git] / C4 / SIP / ILS.pm
1 #
2 # ILS.pm: Koha ILS interface module
3 #
4
5 package ILS;
6
7 use warnings;
8 use strict;
9 use Sys::Syslog qw(syslog);
10
11 use ILS::Item;
12 use ILS::Patron;
13 use ILS::Transaction;
14 use ILS::Transaction::Checkout;
15 use ILS::Transaction::Checkin;
16 use ILS::Transaction::FeePayment;
17 use ILS::Transaction::Hold;
18 use ILS::Transaction::Renew;
19 use ILS::Transaction::RenewAll;
20
21 my %supports = (
22                 'magnetic media'        => 1,
23                 'security inhibit'      => 0,
24                 'offline operation'     => 0,
25                 "patron status request" => 1,
26                 "checkout"              => 1,
27                 "checkin"               => 1,
28                 "block patron"          => 1,
29                 "acs status"            => 1,
30                 "login"                 => 1,
31                 "patron information"    => 1,
32                 "end patron session"    => 1,
33                 "fee paid"              => 0,
34                 "item information"      => 1,
35                 "item status update"    => 0,
36                 "patron enable"         => 1,
37                 "hold"                  => 1,
38                 "renew"                 => 1,
39                 "renew all"             => 0,
40                );
41
42 sub new {
43     my ($class, $institution) = @_;
44     my $type = ref($class) || $class;
45     my $self = {};
46 #use Data::Dumper;
47 #warn " INSTITUTION:";
48 #warn Dumper($institution);
49     syslog("LOG_DEBUG", "new ILS '%s'", $institution->{id});
50     $self->{institution} = $institution;
51
52     return bless $self, $type;
53 }
54
55 sub find_patron {
56     my $self = shift;
57 warn "finding patron";
58     return ILS::Patron->new(@_);
59 }
60
61 sub find_item {
62     my $self = shift;
63 warn "find item";
64     return ILS::Item->new(@_);
65 }
66
67 sub institution {
68     my $self = shift;
69
70     return $self->{institution}->{id};
71 }
72
73 sub supports {
74     my ($self, $op) = @_;
75
76     return (exists($supports{$op}) && $supports{$op});
77 }
78
79 sub check_inst_id {
80     my ($self, $id, $whence) = @_;
81
82     if ($id ne $self->{institution}->{id}) {
83         syslog("LOG_WARNING", "%s: received institution '%s', expected '%s'",
84                $whence, $id, $self->{institution}->{id});
85     }
86 }
87
88 sub to_bool {
89     my $bool = shift;
90
91     # If it's defined, and matches a true sort of string, or is
92     # a non-zero number, then it's true.
93     return defined($bool) && (($bool =~ /true|y|yes/i) || $bool != 0);
94 }
95
96 sub checkout_ok {
97     my $self = shift;
98
99     return (exists($self->{policy}->{checkout})
100             && to_bool($self->{policy}->{checkout}));
101 }
102
103 sub checkin_ok {
104     my $self = shift;
105
106     return (exists($self->{policy}->{checkin})
107             && to_bool($self->{policy}->{checkin}));
108 }
109
110 sub status_update_ok {
111     my $self = shift;
112
113     return (exists($self->{policy}->{status_update})
114             && to_bool($self->{policy}->{status_update}));
115
116 }
117
118 sub offline_ok {
119     my $self = shift;
120
121     return (exists($self->{policy}->{offline})
122             && to_bool($self->{policy}->{offline}));
123 }
124
125 #
126 # Checkout(patron_id, item_id, sc_renew):
127 #    patron_id & item_id are the identifiers send by the terminal
128 #    sc_renew is the renewal policy configured on the terminal
129 # returns a status opject that can be queried for the various bits
130 # of information that the protocol (SIP or NCIP) needs to generate
131 # the response.
132 #
133 sub checkout {
134     my ($self, $patron_id, $item_id, $sc_renew) = @_;
135     my ($patron, $item, $circ);
136
137     $circ = new ILS::Transaction::Checkout;
138     # BEGIN TRANSACTION
139     $circ->patron($patron = new ILS::Patron $patron_id);
140     $circ->item($item = new ILS::Item $item_id);
141
142     if (!$patron) {
143                 $circ->screen_msg("Invalid Patron");
144     } elsif (!$patron->charge_ok) {
145                 $circ->screen_msg("Patron Blocked");
146     } elsif (!$item) {
147                 $circ->screen_msg("Invalid Item");
148     } elsif ($item->hold_queue && @{$item->hold_queue} && ($patron_id ne $item->hold_queue->[0])) {
149                 $circ->screen_msg("Item on Hold for Another User");
150     } elsif ($item->{patron} && ($item->{patron} ne $patron_id)) {
151         # I can't deal with this right now
152                 $circ->screen_msg("Item checked out to another patron");
153     } else {
154                 $circ->do_checkout();
155                 if ($circ->ok){
156                         warn "circ is ok";
157                         # If the item is already associated with this patron, then
158                         # we're renewing it.
159                         $circ->renew_ok($item->{patron} && ($item->{patron} eq $patron_id));
160                 
161                         $item->{patron} = $patron_id;
162 #               $item->{due_date} = time + (14*24*60*60); # two weeks
163                         $item->{due_date} = $circ->{due};
164 #                       warn "$item->{due_date}";
165                         push(@{$patron->{items}}, $item_id);
166                         $circ->desensitize(!$item->magnetic);
167
168                         syslog("LOG_DEBUG", "ILS::Checkout: patron %s has checked out %s",
169                                 $patron_id, join(', ', @{$patron->{items}}));
170                 }
171                 else {
172                         syslog("LOG_DEBUG", "ILS::Checkout Issue failed");
173                         
174                 }
175     }
176
177     # END TRANSACTION
178
179     return $circ;
180 }
181
182 sub checkin {
183     my ($self, $item_id, $trans_date, $return_date,
184         $current_loc, $item_props, $cancel) = @_;
185     my ($patron, $item, $circ);
186
187     $circ = new ILS::Transaction::Checkin;
188     # BEGIN TRANSACTION
189     $circ->item($item = new ILS::Item $item_id);
190
191     # It's ok to check it in if it exists, and if it was checked out
192     $circ->ok($item && $item->{patron});
193     $circ->do_checkin();    
194     if ($circ->ok) {
195                 $circ->patron($patron = new ILS::Patron $item->{patron});
196                 delete $item->{patron};
197                 delete $item->{due_date};
198                 $patron->{items} = [ grep {$_ ne $item_id} @{$patron->{items}} ];
199                 
200     }
201     # END TRANSACTION
202
203     return $circ;
204 }
205
206 # If the ILS caches patron information, this lets it free
207 # it up
208 sub end_patron_session {
209     my ($self, $patron_id) = @_;
210
211     # success?, screen_msg, print_line
212     return (1, 'Thank you !', '');
213 }
214
215 sub pay_fee {
216     my ($self, $patron_id, $patron_pwd, $fee_amt, $fee_type,
217         $pay_type, $fee_id, $trans_id, $currency) = @_;
218     my $trans;
219     my $patron;
220
221 #    $trans = new ILS::Transaction::FeePayment;
222
223     $patron = new ILS::Patron $patron_id;
224
225     $trans->transaction_id($trans_id);
226     $trans->patron($patron);
227     $trans->ok(1);
228
229     return $trans;
230 }
231
232 sub add_hold {
233     my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
234         $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
235     my ($patron, $item);
236     my $hold;
237     my $trans;
238
239
240     $trans = new ILS::Transaction::Hold;
241
242     # BEGIN TRANSACTION
243     $patron = new ILS::Patron $patron_id;
244     if (!$patron
245         || (defined($patron_pwd) && !$patron->check_password($patron_pwd))) {
246         $trans->screen_msg("Invalid Patron.");
247
248         return $trans;
249     }
250
251     $item = new ILS::Item ($item_id || $title_id);
252     if (!$item) {
253         $trans->screen_msg("No such item.");
254
255         # END TRANSACTION (conditionally)
256         return $trans;
257     } elsif ($item->fee && ($fee_ack ne 'Y')) {
258         $trans->screen_msg = "Fee required to place hold.";
259
260         # END TRANSACTION (conditionally)
261         return $trans;
262     }
263
264     $hold = {
265         item_id         => $item->id,
266         patron_id       => $patron->id,
267         expiration_date => $expiry_date,
268         pickup_location => $pickup_location,
269         hold_type       => $hold_type,
270     };
271
272     $trans->ok(1);
273     $trans->patron($patron);
274     $trans->item($item);
275     $trans->pickup_location($pickup_location);
276
277     push(@{$item->hold_queue}, $hold);
278     push(@{$patron->{hold_items}}, $hold);
279
280
281     # END TRANSACTION
282     return $trans;
283 }
284
285 sub cancel_hold {
286     my ($self, $patron_id, $patron_pwd, $item_id, $title_id) = @_;
287     my ($patron, $item, $hold);
288     my $trans;
289
290     $trans = new ILS::Transaction::Hold;
291
292     # BEGIN TRANSACTION
293     $patron = new ILS::Patron $patron_id;
294     if (!$patron) {
295         $trans->screen_msg("Invalid patron barcode.");
296
297         return $trans;
298     } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
299         $trans->screen_msg('Invalid patron password.');
300
301         return $trans;
302     }
303
304     $item = new ILS::Item ($item_id || $title_id);
305     if (!$item) {
306         $trans->screen_msg("No such item.");
307
308         # END TRANSACTION (conditionally)
309         return $trans;
310     }
311
312     # Remove the hold from the patron's record first
313     $trans->ok($patron->drop_hold($item_id));
314
315     if (!$trans->ok) {
316         # We didn't find it on the patron record
317         $trans->screen_msg("No such hold on patron record.");
318
319         # END TRANSACTION (conditionally)
320         return $trans;
321     }
322
323     # Now, remove it from the item record.  If it was on the patron
324     # record but not on the item record, we'll treat that as success.
325     foreach my $i (0 .. scalar @{$item->hold_queue}) {
326         $hold = $item->hold_queue->[$i];
327
328         if ($hold->{patron_id} eq $patron->id) {
329             # found it: delete it.
330             splice @{$item->hold_queue}, $i, 1;
331             last;
332         }
333     }
334
335     $trans->screen_msg("Hold Cancelled.");
336     $trans->patron($patron);
337     $trans->item($item);
338
339     return $trans;
340 }
341
342
343 # The patron and item id's can't be altered, but the
344 # date, location, and type can.
345 sub alter_hold {
346     my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
347         $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
348     my ($patron, $item);
349     my $hold;
350     my $trans;
351
352     $trans = new ILS::Transaction::Hold;
353
354     # BEGIN TRANSACTION
355     $patron = new ILS::Patron $patron_id;
356     if (!$patron) {
357         $trans->screen_msg("Invalid patron barcode.");
358
359         return $trans;
360     }
361
362     foreach my $i (0 .. scalar @{$patron->{hold_items}}) {
363         $hold = $patron->{hold_items}[$i];
364
365         if ($hold->{item_id} eq $item_id) {
366             # Found it.  So fix it.
367             $hold->{expiration_date} = $expiry_date if $expiry_date;
368             $hold->{pickup_location} = $pickup_location if $pickup_location;
369             $hold->{hold_type} = $hold_type if $hold_type;
370
371             $trans->ok(1);
372             $trans->screen_msg("Hold updated.");
373             $trans->patron($patron);
374             $trans->item(new ILS::Item $hold->{item_id});
375             last;
376         }
377     }
378
379     # The same hold structure is linked into both the patron's
380     # list of hold items and into the queue of outstanding holds
381     # for the item, so we don't need to search the hold queue for
382     # the item, since it's already been updated by the patron code.
383
384     if (!$trans->ok) {
385         $trans->screen_msg("No such outstanding hold.");
386     }
387
388     return $trans;
389 }
390
391 sub renew {
392     my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
393         $no_block, $nb_due_date, $third_party,
394         $item_props, $fee_ack) = @_;
395     my ($patron, $item);
396     my $trans;
397
398     $trans = new ILS::Transaction::Renew;
399
400     $trans->patron($patron = new ILS::Patron $patron_id);
401
402     if (!$patron) {
403         $trans->screen_msg("Invalid patron barcode.");
404
405         return $trans;
406     } elsif (!$patron->renew_ok) {
407         $trans->screen_msg("Renewals not allowed.");
408
409         return $trans;
410     }
411
412     if (defined($title_id)) {
413         # renewing a title, rather than an item (sort of)
414         # This is gross, but in a real ILS it would be better
415         foreach my $i (@{$patron->{items}}) {
416             $item = new ILS::Item $i;
417             last if ($title_id eq $item->title_id);
418             $item = undef;
419         }
420     } else {
421         foreach my $i (@{$patron->{items}}) {
422             if ($i == $item_id) {
423                 # We have it checked out
424                 $item = new ILS::Item $item_id;
425                 last;
426             }
427         }
428     }
429
430     $trans->item($item);
431
432     if (!defined($item)) {
433         # It's not checked out to $patron_id
434         $trans->screen_msg("Item not checked out to " . $patron->name);
435     } elsif (!$item->available($patron_id)) {
436          $trans->screen_msg("Item has outstanding holds");
437     } else {
438         $trans->renewal_ok(1);
439
440         $trans->desensitize(0); # It's already checked out
441
442         if ($no_block eq 'Y') {
443             $item->{due_date} = $nb_due_date;
444         } else {
445             $item->{due_date} = time + (14*24*60*60); # two weeks
446         }
447         if ($item_props) {
448             $item->{sip_item_properties} = $item_props;
449         }
450         $trans->ok(1);
451         $trans->renewal_ok(1);
452
453         return $trans;
454     }
455
456     return $trans;
457 }
458
459 sub renew_all {
460     my ($self, $patron_id, $patron_pwd, $fee_ack) = @_;
461     my ($patron, $item_id);
462     my $trans;
463
464     $trans = new ILS::Transaction::RenewAll;
465
466     $trans->patron($patron = new ILS::Patron $patron_id);
467     if (defined $patron) {
468         syslog("LOG_DEBUG", "ILS::renew_all: patron '%s': renew_ok: %s",
469                $patron->name, $patron->renew_ok);
470     } else {
471         syslog("LOG_DEBUG", "ILS::renew_all: Invalid patron id: '%s'",
472                $patron_id);
473     }
474
475     if (!defined($patron)) {
476         $trans->screen_msg("Invalid patron barcode.");
477         return $trans;
478     } elsif (!$patron->renew_ok) {
479         $trans->screen_msg("Renewals not allowed.");
480         return $trans;
481     } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
482         $trans->screen_msg("Invalid patron password.");
483         return $trans;
484     }
485
486     foreach $item_id (@{$patron->{items}}) {
487         my $item = new ILS::Item $item_id;
488
489         if (!defined($item)) {
490             syslog("LOG_WARNING",
491                    "renew_all: Invalid item id associated with patron '%s'",
492                    $patron->id);
493             next;
494         }
495
496         if (@{$item->hold_queue}) {
497             # Can't renew if there are outstanding holds
498             push @{$trans->unrenewed}, $item_id;
499         } else {
500             $item->{due_date} = time + (14*24*60*60); # two weeks hence
501             push @{$trans->renewed}, $item_id;
502         }
503     }
504
505     $trans->ok(1);
506
507     return $trans;
508 }
509
510 1;