Bug 19036: (QA follow-up) Use Koha::DateUtils
[koha.git] / Koha / Item.pm
blob8602b9a308a9fed458fefebcbb44005c2fe74f21
1 package Koha::Item;
3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20 use Modern::Perl;
22 use Carp;
23 use List::MoreUtils qw(any);
24 use Data::Dumper;
25 use Try::Tiny;
27 use Koha::Database;
28 use Koha::DateUtils qw( dt_from_string );
30 use C4::Context;
31 use C4::Circulation;
32 use C4::Reserves;
33 use C4::Biblio qw( ModZebra ); # FIXME This is terrible, we should move the indexation code outside of C4::Biblio
34 use C4::ClassSource; # FIXME We would like to avoid that
35 use C4::Log qw( logaction );
37 use Koha::Checkouts;
38 use Koha::CirculationRules;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
41 use Koha::ItemTypes;
42 use Koha::Patrons;
43 use Koha::Plugins;
44 use Koha::Libraries;
45 use Koha::StockRotationItem;
46 use Koha::StockRotationRotas;
48 use base qw(Koha::Object);
50 =head1 NAME
52 Koha::Item - Koha Item object class
54 =head1 API
56 =head2 Class methods
58 =cut
60 =head3 store
62 $item->store;
64 $params can take an optional 'skip_modzebra_update' parameter.
65 If set, the reindexation process will not happen (ModZebra not called)
67 NOTE: This is a temporary fix to answer a performance issue when lot of items
68 are added (or modified) at the same time.
69 The correct way to fix this is to make the ES reindexation process async.
70 You should not turn it on if you do not understand what it is doing exactly.
72 =cut
74 sub store {
75 my $self = shift;
76 my $params = @_ ? shift : {};
78 my $log_action = $params->{log_action} // 1;
80 # We do not want to oblige callers to pass this value
81 # Dev conveniences vs performance?
82 unless ( $self->biblioitemnumber ) {
83 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
86 # See related changes from C4::Items::AddItem
87 unless ( $self->itype ) {
88 $self->itype($self->biblio->biblioitem->itemtype);
91 my $today = dt_from_string;
92 unless ( $self->in_storage ) { #AddItem
93 unless ( $self->permanent_location ) {
94 $self->permanent_location($self->location);
96 unless ( $self->replacementpricedate ) {
97 $self->replacementpricedate($today);
99 unless ( $self->datelastseen ) {
100 $self->datelastseen($today);
103 unless ( $self->dateaccessioned ) {
104 $self->dateaccessioned($today);
107 if ( $self->itemcallnumber
108 or $self->cn_source )
110 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
111 $self->cn_sort($cn_sort);
114 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
115 unless $params->{skip_modzebra_update};
117 logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
118 if $log_action && C4::Context->preference("CataloguingLog");
120 $self->_after_item_action_hooks({ action => 'create' });
122 } else { # ModItem
124 { # Update *_on fields if needed
125 # Why not for AddItem as well?
126 my @fields = qw( itemlost withdrawn damaged );
128 # Only retrieve the item if we need to set an "on" date field
129 if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
130 my $pre_mod_item = $self->get_from_storage;
131 for my $field (@fields) {
132 if ( $self->$field
133 and not $pre_mod_item->$field )
135 my $field_on = "${field}_on";
136 $self->$field_on(
137 DateTime::Format::MySQL->format_datetime( dt_from_string() )
143 # If the field is defined but empty, we are removing and,
144 # and thus need to clear out the 'on' field as well
145 for my $field (@fields) {
146 if ( defined( $self->$field ) && !$self->$field ) {
147 my $field_on = "${field}_on";
148 $self->$field_on(undef);
153 my %updated_columns = $self->_result->get_dirty_columns;
154 return $self->SUPER::store unless %updated_columns;
156 if ( exists $updated_columns{itemcallnumber}
157 or exists $updated_columns{cn_source} )
159 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
160 $self->cn_sort($cn_sort);
164 if ( exists $updated_columns{location}
165 and $self->location ne 'CART'
166 and $self->location ne 'PROC'
167 and not exists $updated_columns{permanent_location} )
169 $self->permanent_location( $self->location );
172 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
173 unless $params->{skip_modzebra_update};
175 $self->_after_item_action_hooks({ action => 'modify' });
177 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
178 if $log_action && C4::Context->preference("CataloguingLog");
181 unless ( $self->dateaccessioned ) {
182 $self->dateaccessioned($today);
185 return $self->SUPER::store;
188 =head3 delete
190 =cut
192 sub delete {
193 my $self = shift;
194 my $params = @_ ? shift : {};
196 # FIXME check the item has no current issues
197 # i.e. raise the appropriate exception
199 C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
200 unless $params->{skip_modzebra_update};
202 $self->_after_item_action_hooks({ action => 'delete' });
204 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
205 if C4::Context->preference("CataloguingLog");
207 return $self->SUPER::delete;
210 =head3 safe_delete
212 =cut
214 sub safe_delete {
215 my $self = shift;
216 my $params = @_ ? shift : {};
218 my $safe_to_delete = $self->safe_to_delete;
219 return $safe_to_delete unless $safe_to_delete eq '1';
221 $self->move_to_deleted;
223 return $self->delete($params);
226 =head3 safe_to_delete
228 returns 1 if the item is safe to delete,
230 "book_on_loan" if the item is checked out,
232 "not_same_branch" if the item is blocked by independent branches,
234 "book_reserved" if the there are holds aganst the item, or
236 "linked_analytics" if the item has linked analytic records.
238 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
240 =cut
242 sub safe_to_delete {
243 my ($self) = @_;
245 return "book_on_loan" if $self->checkout;
247 return "not_same_branch"
248 if defined C4::Context->userenv
249 and !C4::Context->IsSuperLibrarian()
250 and C4::Context->preference("IndependentBranches")
251 and ( C4::Context->userenv->{branch} ne $self->homebranch );
253 # check it doesn't have a waiting reserve
254 return "book_reserved"
255 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
257 return "linked_analytics"
258 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
260 return "last_item_for_hold"
261 if $self->biblio->items->count == 1
262 && $self->biblio->holds->search(
264 itemnumber => undef,
266 )->count;
268 return 1;
271 =head3 move_to_deleted
273 my $is_moved = $item->move_to_deleted;
275 Move an item to the deleteditems table.
276 This can be done before deleting an item, to make sure the data are not completely deleted.
278 =cut
280 sub move_to_deleted {
281 my ($self) = @_;
282 my $item_infos = $self->unblessed;
283 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
284 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
288 =head3 effective_itemtype
290 Returns the itemtype for the item based on whether item level itemtypes are set or not.
292 =cut
294 sub effective_itemtype {
295 my ( $self ) = @_;
297 return $self->_result()->effective_itemtype();
300 =head3 home_branch
302 =cut
304 sub home_branch {
305 my ($self) = @_;
307 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
309 return $self->{_home_branch};
312 =head3 holding_branch
314 =cut
316 sub holding_branch {
317 my ($self) = @_;
319 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
321 return $self->{_holding_branch};
324 =head3 biblio
326 my $biblio = $item->biblio;
328 Return the bibliographic record of this item
330 =cut
332 sub biblio {
333 my ( $self ) = @_;
334 my $biblio_rs = $self->_result->biblio;
335 return Koha::Biblio->_new_from_dbic( $biblio_rs );
338 =head3 biblioitem
340 my $biblioitem = $item->biblioitem;
342 Return the biblioitem record of this item
344 =cut
346 sub biblioitem {
347 my ( $self ) = @_;
348 my $biblioitem_rs = $self->_result->biblioitem;
349 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
352 =head3 checkout
354 my $checkout = $item->checkout;
356 Return the checkout for this item
358 =cut
360 sub checkout {
361 my ( $self ) = @_;
362 my $checkout_rs = $self->_result->issue;
363 return unless $checkout_rs;
364 return Koha::Checkout->_new_from_dbic( $checkout_rs );
367 =head3 holds
369 my $holds = $item->holds();
370 my $holds = $item->holds($params);
371 my $holds = $item->holds({ found => 'W'});
373 Return holds attached to an item, optionally accept a hashref of params to pass to search
375 =cut
377 sub holds {
378 my ( $self,$params ) = @_;
379 my $holds_rs = $self->_result->reserves->search($params);
380 return Koha::Holds->_new_from_dbic( $holds_rs );
383 =head3 get_transfer
385 my $transfer = $item->get_transfer;
387 Return the transfer if the item is in transit or undef
389 =cut
391 sub get_transfer {
392 my ( $self ) = @_;
393 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
394 return unless $transfer_rs;
395 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
398 =head3 last_returned_by
400 Gets and sets the last borrower to return an item.
402 Accepts and returns Koha::Patron objects
404 $item->last_returned_by( $borrowernumber );
406 $last_returned_by = $item->last_returned_by();
408 =cut
410 sub last_returned_by {
411 my ( $self, $borrower ) = @_;
413 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
415 if ($borrower) {
416 return $items_last_returned_by_rs->update_or_create(
417 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
419 else {
420 unless ( $self->{_last_returned_by} ) {
421 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
422 if ($result) {
423 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
427 return $self->{_last_returned_by};
431 =head3 can_article_request
433 my $bool = $item->can_article_request( $borrower )
435 Returns true if item can be specifically requested
437 $borrower must be a Koha::Patron object
439 =cut
441 sub can_article_request {
442 my ( $self, $borrower ) = @_;
444 my $rule = $self->article_request_type($borrower);
446 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
447 return q{};
450 =head3 hidden_in_opac
452 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
454 Returns true if item fields match the hidding criteria defined in $rules.
455 Returns false otherwise.
457 Takes HASHref that can have the following parameters:
458 OPTIONAL PARAMETERS:
459 $rules : { <field> => [ value_1, ... ], ... }
461 Note: $rules inherits its structure from the parsed YAML from reading
462 the I<OpacHiddenItems> system preference.
464 =cut
466 sub hidden_in_opac {
467 my ( $self, $params ) = @_;
469 my $rules = $params->{rules} // {};
471 return 1
472 if C4::Context->preference('hidelostitems') and
473 $self->itemlost > 0;
475 my $hidden_in_opac = 0;
477 foreach my $field ( keys %{$rules} ) {
479 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
480 $hidden_in_opac = 1;
481 last;
485 return $hidden_in_opac;
488 =head3 can_be_transferred
490 $item->can_be_transferred({ to => $to_library, from => $from_library })
491 Checks if an item can be transferred to given library.
493 This feature is controlled by two system preferences:
494 UseBranchTransferLimits to enable / disable the feature
495 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
496 for setting the limitations
498 Takes HASHref that can have the following parameters:
499 MANDATORY PARAMETERS:
500 $to : Koha::Library
501 OPTIONAL PARAMETERS:
502 $from : Koha::Library # if not given, item holdingbranch
503 # will be used instead
505 Returns 1 if item can be transferred to $to_library, otherwise 0.
507 To find out whether at least one item of a Koha::Biblio can be transferred, please
508 see Koha::Biblio->can_be_transferred() instead of using this method for
509 multiple items of the same biblio.
511 =cut
513 sub can_be_transferred {
514 my ($self, $params) = @_;
516 my $to = $params->{to};
517 my $from = $params->{from};
519 $to = $to->branchcode;
520 $from = defined $from ? $from->branchcode : $self->holdingbranch;
522 return 1 if $from eq $to; # Transfer to current branch is allowed
523 return 1 unless C4::Context->preference('UseBranchTransferLimits');
525 my $limittype = C4::Context->preference('BranchTransferLimitsType');
526 return Koha::Item::Transfer::Limits->search({
527 toBranch => $to,
528 fromBranch => $from,
529 $limittype => $limittype eq 'itemtype'
530 ? $self->effective_itemtype : $self->ccode
531 })->count ? 0 : 1;
534 =head3 pickup_locations
536 $pickup_locations = $item->pickup_locations( {patron => $patron } )
538 Returns possible pickup locations for this item, according to patron's home library (if patron is defined and holds are allowed only from hold groups)
539 and if item can be transferred to each pickup location.
541 =cut
543 sub pickup_locations {
544 my ($self, $params) = @_;
546 my $patron = $params->{patron};
548 my $circ_control_branch =
549 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
550 my $branchitemrule =
551 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
553 my @libs;
554 if(defined $patron) {
555 return \@libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
556 return \@libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
559 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
560 @libs = $self->home_branch->get_hold_libraries;
561 push @libs, $self->home_branch unless scalar(@libs) > 0;
562 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
563 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
564 @libs = $plib->get_hold_libraries;
565 push @libs, $self->home_branch unless scalar(@libs) > 0;
566 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
567 push @libs, $self->home_branch;
568 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
569 push @libs, $self->holding_branch;
570 } else {
571 @libs = Koha::Libraries->search({
572 pickup_location => 1
573 }, {
574 order_by => ['branchname']
575 })->as_list;
578 my @pickup_locations;
579 foreach my $library (@libs) {
580 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
581 push @pickup_locations, $library;
585 return \@pickup_locations;
588 =head3 article_request_type
590 my $type = $item->article_request_type( $borrower )
592 returns 'yes', 'no', 'bib_only', or 'item_only'
594 $borrower must be a Koha::Patron object
596 =cut
598 sub article_request_type {
599 my ( $self, $borrower ) = @_;
601 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
602 my $branchcode =
603 $branch_control eq 'homebranch' ? $self->homebranch
604 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
605 : undef;
606 my $borrowertype = $borrower->categorycode;
607 my $itemtype = $self->effective_itemtype();
608 my $rule = Koha::CirculationRules->get_effective_rule(
610 rule_name => 'article_requests',
611 categorycode => $borrowertype,
612 itemtype => $itemtype,
613 branchcode => $branchcode
617 return q{} unless $rule;
618 return $rule->rule_value || q{}
621 =head3 current_holds
623 =cut
625 sub current_holds {
626 my ( $self ) = @_;
627 my $attributes = { order_by => 'priority' };
628 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
629 my $params = {
630 itemnumber => $self->itemnumber,
631 suspend => 0,
632 -or => [
633 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
634 waitingdate => { '!=' => undef },
637 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
638 return Koha::Holds->_new_from_dbic($hold_rs);
641 =head3 stockrotationitem
643 my $sritem = Koha::Item->stockrotationitem;
645 Returns the stock rotation item associated with the current item.
647 =cut
649 sub stockrotationitem {
650 my ( $self ) = @_;
651 my $rs = $self->_result->stockrotationitem;
652 return 0 if !$rs;
653 return Koha::StockRotationItem->_new_from_dbic( $rs );
656 =head3 add_to_rota
658 my $item = $item->add_to_rota($rota_id);
660 Add this item to the rota identified by $ROTA_ID, which means associating it
661 with the first stage of that rota. Should this item already be associated
662 with a rota, then we will move it to the new rota.
664 =cut
666 sub add_to_rota {
667 my ( $self, $rota_id ) = @_;
668 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
669 return $self;
672 =head3 has_pending_hold
674 my $is_pending_hold = $item->has_pending_hold();
676 This method checks the tmp_holdsqueue to see if this item has been selected for a hold, but not filled yet and returns true or false
678 =cut
680 sub has_pending_hold {
681 my ( $self ) = @_;
682 my $pending_hold = $self->_result->tmp_holdsqueues;
683 return $pending_hold->count ? 1: 0;
686 =head3 as_marc_field
688 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
689 my $field = $item->as_marc_field({ [ mss => $mss ] });
691 This method returns a MARC::Field object representing the Koha::Item object
692 with the current mappings configuration.
694 =cut
696 sub as_marc_field {
697 my ( $self, $params ) = @_;
699 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
700 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
702 my @subfields;
704 my @columns = $self->_result->result_source->columns;
706 foreach my $item_field ( @columns ) {
707 my $mapping = $mss->{ "items.$item_field"}[0];
708 my $tagfield = $mapping->{tagfield};
709 my $tagsubfield = $mapping->{tagsubfield};
710 next if !$tagfield; # TODO: Should we raise an exception instead?
711 # Feels like safe fallback is better
713 push @subfields, $tagsubfield => $self->$item_field
714 if defined $self->$item_field and $item_field ne '';
717 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
718 push( @subfields, @{$unlinked_item_subfields} )
719 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
721 my $field;
723 $field = MARC::Field->new(
724 "$item_tag", ' ', ' ', @subfields
725 ) if @subfields;
727 return $field;
730 =head3 renewal_branchcode
732 Returns the branchcode to be recorded in statistics renewal of the item
734 =cut
736 sub renewal_branchcode {
738 my ($self, $params ) = @_;
740 my $interface = C4::Context->interface;
741 my $branchcode;
742 if ( $interface eq 'opac' ){
743 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
744 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
745 $branchcode = 'OPACRenew';
747 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
748 $branchcode = $self->homebranch;
750 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
751 $branchcode = $self->checkout->patron->branchcode;
753 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
754 $branchcode = $self->checkout->branchcode;
756 else {
757 $branchcode = "";
759 } else {
760 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
761 ? C4::Context->userenv->{branch} : $params->{branch};
763 return $branchcode;
766 =head3 to_api_mapping
768 This method returns the mapping for representing a Koha::Item object
769 on the API.
771 =cut
773 sub to_api_mapping {
774 return {
775 itemnumber => 'item_id',
776 biblionumber => 'biblio_id',
777 biblioitemnumber => undef,
778 barcode => 'external_id',
779 dateaccessioned => 'acquisition_date',
780 booksellerid => 'acquisition_source',
781 homebranch => 'home_library_id',
782 price => 'purchase_price',
783 replacementprice => 'replacement_price',
784 replacementpricedate => 'replacement_price_date',
785 datelastborrowed => 'last_checkout_date',
786 datelastseen => 'last_seen_date',
787 stack => undef,
788 notforloan => 'not_for_loan_status',
789 damaged => 'damaged_status',
790 damaged_on => 'damaged_date',
791 itemlost => 'lost_status',
792 itemlost_on => 'lost_date',
793 withdrawn => 'withdrawn',
794 withdrawn_on => 'withdrawn_date',
795 itemcallnumber => 'callnumber',
796 coded_location_qualifier => 'coded_location_qualifier',
797 issues => 'checkouts_count',
798 renewals => 'renewals_count',
799 reserves => 'holds_count',
800 restricted => 'restricted_status',
801 itemnotes => 'public_notes',
802 itemnotes_nonpublic => 'internal_notes',
803 holdingbranch => 'holding_library_id',
804 paidfor => undef,
805 timestamp => 'timestamp',
806 location => 'location',
807 permanent_location => 'permanent_location',
808 onloan => 'checked_out_date',
809 cn_source => 'call_number_source',
810 cn_sort => 'call_number_sort',
811 ccode => 'collection_code',
812 materials => 'materials_notes',
813 uri => 'uri',
814 itype => 'item_type',
815 more_subfields_xml => 'extended_subfields',
816 enumchron => 'serial_issue_number',
817 copynumber => 'copy_number',
818 stocknumber => 'inventory_number',
819 new_status => 'new_status'
823 =head3 itemtype
825 my $itemtype = $item->itemtype;
827 Returns Koha object for effective itemtype
829 =cut
831 sub itemtype {
832 my ( $self ) = @_;
833 return Koha::ItemTypes->find( $self->effective_itemtype );
836 =head2 Internal methods
838 =head3 _after_item_action_hooks
840 Helper method that takes care of calling all plugin hooks
842 =cut
844 sub _after_item_action_hooks {
845 my ( $self, $params ) = @_;
847 my $action = $params->{action};
849 Koha::Plugins->call(
850 'after_item_action',
852 action => $action,
853 item => $self,
854 item_id => $self->itemnumber,
859 =head3 _type
861 =cut
863 sub _type {
864 return 'Item';
867 =head1 AUTHOR
869 Kyle M Hall <kyle@bywatersolutions.com>
871 =cut