]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/SIP.pm
TPac: Multiple holds in staff client place-holds session
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / SIP.pm
1 #
2 # ILS.pm: Test ILS interface module
3 #
4
5 package OpenILS::SIP;
6 use warnings; use strict;
7
8 use Sys::Syslog qw(syslog);
9 use Time::HiRes q/time/;
10
11 use OpenILS::SIP::Item;
12 use OpenILS::SIP::Patron;
13 use OpenILS::SIP::Transaction;
14 use OpenILS::SIP::Transaction::Checkout;
15 use OpenILS::SIP::Transaction::Checkin;
16 use OpenILS::SIP::Transaction::Renew;
17 use OpenILS::SIP::Transaction::FeePayment;
18
19 use OpenSRF::System;
20 use OpenILS::Utils::Fieldmapper;
21 use OpenSRF::Utils::SettingsClient;
22 use OpenILS::Application::AppUtils;
23 use OpenSRF::Utils qw/:datetime/;
24 use DateTime::Format::ISO8601;
25 use Encode;
26 use Unicode::Normalize;
27 my $U = 'OpenILS::Application::AppUtils';
28
29 my $editor;
30 my $config;
31 my $target_encoding;    # FIXME: this is configured at the institution level. 
32
33 use Digest::MD5 qw(md5_hex);
34
35 sub new {
36         my ($class, $institution, $login) = @_;
37         my $type = ref($class) || $class;
38         my $self = {};
39
40         $self->{login} = $login;
41
42         $config = $institution;
43         syslog("LOG_DEBUG", "OILS: new ILS '%s'", $institution->{id});
44         $self->{institution} = $institution;
45
46         my $bsconfig     = $institution->{implementation_config}->{bootstrap};
47         $target_encoding = $institution->{implementation_config}->{encoding} || 'ascii';
48
49         syslog('LOG_DEBUG', "OILS: loading bootstrap config: $bsconfig");
50         
51         local $/ = "\n";    # why?
52         OpenSRF::System->bootstrap_client(config_file => $bsconfig);
53         syslog('LOG_DEBUG', "OILS: bootstrap loaded..");
54
55         $self->{osrf_config} = OpenSRF::Utils::SettingsClient->new;
56
57         Fieldmapper->import($self->{osrf_config}->config_value('IDL'));
58
59         bless( $self, $type );
60
61         return undef unless 
62                 $self->login( $login->{id}, $login->{password} );
63
64         return $self;
65 }
66
67 sub fetch_session {
68     my $self = shift;
69
70         my $ses = $U->simplereq( 
71                 'open-ils.auth',
72                 'open-ils.auth.session.retrieve',  $self->{authtoken});
73
74     return undef if $U->event_code($ses); # auth timed out
75     return $self->{login_session} = $ses;
76 }
77
78 sub verify_session {
79         my $self = shift;
80
81     return 1 if $self->fetch_session;
82
83     syslog('LOG_INFO', "OILS: Logging back after session timeout as user ".$self->{login}->{id});
84     return $self->login( $self->{login}->{id}, $self->{login}->{password} );
85 }
86
87 sub editor {
88         return $editor = make_editor();
89 }
90
91 sub config {
92         return $config;
93 }
94
95 sub get_option_value {
96     my($self, $option) = @_;
97     my $ops = $config->{implementation_config}->{options}->{option};
98     $ops = [$ops] unless ref $ops eq 'ARRAY';
99     my @vals = grep { $_->{name} eq $option } @$ops;
100     return @vals ? $vals[0]->{value} : undef;
101 }
102
103
104 # Creates the global editor object
105 my $cstore_init = 1; # call init on first use
106 sub make_editor {
107     OpenILS::Utils::CStoreEditor::init() if $cstore_init;
108     $cstore_init = 0;
109         return OpenILS::Utils::CStoreEditor->new;
110 }
111
112 =head2 clean_text(scalar)
113
114 Evergreen uses the UTF8 encoding for everything from the database up. Perl
115 doesn't know this, however, so we have to convince it to treat our UTF8 strings
116 as UTF8 strings. This may enable OpenNCIP to correctly calculate the checksums
117 for UTF8 text for SIP clients that support such modern options.
118
119 The target encoding is set in the <encoding> element of the SIPServer.pm
120 configuration file.
121
122 =cut
123
124 sub clean_text {
125     my $text = shift || '';
126
127     # Convert our incoming UTF8 data into Perl's internal string format
128
129     # Also convert to Normalization Form D, as the ASCII, iso-8859-1,
130     # and latin-1 encodings (at least) require this to substitute
131     # characters rather than simply returning a string truncated
132     # after the first non-ASCII character
133     $text = NFD(decode_utf8($text));
134
135     if ($target_encoding eq 'ascii') {
136
137         # Try to maintain a reasonable version of the content by
138         # stripping diacritics from the text, given that the SIP client
139         # wants just plain ASCII. This is the base requirement according
140         # to the SIP2 specification.
141
142         # Stripping the combining characters converts ""béè♁ts"
143         # into "bee?ts" instead of "b???ts" - better, eh?
144         $text =~ s/\pM+//og;
145     }
146
147     # Characters that cannot be represented in the target encoding will
148     # generally be replaced with a question mark (?) character.
149     $text = encode($target_encoding, $text);
150
151     return $text;
152 }
153
154 my %org_sn_cache;
155 sub shortname_from_id {
156     my $id = shift or return;
157     return $id->shortname if ref $id;
158     return $org_sn_cache{$id} if $org_sn_cache{$id};
159     return $org_sn_cache{$id} = editor()->retrieve_actor_org_unit($id)->shortname;
160 }
161 sub patron_barcode_from_id {
162     my $id = shift or return;
163     return editor()->search_actor_card({ usr => $id, active => 't' })->[0]->barcode;
164 }
165
166 sub format_date {
167         my $class = shift;
168         my $date = shift;
169         my $type = shift || 'dob';
170
171         return "" unless $date;
172
173         $date = DateTime::Format::ISO8601->new->
174                 parse_datetime(OpenSRF::Utils::cleanse_ISO8601($date));
175         my @time = localtime($date->epoch);
176
177         my $year   = $time[5]+1900;
178         my $mon    = $time[4]+1;
179         my $day    = $time[3];
180         my $hour   = $time[2];
181         my $minute = $time[1];
182         my $second = $time[0];
183   
184         $date = sprintf("%04d%02d%02d", $year, $mon, $day);
185
186         # Due dates need hyphen separators and time of day as well
187         if ($type eq 'due') {
188                 $date = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year, $mon, $day, $hour, $minute, $second);
189         }
190
191         syslog('LOG_DEBUG', "OILS: formatted date [type=$type]: $date");
192         return $date;
193 }
194
195
196
197 sub login {
198         my( $self, $username, $password ) = @_;
199         syslog('LOG_DEBUG', "OILS: Logging in with username $username");
200
201         my $seed = $U->simplereq( 
202                 'open-ils.auth',
203                 'open-ils.auth.authenticate.init', $username );
204
205         my $response = $U->simplereq(
206                 'open-ils.auth', 
207                 'open-ils.auth.authenticate.complete', 
208                 {       
209                         username => $username, 
210                         password => md5_hex($seed . md5_hex($password)), 
211                         type     => 'opac',
212                 }
213         );
214
215         if( my $code = $U->event_code($response) ) {
216                 my $txt = $response->{textcode};
217                 syslog('LOG_WARNING', "OILS: Login failed for $username.  $txt:$code");
218                 return undef;
219         }
220
221         my $key = $response->{payload}->{authtoken};
222         syslog('LOG_INFO', "OILS: Login succeeded for $username : authkey = $key");
223
224     $self->fetch_session; # to cache the login
225
226         return $self->{authtoken} = $key;
227 }
228
229
230 sub find_patron {
231         my $self = shift;
232         return OpenILS::SIP::Patron->new(@_);
233 }
234
235
236 sub find_item {
237         my $self = shift;
238         return OpenILS::SIP::Item->new(@_);
239 }
240
241
242 sub institution {
243     my $self = shift;
244     return $self->{institution}->{id};  # consider making this return the whole institution
245 }
246
247 sub institution_id {
248     my $self = shift;
249     return $self->{institution}->{id};  # then use this for just the ID
250 }
251
252 sub supports {
253         my ($self, $op) = @_;
254         my ($i) = grep { $_->{name} eq $op }  
255                 @{$config->{implementation_config}->{supports}->{item}};
256         return to_bool($i->{value});
257 }
258
259 sub check_inst_id {
260     my ($self, $id, $whence) = @_;
261     if ($id ne $self->{institution}->{id}) {
262         syslog("LOG_WARNING", "OILS: %s: received institution '%s', expected '%s'", $whence, $id, $self->{institution}->{id});
263         # Just an FYI check, we don't expect the user to change location from that in SIPconfig.xml
264     }
265 }
266
267
268 sub to_bool {
269     my $bool = shift;
270     # If it's defined, and matches a true sort of string, or is
271     # a non-zero number, then it's true.
272     defined($bool) or return;                   # false
273     ($bool =~ /true|y|yes/i) and return 1;      # true
274     return ($bool =~ /^\d+$/ and $bool != 0);   # true for non-zero numbers, false otherwise
275 }
276
277 sub checkout_ok {
278         return to_bool($config->{policy}->{checkout});
279 }
280
281 sub checkin_ok {
282         return to_bool($config->{policy}->{checkin});
283 }
284
285 sub renew_ok {
286         return to_bool($config->{policy}->{renew});
287 }
288
289 sub status_update_ok {
290         return to_bool($config->{policy}->{status_update});
291 }
292
293 sub offline_ok {
294         return to_bool($config->{policy}->{offline});
295 }
296
297
298
299 ##
300 ## Checkout(patron_id, item_id, sc_renew, fee_ack):
301 ##    patron_id & item_id are the identifiers send by the terminal
302 ##    sc_renew is the renewal policy configured on the terminal
303 ## returns a status opject that can be queried for the various bits
304 ## of information that the protocol (SIP or NCIP) needs to generate
305 ## the response.
306 ##    fee_ack is the fee_acknowledged field (BO) sent from the sc
307 ## when doing chargeable loans.
308 ##
309
310 sub checkout {
311         my ($self, $patron_id, $item_id, $sc_renew, $fee_ack) = @_;
312         $sc_renew = 0;
313
314         $self->verify_session;
315
316         syslog('LOG_DEBUG', "OILS: OpenILS::Checkout attempt: patron=$patron_id, item=$item_id");
317
318     my $xact   = OpenILS::SIP::Transaction::Checkout->new( authtoken => $self->{authtoken} );
319     my $patron = $self->find_patron($patron_id);
320     my $item   = $self->find_item($item_id);
321
322         $xact->patron($patron);
323         $xact->item($item);
324
325         if (!$patron) {
326                 $xact->screen_msg("Invalid Patron Barcode '$patron_id'");
327                 return $xact;
328         }
329
330         if (!$patron->charge_ok) {
331                 $xact->screen_msg("Patron Blocked");
332                 return $xact;
333         }
334
335         if( !$item ) {
336                 $xact->screen_msg("Invalid Item Barcode: '$item_id'");
337                 return $xact;
338         }
339
340         syslog('LOG_DEBUG', "OILS: OpenILS::Checkout data loaded OK, checking out...");
341
342         if ($item->{patron} && ($item->{patron} eq $patron_id)) {
343                 syslog('LOG_INFO', "OILS: OpenILS::Checkout data loaded OK, doing renew...");
344                 $sc_renew = 1;
345         } elsif ($item->{patron} && ($item->{patron} ne $patron_id)) {
346                 # I can't deal with this right now
347                 # XXX check in then check out?
348                 $xact->screen_msg("Item checked out to another patron");
349                 $xact->ok(0);
350         } 
351
352         # Check for fee and $fee_ack. If there is a fee, and $fee_ack
353         # is 'Y', we proceed, otherwise we reject the checkout.
354         if ($item->fee > 0.0) {
355             $xact->fee_amount($item->fee);
356             $xact->sip_fee_type($item->sip_fee_type);
357             $xact->sip_currency($item->fee_currency);
358             if ($fee_ack && $fee_ack eq 'Y') {
359                 $xact->fee_ack(1);
360             } else {
361                 $xact->screen_msg('Fee required');
362                 $xact->ok(0);
363                 return $xact;
364             }
365         }
366
367         $xact->do_checkout($sc_renew);
368         $xact->desensitize(!$item->magnetic);
369
370         if( $xact->ok ) {
371                 #editor()->commit;
372                 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
373                         "patron %s checkout %s succeeded", $patron_id, $item_id);
374         } else {
375                 #editor()->xact_rollback;
376                 syslog("LOG_DEBUG", "OILS: OpenILS::Checkout: " .
377                         "patron %s checkout %s FAILED, rolling back xact...", $patron_id, $item_id);
378         }
379
380         return $xact;
381 }
382
383
384 sub checkin {
385         my ($self, $item_id, $inst_id, $trans_date, $return_date,
386         $current_loc, $item_props, $cancel) = @_;
387
388     my $start_time = time();
389
390         $self->verify_session;
391
392         syslog('LOG_DEBUG', "OILS: OpenILS::Checkin of item=$item_id (to $inst_id)");
393         
394     my $xact = OpenILS::SIP::Transaction::Checkin->new(authtoken => $self->{authtoken});
395     my $item = OpenILS::SIP::Item->new($item_id);
396
397     unless ( $xact->item($item) ) {
398         $xact->ok(0);
399         # $circ->alert(1); $circ->alert_type(99);
400         $xact->screen_msg("Invalid Item Barcode: '$item_id'");
401         syslog('LOG_INFO', "OILS: Checkin failed.  " . $xact->screen_msg() );
402         return $xact;
403     }
404
405         $xact->do_checkin( $self, $inst_id, $trans_date, $return_date, $current_loc, $item_props );
406         
407         if ($xact->ok) {
408         $xact->patron($self->find_patron(usr => $xact->{circ_user_id}, slim_user => 1)) if $xact->{circ_user_id};
409         delete $item->{patron};
410         delete $item->{due_date};
411         syslog('LOG_INFO', "OILS: Checkin succeeded");
412     } else {
413         syslog('LOG_WARNING', "OILS: Checkin failed");
414     }
415
416     syslog('LOG_INFO', "OILS: SIP Checkin request took %0.3f seconds", (time() - $start_time));
417         return $xact;
418 }
419
420 ## If the ILS caches patron information, this lets it free it up.
421 ## Also, this could be used for centrally logging session duration.
422 ## We don't do anything with it.
423 sub end_patron_session {
424     my ($self, $patron_id) = @_;
425     return (1, 'Thank you!', '');
426 }
427
428
429 sub pay_fee {
430     my ($self, $patron_id, $patron_pwd, $fee_amt, $fee_type,
431         $pay_type, $fee_id, $trans_id, $currency) = @_;
432
433     my $xact = OpenILS::SIP::Transaction::FeePayment->new(authtoken => $self->{authtoken});
434     my $patron = $self->find_patron($patron_id);
435
436     if (!$patron) {
437         $xact->screen_msg("Invalid Patron Barcode '$patron_id'");
438         $xact->ok(0);
439         return $xact;
440     }
441
442     $xact->patron($patron);
443     $xact->sip_currency($currency);
444     $xact->fee_amount($fee_amt);
445     $xact->sip_fee_type($fee_type);
446     $xact->transaction_id($trans_id);
447     $xact->fee_id($fee_id);
448     # We don't presently use these, but we might in the future.
449     $xact->patron_password($patron_pwd);
450     $xact->sip_payment_type($pay_type);
451
452     $xact->do_fee_payment();
453
454     return $xact;
455 }
456
457 #sub add_hold {
458 #    my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
459 #       $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
460 #    my ($patron, $item);
461 #    my $hold;
462 #    my $trans;
463 #
464 #
465 #    $trans = new ILS::Transaction::Hold;
466 #
467 #    # BEGIN TRANSACTION
468 #    $patron = new ILS::Patron $patron_id;
469 #    if (!$patron
470 #       || (defined($patron_pwd) && !$patron->check_password($patron_pwd))) {
471 #       $trans->screen_msg("Invalid Patron.");
472 #
473 #       return $trans;
474 #    }
475 #
476 #    $item = new ILS::Item ($item_id || $title_id);
477 #    if (!$item) {
478 #       $trans->screen_msg("No such item.");
479 #
480 #       # END TRANSACTION (conditionally)
481 #       return $trans;
482 #    } elsif ($item->fee && ($fee_ack ne 'Y')) {
483 #       $trans->screen_msg = "Fee required to place hold.";
484 #
485 #       # END TRANSACTION (conditionally)
486 #       return $trans;
487 #    }
488 #
489 #    $hold = {
490 #       item_id         => $item->id,
491 #       patron_id       => $patron->id,
492 #       expiration_date => $expiry_date,
493 #       pickup_location => $pickup_location,
494 #       hold_type       => $hold_type,
495 #    };
496 #
497 #    $trans->ok(1);
498 #    $trans->patron($patron);
499 #    $trans->item($item);
500 #    $trans->pickup_location($pickup_location);
501 #
502 #    push(@{$item->hold_queue}, $hold);
503 #    push(@{$patron->{hold_items}}, $hold);
504 #
505 #
506 #    # END TRANSACTION
507 #    return $trans;
508 #}
509 #
510 #sub cancel_hold {
511 #    my ($self, $patron_id, $patron_pwd, $item_id, $title_id) = @_;
512 #    my ($patron, $item, $hold);
513 #    my $trans;
514 #
515 #    $trans = new ILS::Transaction::Hold;
516 #
517 #    # BEGIN TRANSACTION
518 #    $patron = new ILS::Patron $patron_id;
519 #    if (!$patron) {
520 #       $trans->screen_msg("Invalid patron barcode.");
521 #
522 #       return $trans;
523 #    } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
524 #       $trans->screen_msg('Invalid patron password.');
525 #
526 #       return $trans;
527 #    }
528 #
529 #    $item = new ILS::Item ($item_id || $title_id);
530 #    if (!$item) {
531 #       $trans->screen_msg("No such item.");
532 #
533 #       # END TRANSACTION (conditionally)
534 #       return $trans;
535 #    }
536 #
537 #    # Remove the hold from the patron's record first
538 #    $trans->ok($patron->drop_hold($item_id));
539 #
540 #    if (!$trans->ok) {
541 #       # We didn't find it on the patron record
542 #       $trans->screen_msg("No such hold on patron record.");
543 #
544 #       # END TRANSACTION (conditionally)
545 #       return $trans;
546 #    }
547 #
548 #    # Now, remove it from the item record.  If it was on the patron
549 #    # record but not on the item record, we'll treat that as success.
550 #    foreach my $i (0 .. scalar @{$item->hold_queue}) {
551 #       $hold = $item->hold_queue->[$i];
552 #
553 #       if ($hold->{patron_id} eq $patron->id) {
554 #           # found it: delete it.
555 #           splice @{$item->hold_queue}, $i, 1;
556 #           last;
557 #       }
558 #    }
559 #
560 #    $trans->screen_msg("Hold Cancelled.");
561 #    $trans->patron($patron);
562 #    $trans->item($item);
563 #
564 #    return $trans;
565 #}
566 #
567 #
568 ## The patron and item id's can't be altered, but the
569 ## date, location, and type can.
570 #sub alter_hold {
571 #    my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
572 #       $expiry_date, $pickup_location, $hold_type, $fee_ack) = @_;
573 #    my ($patron, $item);
574 #    my $hold;
575 #    my $trans;
576 #
577 #    $trans = new ILS::Transaction::Hold;
578 #
579 #    # BEGIN TRANSACTION
580 #    $patron = new ILS::Patron $patron_id;
581 #    if (!$patron) {
582 #       $trans->screen_msg("Invalid patron barcode.");
583 #
584 #       return $trans;
585 #    }
586 #
587 #    foreach my $i (0 .. scalar @{$patron->{hold_items}}) {
588 #       $hold = $patron->{hold_items}[$i];
589 #
590 #       if ($hold->{item_id} eq $item_id) {
591 #           # Found it.  So fix it.
592 #           $hold->{expiration_date} = $expiry_date if $expiry_date;
593 #           $hold->{pickup_location} = $pickup_location if $pickup_location;
594 #           $hold->{hold_type} = $hold_type if $hold_type;
595 #
596 #           $trans->ok(1);
597 #           $trans->screen_msg("Hold updated.");
598 #           $trans->patron($patron);
599 #           $trans->item(new ILS::Item $hold->{item_id});
600 #           last;
601 #       }
602 #    }
603 #
604 #    # The same hold structure is linked into both the patron's
605 #    # list of hold items and into the queue of outstanding holds
606 #    # for the item, so we don't need to search the hold queue for
607 #    # the item, since it's already been updated by the patron code.
608 #
609 #    if (!$trans->ok) {
610 #       $trans->screen_msg("No such outstanding hold.");
611 #    }
612 #
613 #    return $trans;
614 #}
615
616
617 sub renew {
618         my ($self, $patron_id, $patron_pwd, $item_id, $title_id,
619                 $no_block, $nb_due_date, $third_party, $item_props, $fee_ack) = @_;
620
621         $self->verify_session;
622
623         my $trans = OpenILS::SIP::Transaction::Renew->new( authtoken => $self->{authtoken} );
624         $trans->patron($self->find_patron($patron_id));
625         $trans->item($self->find_item($item_id));
626
627         if(!$trans->patron) {
628                 $trans->screen_msg("Invalid patron barcode.");
629                 $trans->ok(0);
630                 return $trans;
631         }
632
633         if(!$trans->patron->renew_ok) {
634                 $trans->screen_msg("Renewals not allowed.");
635                 $trans->ok(0);
636                 return $trans;
637         }
638
639         if(!$trans->item) {
640                 if( $title_id ) {
641                         $trans->screen_msg("Title ID renewal not supported.  Use item barcode.");
642                 } else {
643                         $trans->screen_msg("Invalid item barcode.");
644                 }
645                 $trans->ok(0);
646                 return $trans;
647         }
648
649         if(!$trans->item->{patron} or 
650                         $trans->item->{patron} ne $patron_id) {
651                 $trans->screen_msg("Item not checked out to " . $trans->patron->name);
652                 $trans->ok(0);
653                 return $trans;
654         }
655
656         # Perform the renewal
657         $trans->do_renew();
658
659         $trans->desensitize(0); # It's already checked out
660         $trans->item->{due_date} = $nb_due_date if $no_block eq 'Y';
661         $trans->item->{sip_item_properties} = $item_props if $item_props;
662
663         return $trans;
664 }
665
666
667
668
669
670 #
671 #sub renew_all {
672 #    my ($self, $patron_id, $patron_pwd, $fee_ack) = @_;
673 #    my ($patron, $item_id);
674 #    my $trans;
675 #
676 #    $trans = new ILS::Transaction::RenewAll;
677 #
678 #    $trans->patron($patron = new ILS::Patron $patron_id);
679 #    if (defined $patron) {
680 #       syslog("LOG_DEBUG", "ILS::renew_all: patron '%s': renew_ok: %s",
681 #              $patron->name, $patron->renew_ok);
682 #    } else {
683 #       syslog("LOG_DEBUG", "ILS::renew_all: Invalid patron id: '%s'",
684 #              $patron_id);
685 #    }
686 #
687 #    if (!defined($patron)) {
688 #       $trans->screen_msg("Invalid patron barcode.");
689 #       return $trans;
690 #    } elsif (!$patron->renew_ok) {
691 #       $trans->screen_msg("Renewals not allowed.");
692 #       return $trans;
693 #    } elsif (defined($patron_pwd) && !$patron->check_password($patron_pwd)) {
694 #       $trans->screen_msg("Invalid patron password.");
695 #       return $trans;
696 #    }
697 #
698 #    foreach $item_id (@{$patron->{items}}) {
699 #       my $item = new ILS::Item $item_id;
700 #
701 #       if (!defined($item)) {
702 #           syslog("LOG_WARNING",
703 #                  "renew_all: Invalid item id associated with patron '%s'",
704 #                  $patron->id);
705 #           next;
706 #       }
707 #
708 #       if (@{$item->hold_queue}) {
709 #           # Can't renew if there are outstanding holds
710 #           push @{$trans->unrenewed}, $item_id;
711 #       } else {
712 #           $item->{due_date} = time + (14*24*60*60); # two weeks hence
713 #           push @{$trans->renewed}, $item_id;
714 #       }
715 #    }
716 #
717 #    $trans->ok(1);
718 #
719 #    return $trans;
720 #}
721
722 1;