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