yow. fix up group handling
[freeside.git] / FS / FS / svc_Common.pm
1 package FS::svc_Common;
2
3 use strict;
4 use vars qw( @ISA $noexport_hack $DEBUG );
5 use Carp;
6 use FS::Record qw( qsearch qsearchs fields dbh );
7 use FS::cust_main_Mixin;
8 use FS::cust_svc;
9 use FS::part_svc;
10 use FS::queue;
11 use FS::cust_main;
12 use FS::inventory_item;
13 use FS::inventory_class;
14
15 @ISA = qw( FS::cust_main_Mixin FS::Record );
16
17 $DEBUG = 1;
18
19 =head1 NAME
20
21 FS::svc_Common - Object method for all svc_ records
22
23 =head1 SYNOPSIS
24
25 use FS::svc_Common;
26
27 @ISA = qw( FS::svc_Common );
28
29 =head1 DESCRIPTION
30
31 FS::svc_Common is intended as a base class for table-specific classes to
32 inherit from, i.e. FS::svc_acct.  FS::svc_Common inherits from FS::Record.
33
34 =head1 METHODS
35
36 =over 4
37
38 =cut
39
40 sub new {
41   my $proto = shift;
42   my $class = ref($proto) || $proto;
43   my $self = {};
44   bless ($self, $class);
45
46   unless ( defined ( $self->table ) ) {
47     $self->{'Table'} = shift;
48     carp "warning: FS::Record::new called with table name ". $self->{'Table'};
49   }
50   
51   #$self->{'Hash'} = shift;
52   my $newhash = shift;
53   $self->{'Hash'} = { map { $_ => $newhash->{$_} } qw(svcnum svcpart) };
54
55   $self->setdefault( $self->_fieldhandlers );
56
57   $self->{'Hash'}{$_} = $newhash->{$_}
58     foreach grep { defined($newhash->{$_}) && length($newhash->{$_}) }
59                  keys %$newhash;
60
61   foreach my $field ( grep !defined($self->{'Hash'}{$_}), $self->fields ) { 
62     $self->{'Hash'}{$field}='';
63   }
64
65   $self->_rebless if $self->can('_rebless');
66
67   $self->{'modified'} = 0;
68
69   $self->_cache($self->{'Hash'}, shift) if $self->can('_cache') && @_;
70
71   $self;
72 }
73
74 #empty default
75 sub _fieldhandlers { {}; }
76
77 sub virtual_fields {
78
79   # This restricts the fields based on part_svc_column and the svcpart of 
80   # the service.  There are four possible cases:
81   # 1.  svcpart passed as part of the svc_x hash.
82   # 2.  svcpart fetched via cust_svc based on svcnum.
83   # 3.  No svcnum or svcpart.  In this case, return ALL the fields with 
84   #     dbtable eq $self->table.
85   # 4.  Called via "fields('svc_acct')" or something similar.  In this case
86   #     there is no $self object.
87
88   my $self = shift;
89   my $svcpart;
90   my @vfields = $self->SUPER::virtual_fields;
91
92   return @vfields unless (ref $self); # Case 4
93
94   if ($self->svcpart) { # Case 1
95     $svcpart = $self->svcpart;
96   } elsif ( $self->svcnum
97             && qsearchs('cust_svc',{'svcnum'=>$self->svcnum} )
98           ) { #Case 2
99     $svcpart = $self->cust_svc->svcpart;
100   } else { # Case 3
101     $svcpart = '';
102   }
103
104   if ($svcpart) { #Cases 1 and 2
105     my %flags = map { $_->columnname, $_->columnflag } (
106         qsearch ('part_svc_column', { svcpart => $svcpart } )
107       );
108     return grep { not ($flags{$_} eq 'X') } @vfields;
109   } else { # Case 3
110     return @vfields;
111   } 
112   return ();
113 }
114
115 =item check
116
117 Checks the validity of fields in this record.
118
119 At present, this does nothing but call FS::Record::check (which, in turn, 
120 does nothing but run virtual field checks).
121
122 =cut
123
124 sub check {
125   my $self = shift;
126   $self->SUPER::check;
127 }
128
129 =item insert [ , OPTION => VALUE ... ]
130
131 Adds this record to the database.  If there is an error, returns the error,
132 otherwise returns false.
133
134 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
135 defined.  An FS::cust_svc record will be created and inserted.
136
137 Currently available options are: I<jobnums>, I<child_objects> and
138 I<depend_jobnum>.
139
140 If I<jobnum> is set to an array reference, the jobnums of any export jobs will
141 be added to the referenced array.
142
143 If I<child_objects> is set to an array reference of FS::tablename objects (for
144 example, FS::acct_snarf objects), they will have their svcnum field set and
145 will be inserted after this record, but before any exports are run.  Each
146 element of the array can also optionally be a two-element array reference
147 containing the child object and the name of an alternate field to be filled in
148 with the newly-inserted svcnum, for example C<[ $svc_forward, 'srcsvc' ]>
149
150 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
151 jobnums), all provisioning jobs will have a dependancy on the supplied
152 jobnum(s) (they will not run until the specific job(s) complete(s)).
153
154 =cut
155
156 sub insert {
157   my $self = shift;
158   my %options = @_;
159   warn "FS::svc_Common::insert called with options ".
160      join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
161   if $DEBUG;
162
163   my @jobnums = ();
164   local $FS::queue::jobnums = \@jobnums;
165   warn "FS::svc_Common::insert: set \$FS::queue::jobnums to $FS::queue::jobnums"
166     if $DEBUG;
167   my $objects = $options{'child_objects'} || [];
168   my $depend_jobnums = $options{'depend_jobnum'} || [];
169   $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
170   my $error;
171
172   local $SIG{HUP} = 'IGNORE';
173   local $SIG{INT} = 'IGNORE';
174   local $SIG{QUIT} = 'IGNORE';
175   local $SIG{TERM} = 'IGNORE';
176   local $SIG{TSTP} = 'IGNORE';
177   local $SIG{PIPE} = 'IGNORE';
178
179   my $oldAutoCommit = $FS::UID::AutoCommit;
180   local $FS::UID::AutoCommit = 0;
181   my $dbh = dbh;
182
183   $error = $self->check;
184   return $error if $error;
185
186   my $svcnum = $self->svcnum;
187   my $cust_svc = $svcnum ? qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) : '';
188   #unless ( $svcnum ) {
189   if ( !$svcnum or !$cust_svc ) {
190     $cust_svc = new FS::cust_svc ( {
191       #hua?# 'svcnum'  => $svcnum,
192       'svcnum'  => $self->svcnum,
193       'pkgnum'  => $self->pkgnum,
194       'svcpart' => $self->svcpart,
195     } );
196     $error = $cust_svc->insert;
197     if ( $error ) {
198       $dbh->rollback if $oldAutoCommit;
199       return $error;
200     }
201     $svcnum = $self->svcnum($cust_svc->svcnum);
202   } else {
203     #$cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
204     unless ( $cust_svc ) {
205       $dbh->rollback if $oldAutoCommit;
206       return "no cust_svc record found for svcnum ". $self->svcnum;
207     }
208     $self->pkgnum($cust_svc->pkgnum);
209     $self->svcpart($cust_svc->svcpart);
210   }
211
212   $error = $self->set_auto_inventory;
213   if ( $error ) {
214     $dbh->rollback if $oldAutoCommit;
215     return $error;
216   }
217
218   $error = $self->SUPER::insert;
219   if ( $error ) {
220     $dbh->rollback if $oldAutoCommit;
221     return $error;
222   }
223
224   foreach my $object ( @$objects ) {
225     my($field, $obj);
226     if ( ref($object) eq 'ARRAY' ) {
227       ($obj, $field) = @$object;
228     } else {
229       $obj = $object;
230       $field = 'svcnum';
231     }
232     $obj->$field($self->svcnum);
233     $error = $obj->insert;
234     if ( $error ) {
235       $dbh->rollback if $oldAutoCommit;
236       return $error;
237     }
238   }
239
240   #new-style exports!
241   unless ( $noexport_hack ) {
242
243     warn "FS::svc_Common::insert: \$FS::queue::jobnums is $FS::queue::jobnums"
244       if $DEBUG;
245
246     foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
247       my $error = $part_export->export_insert($self);
248       if ( $error ) {
249         $dbh->rollback if $oldAutoCommit;
250         return "exporting to ". $part_export->exporttype.
251                " (transaction rolled back): $error";
252       }
253     }
254
255     foreach my $depend_jobnum ( @$depend_jobnums ) {
256       warn "inserting dependancies on supplied job $depend_jobnum\n"
257         if $DEBUG;
258       foreach my $jobnum ( @jobnums ) {
259         my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
260         warn "inserting dependancy for job $jobnum on $depend_jobnum\n"
261           if $DEBUG;
262         my $error = $queue->depend_insert($depend_jobnum);
263         if ( $error ) {
264           $dbh->rollback if $oldAutoCommit;
265           return "error queuing job dependancy: $error";
266         }
267       }
268     }
269
270   }
271
272   if ( exists $options{'jobnums'} ) {
273     push @{ $options{'jobnums'} }, @jobnums;
274   }
275
276   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
277
278   '';
279 }
280
281 =item delete
282
283 Deletes this account from the database.  If there is an error, returns the
284 error, otherwise returns false.
285
286 The corresponding FS::cust_svc record will be deleted as well.
287
288 =cut
289
290 sub delete {
291   my $self = shift;
292   my $error;
293
294   local $SIG{HUP} = 'IGNORE';
295   local $SIG{INT} = 'IGNORE';
296   local $SIG{QUIT} = 'IGNORE';
297   local $SIG{TERM} = 'IGNORE';
298   local $SIG{TSTP} = 'IGNORE';
299   local $SIG{PIPE} = 'IGNORE';
300
301   my $svcnum = $self->svcnum;
302
303   my $oldAutoCommit = $FS::UID::AutoCommit;
304   local $FS::UID::AutoCommit = 0;
305   my $dbh = dbh;
306
307   $error = $self->SUPER::delete;
308   return $error if $error;
309
310   #new-style exports!
311   unless ( $noexport_hack ) {
312     foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
313       $error = $part_export->export_delete($self);
314       if ( $error ) {
315         $dbh->rollback if $oldAutoCommit;
316         return "exporting to ". $part_export->exporttype.
317                " (transaction rolled back): $error";
318       }
319     }
320   }
321
322   $error = $self->return_inventory;
323   if ( $error ) {
324     $dbh->rollback if $oldAutoCommit;
325     return "error returning inventory: $error";
326   }
327
328   my $cust_svc = $self->cust_svc;
329   $error = $cust_svc->delete;
330   if ( $error ) {
331     $dbh->rollback if $oldAutoCommit;
332     return $error;
333   }
334
335   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
336
337   '';
338 }
339
340 =item replace OLD_RECORD
341
342 Replaces OLD_RECORD with this one.  If there is an error, returns the error,
343 otherwise returns false.
344
345 =cut
346
347 sub replace {
348   my ($new, $old) = (shift, shift);
349
350   local $SIG{HUP} = 'IGNORE';
351   local $SIG{INT} = 'IGNORE';
352   local $SIG{QUIT} = 'IGNORE';
353   local $SIG{TERM} = 'IGNORE';
354   local $SIG{TSTP} = 'IGNORE';
355   local $SIG{PIPE} = 'IGNORE';
356
357   my $oldAutoCommit = $FS::UID::AutoCommit;
358   local $FS::UID::AutoCommit = 0;
359   my $dbh = dbh;
360
361   my $error = $new->set_auto_inventory;
362   if ( $error ) {
363     $dbh->rollback if $oldAutoCommit;
364     return $error;
365   }
366
367   $error = $new->SUPER::replace($old);
368   if ($error) {
369     $dbh->rollback if $oldAutoCommit;
370     return $error;
371   }
372
373   #new-style exports!
374   unless ( $noexport_hack ) {
375
376     #not quite false laziness, but same pattern as FS::svc_acct::replace and
377     #FS::part_export::sqlradius::_export_replace.  List::Compare or something
378     #would be useful but too much of a pain in the ass to deploy
379
380     my @old_part_export = $old->cust_svc->part_svc->part_export;
381     my %old_exportnum = map { $_->exportnum => 1 } @old_part_export;
382     my @new_part_export = 
383       $new->svcpart
384         ? qsearchs('part_svc', { svcpart=>$new->svcpart } )->part_export
385         : $new->cust_svc->part_svc->part_export;
386     my %new_exportnum = map { $_->exportnum => 1 } @new_part_export;
387
388     foreach my $delete_part_export (
389       grep { ! $new_exportnum{$_->exportnum} } @old_part_export
390     ) {
391       my $error = $delete_part_export->export_delete($old);
392       if ( $error ) {
393         $dbh->rollback if $oldAutoCommit;
394         return "error deleting, export to ". $delete_part_export->exporttype.
395                " (transaction rolled back): $error";
396       }
397     }
398
399     foreach my $replace_part_export (
400       grep { $old_exportnum{$_->exportnum} } @new_part_export
401     ) {
402       my $error = $replace_part_export->export_replace($new,$old);
403       if ( $error ) {
404         $dbh->rollback if $oldAutoCommit;
405         return "error exporting to ". $replace_part_export->exporttype.
406                " (transaction rolled back): $error";
407       }
408     }
409
410     foreach my $insert_part_export (
411       grep { ! $old_exportnum{$_->exportnum} } @new_part_export
412     ) {
413       my $error = $insert_part_export->export_insert($new);
414       if ( $error ) {
415         $dbh->rollback if $oldAutoCommit;
416         return "error inserting export to ". $insert_part_export->exporttype.
417                " (transaction rolled back): $error";
418       }
419     }
420
421   }
422
423   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
424   '';
425 }
426
427
428 =item setfixed
429
430 Sets any fixed fields for this service (see L<FS::part_svc>).  If there is an
431 error, returns the error, otherwise returns the FS::part_svc object (use ref()
432 to test the return).  Usually called by the check method.
433
434 =cut
435
436 sub setfixed {
437   my $self = shift;
438   $self->setx('F', @_);
439 }
440
441 =item setdefault
442
443 Sets all fields to their defaults (see L<FS::part_svc>), overriding their
444 current values.  If there is an error, returns the error, otherwise returns
445 the FS::part_svc object (use ref() to test the return).
446
447 =cut
448
449 sub setdefault {
450   my $self = shift;
451   $self->setx('D', @_ );
452 }
453
454 =item set_default_and_fixed
455
456 =cut
457
458 sub set_default_and_fixed {
459   my $self = shift;
460   $self->setx( [ 'D', 'F' ], @_ );
461 }
462
463 =item setx FLAG | FLAG_ARRAYREF , [ CALLBACK_HASHREF ]
464
465 Sets fields according to the passed in flag or arrayref of flags.
466
467 Optionally, a hashref of field names and callback coderefs can be passed.
468 If a coderef exists for a given field name, instead of setting the field,
469 the coderef is called with the column value (part_svc_column.columnvalue)
470 as the single parameter.
471
472 =cut
473
474 sub setx {
475   my $self = shift;
476   my $x = shift;
477   my @x = ref($x) ? @$x : ($x);
478   my $coderef = scalar(@_) ? shift : $self->_fieldhandlers;
479
480   my $error =
481     $self->ut_numbern('svcnum')
482   ;
483   return $error if $error;
484
485   my $part_svc = $self->part_svc;
486   return "Unkonwn svcpart" unless $part_svc;
487
488   #set default/fixed/whatever fields from part_svc
489
490   foreach my $part_svc_column (
491     grep { my $f = $_->columnflag; grep { $f eq $_ } @x } #columnflag in @x
492     $part_svc->all_part_svc_column
493   ) {
494
495     my $columnname  = $part_svc_column->columnname;
496     my $columnvalue = $part_svc_column->columnvalue;
497
498     $columnvalue = &{ $coderef->{$columnname} }( $self, $columnvalue )
499       if exists( $coderef->{$columnname} );
500     $self->setfield( $columnname, $columnvalue );
501
502   }
503
504  $part_svc;
505
506 }
507
508 sub part_svc {
509   my $self = shift;
510
511   #get part_svc
512   my $svcpart;
513   if ( $self->get('svcpart') ) {
514     $svcpart = $self->get('svcpart');
515   } elsif ( $self->svcnum && qsearchs('cust_svc', {'svcnum'=>$self->svcnum}) ) {
516     my $cust_svc = $self->cust_svc;
517     return "Unknown svcnum" unless $cust_svc; 
518     $svcpart = $cust_svc->svcpart;
519   }
520
521   qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
522
523 }
524
525 =item set_auto_inventory
526
527 Sets any fields which auto-populate from inventory (see L<FS::part_svc>).
528 If there is an error, returns the error, otherwise returns false.
529
530 =cut
531
532 sub set_auto_inventory {
533   my $self = shift;
534
535   my $error =
536     $self->ut_numbern('svcnum')
537   ;
538   return $error if $error;
539
540   my $part_svc = $self->part_svc;
541   return "Unkonwn svcpart" unless $part_svc;
542
543   local $SIG{HUP} = 'IGNORE';
544   local $SIG{INT} = 'IGNORE';
545   local $SIG{QUIT} = 'IGNORE';
546   local $SIG{TERM} = 'IGNORE';
547   local $SIG{TSTP} = 'IGNORE';
548   local $SIG{PIPE} = 'IGNORE';
549
550   my $oldAutoCommit = $FS::UID::AutoCommit;
551   local $FS::UID::AutoCommit = 0;
552   my $dbh = dbh;
553
554   #set default/fixed/whatever fields from part_svc
555   my $table = $self->table;
556   foreach my $field ( grep { $_ ne 'svcnum' } $self->fields ) {
557     my $part_svc_column = $part_svc->part_svc_column($field);
558     if ( $part_svc_column->columnflag eq 'A' && $self->$field() eq '' ) {
559
560       my $classnum = $part_svc_column->columnvalue;
561       my $inventory_item = qsearchs({
562         'table'     => 'inventory_item',
563         'hashref'   => { 'classnum' => $classnum, 
564                          'svcnum'   => '',
565                        },
566         'extra_sql' => 'LIMIT 1 FOR UPDATE',
567       });
568
569       unless ( $inventory_item ) {
570         $dbh->rollback if $oldAutoCommit;
571         my $inventory_class =
572           qsearchs('inventory_class', { 'classnum' => $classnum } );
573         return "Can't find inventory_class.classnum $classnum"
574           unless $inventory_class;
575         return "Out of ". $inventory_class->classname. "s\n"; #Lingua:: BS
576                                                               #for pluralizing
577       }
578
579       $inventory_item->svcnum( $self->svcnum );
580       my $ierror = $inventory_item->replace();
581       if ( $ierror ) {
582         $dbh->rollback if $oldAutoCommit;
583         return "Error provisioning inventory: $ierror";
584         
585       }
586
587       $self->setfield( $field, $inventory_item->item );
588
589     }
590   }
591
592  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
593
594  '';
595
596 }
597
598 =item return_inventory
599
600 =cut
601
602 sub return_inventory {
603   my $self = shift;
604
605   local $SIG{HUP} = 'IGNORE';
606   local $SIG{INT} = 'IGNORE';
607   local $SIG{QUIT} = 'IGNORE';
608   local $SIG{TERM} = 'IGNORE';
609   local $SIG{TSTP} = 'IGNORE';
610   local $SIG{PIPE} = 'IGNORE';
611
612   my $oldAutoCommit = $FS::UID::AutoCommit;
613   local $FS::UID::AutoCommit = 0;
614   my $dbh = dbh;
615
616   foreach my $inventory_item ( $self->inventory_item ) {
617     $inventory_item->svcnum('');
618     my $error = $inventory_item->replace();
619     if ( $error ) {
620       $dbh->rollback if $oldAutoCommit;
621       return "Error returning inventory: $error";
622     }
623   }
624
625   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
626
627   '';
628 }
629
630 =item inventory_item
631
632 Returns the inventory items associated with this svc_ record, as
633 FS::inventory_item objects (see L<FS::inventory_item>.
634
635 =cut
636
637 sub inventory_item {
638   my $self = shift;
639   qsearch({
640     'table'     => 'inventory_item',
641     'hashref'   => { 'svcnum' => $self->svcnum, },
642   });
643 }
644
645 =item cust_svc
646
647 Returns the cust_svc record associated with this svc_ record, as a FS::cust_svc
648 object (see L<FS::cust_svc>).
649
650 =cut
651
652 sub cust_svc {
653   my $self = shift;
654   qsearchs('cust_svc', { 'svcnum' => $self->svcnum } );
655 }
656
657 =item suspend
658
659 Runs export_suspend callbacks.
660
661 =cut
662
663 sub suspend {
664   my $self = shift;
665
666   local $SIG{HUP} = 'IGNORE';
667   local $SIG{INT} = 'IGNORE';
668   local $SIG{QUIT} = 'IGNORE';
669   local $SIG{TERM} = 'IGNORE';
670   local $SIG{TSTP} = 'IGNORE';
671   local $SIG{PIPE} = 'IGNORE';
672
673   my $oldAutoCommit = $FS::UID::AutoCommit;
674   local $FS::UID::AutoCommit = 0;
675   my $dbh = dbh;
676
677   #new-style exports!
678   unless ( $noexport_hack ) {
679     foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
680       my $error = $part_export->export_suspend($self);
681       if ( $error ) {
682         $dbh->rollback if $oldAutoCommit;
683         return "error exporting to ". $part_export->exporttype.
684                " (transaction rolled back): $error";
685       }
686     }
687   }
688
689   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
690   '';
691
692 }
693
694 =item unsuspend
695
696 Runs export_unsuspend callbacks.
697
698 =cut
699
700 sub unsuspend {
701   my $self = shift;
702
703   local $SIG{HUP} = 'IGNORE';
704   local $SIG{INT} = 'IGNORE';
705   local $SIG{QUIT} = 'IGNORE';
706   local $SIG{TERM} = 'IGNORE';
707   local $SIG{TSTP} = 'IGNORE';
708   local $SIG{PIPE} = 'IGNORE';
709
710   my $oldAutoCommit = $FS::UID::AutoCommit;
711   local $FS::UID::AutoCommit = 0;
712   my $dbh = dbh;
713
714   #new-style exports!
715   unless ( $noexport_hack ) {
716     foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
717       my $error = $part_export->export_unsuspend($self);
718       if ( $error ) {
719         $dbh->rollback if $oldAutoCommit;
720         return "error exporting to ". $part_export->exporttype.
721                " (transaction rolled back): $error";
722       }
723     }
724   }
725
726   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
727   '';
728
729 }
730
731 =item cancel
732
733 Stub - returns false (no error) so derived classes don't need to define these
734 methods.  Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
735
736 =cut
737
738 sub cancel { ''; }
739
740 =item clone_suspended
741
742 Constructor used by FS::part_export::_export_suspend fallback.  Stub returning
743 same object for svc_ classes which don't implement a suspension fallback
744 (everything except svc_acct at the moment).  Document better.
745
746 =cut
747
748 sub clone_suspended {
749   shift;
750 }
751
752 =item clone_kludge_unsuspend 
753
754 Constructor used by FS::part_export::_export_unsuspend fallback.  Stub returning
755 same object for svc_ classes which don't implement a suspension fallback
756 (everything except svc_acct at the moment).  Document better.
757
758 =cut
759
760 sub clone_kludge_unsuspend {
761   shift;
762 }
763
764 =back
765
766 =head1 BUGS
767
768 The setfixed method return value.
769
770 =head1 SEE ALSO
771
772 L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, schema.html
773 from the base documentation.
774
775 =cut
776
777 1;
778