fix big in RADIUS session viewing when using an ignored-accounting export
[freeside.git] / FS / FS / part_svc.pm
1 package FS::part_svc;
2
3 use strict;
4 use vars qw( @ISA );
5 use FS::Record qw( qsearch qsearchs fields dbh );
6 use FS::part_svc_column;
7 use FS::part_export;
8 use FS::export_svc;
9 use FS::cust_svc;
10
11 @ISA = qw(FS::Record);
12
13 =head1 NAME
14
15 FS::part_svc - Object methods for part_svc objects
16
17 =head1 SYNOPSIS
18
19   use FS::part_svc;
20
21   $record = new FS::part_svc \%hash
22   $record = new FS::part_svc { 'column' => 'value' };
23
24   $error = $record->insert;
25   $error = $record->insert( [ 'pseudofield' ] );
26   $error = $record->insert( [ 'pseudofield' ], \%exportnums );
27
28   $error = $new_record->replace($old_record);
29   $error = $new_record->replace($old_record, '1.3-COMPAT', [ 'pseudofield' ] );
30   $error = $new_record->replace($old_record, '1.3-COMPAT', [ 'pseudofield' ], \%exportnums );
31
32   $error = $record->delete;
33
34   $error = $record->check;
35
36 =head1 DESCRIPTION
37
38 An FS::part_svc represents a service definition.  FS::part_svc inherits from
39 FS::Record.  The following fields are currently supported:
40
41 =over 4
42
43 =item svcpart - primary key (assigned automatically for new service definitions)
44
45 =item svc - text name of this service definition
46
47 =item svcdb - table used for this service.  See L<FS::svc_acct>,
48 L<FS::svc_domain>, and L<FS::svc_forward>, among others.
49
50 =item disabled - Disabled flag, empty or `Y'
51
52 =back
53
54 =head1 METHODS
55
56 =over 4
57
58 =item new HASHREF
59
60 Creates a new service definition.  To add the service definition to the
61 database, see L<"insert">.
62
63 =cut
64
65 sub table { 'part_svc'; }
66
67 =item insert [ EXTRA_FIELDS_ARRAYREF [ , EXPORTNUMS_HASHREF ] ] 
68
69 Adds this service definition to the database.  If there is an error, returns
70 the error, otherwise returns false.
71
72 The following pseudo-fields may be defined, and will be maintained in
73 the part_svc_column table appropriately (see L<FS::part_svc_column>).
74
75 =over 4
76
77 =item I<svcdb>__I<field> - Default or fixed value for I<field> in I<svcdb>.
78
79 =item I<svcdb>__I<field>_flag - defines I<svcdb>__I<field> action: null, `D' for default, or `F' for fixed.  For virtual fields, can also be 'X' for excluded.
80
81 =back
82
83 If you want to add part_svc_column records for fields that do not exist as
84 (real or virtual) fields in the I<svcdb> table, make sure to list then in 
85 EXTRA_FIELDS_ARRAYREF also.
86
87 If EXPORTNUMS_HASHREF is specified (keys are exportnums and values are
88 boolean), the appopriate export_svc records will be inserted.
89
90 =cut
91
92 sub insert {
93   my $self = shift;
94   my @fields = ();
95   my @exportnums = ();
96   @fields = @{shift(@_)} if @_;
97   if ( @_ ) {
98     my $exportnums = shift;
99     @exportnums = grep $exportnums->{$_}, keys %$exportnums;
100   }
101
102   local $SIG{HUP} = 'IGNORE';
103   local $SIG{INT} = 'IGNORE';
104   local $SIG{QUIT} = 'IGNORE';
105   local $SIG{TERM} = 'IGNORE';
106   local $SIG{TSTP} = 'IGNORE';
107   local $SIG{PIPE} = 'IGNORE';
108
109   my $oldAutoCommit = $FS::UID::AutoCommit;
110   local $FS::UID::AutoCommit = 0;
111   my $dbh = dbh;
112
113   my $error = $self->SUPER::insert;
114   if ( $error ) {
115     $dbh->rollback if $oldAutoCommit;
116     return $error;
117   }
118
119   # add part_svc_column records
120
121   my $svcdb = $self->svcdb;
122 #  my @rows = map { /^${svcdb}__(.*)$/; $1 }
123 #    grep ! /_flag$/,
124 #      grep /^${svcdb}__/,
125 #        fields('part_svc');
126   foreach my $field (
127     grep { $_ ne 'svcnum'
128            && defined( $self->getfield($svcdb.'__'.$_.'_flag') )
129          } (fields($svcdb), @fields)
130   ) {
131     my $part_svc_column = $self->part_svc_column($field);
132     my $previous = qsearchs('part_svc_column', {
133       'svcpart'    => $self->svcpart,
134       'columnname' => $field,
135     } );
136
137     my $flag = $self->getfield($svcdb.'__'.$field.'_flag');
138     if ( uc($flag) =~ /^([DFX])$/ ) {
139       $part_svc_column->setfield('columnflag', $1);
140       $part_svc_column->setfield('columnvalue',
141         $self->getfield($svcdb.'__'.$field)
142       );
143       if ( $previous ) {
144         $error = $part_svc_column->replace($previous);
145       } else {
146         $error = $part_svc_column->insert;
147       }
148     } else {
149       $error = $previous ? $previous->delete : '';
150     }
151     if ( $error ) {
152       $dbh->rollback if $oldAutoCommit;
153       return $error;
154     }
155
156   }
157
158   # add export_svc records
159
160   foreach my $exportnum ( @exportnums ) {
161     my $export_svc = new FS::export_svc ( {
162       'exportnum' => $exportnum,
163       'svcpart'   => $self->svcpart,
164     } );
165     $error = $export_svc->insert;
166     if ( $error ) {
167       $dbh->rollback if $oldAutoCommit;
168       return $error;
169     }
170   }
171
172   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
173
174   '';
175 }
176
177 =item delete
178
179 Currently unimplemented.  Set the "disabled" field instead.
180
181 =cut
182
183 sub delete {
184   return "Can't (yet?) delete service definitions.";
185 # check & make sure the svcpart isn't in cust_svc or pkg_svc (in any packages)?
186 }
187
188 =item replace OLD_RECORD [ '1.3-COMPAT' [ , EXTRA_FIELDS_ARRAYREF [ , EXPORTNUMS_HASHREF ] ] ]
189
190 Replaces OLD_RECORD with this one in the database.  If there is an error,
191 returns the error, otherwise returns false.
192
193 TODOC: 1.3-COMPAT
194
195 TODOC: EXTRA_FIELDS_ARRAYREF (same as insert method)
196
197 =cut
198
199 sub replace {
200   my ( $new, $old ) = ( shift, shift );
201
202   return "Can't change svcdb for an existing service definition!"
203     unless $old->svcdb eq $new->svcdb;
204
205   local $SIG{HUP} = 'IGNORE';
206   local $SIG{INT} = 'IGNORE';
207   local $SIG{QUIT} = 'IGNORE';
208   local $SIG{TERM} = 'IGNORE';
209   local $SIG{TSTP} = 'IGNORE';
210   local $SIG{PIPE} = 'IGNORE';
211
212   my $oldAutoCommit = $FS::UID::AutoCommit;
213   local $FS::UID::AutoCommit = 0;
214   my $dbh = dbh;
215
216   my $error = $new->SUPER::replace( $old );
217   if ( $error ) {
218     $dbh->rollback if $oldAutoCommit;
219     return $error;
220   }
221
222   if ( @_ && $_[0] eq '1.3-COMPAT' ) {
223     shift;
224     my @fields = ();
225     @fields = @{shift(@_)} if @_;
226     my $exportnums = @_ ? shift : '';
227
228    # maintain part_svc_column records
229
230     my $svcdb = $new->svcdb;
231     foreach my $field (
232       grep { $_ ne 'svcnum'
233              && defined( $new->getfield($svcdb.'__'.$_.'_flag') )
234            } (fields($svcdb),@fields)
235     ) {
236       my $part_svc_column = $new->part_svc_column($field);
237       my $previous = qsearchs('part_svc_column', {
238         'svcpart'    => $new->svcpart,
239         'columnname' => $field,
240       } );
241
242       my $flag = $new->getfield($svcdb.'__'.$field.'_flag');
243       if ( uc($flag) =~ /^([DFX])$/ ) {
244         $part_svc_column->setfield('columnflag', $1);
245         $part_svc_column->setfield('columnvalue',
246           $new->getfield($svcdb.'__'.$field)
247         );
248         if ( $previous ) {
249           $error = $part_svc_column->replace($previous);
250         } else {
251           $error = $part_svc_column->insert;
252         }
253       } else {
254         $error = $previous ? $previous->delete : '';
255       }
256       if ( $error ) {
257         $dbh->rollback if $oldAutoCommit;
258         return $error;
259       }
260     }
261
262     # maintain export_svc records
263
264     if ( $exportnums ) {
265
266       #false laziness w/ edit/process/agent_type.cgi
267       foreach my $part_export ( qsearch('part_export', {}) ) {
268         my $exportnum = $part_export->exportnum;
269         my $hashref = {
270           'exportnum' => $exportnum,
271           'svcpart'   => $new->svcpart,
272         };
273         my $export_svc = qsearchs('export_svc', $hashref);
274
275         if ( $export_svc && ! $exportnums->{$exportnum} ) {
276           $error = $export_svc->delete;
277           if ( $error ) {
278             $dbh->rollback if $oldAutoCommit;
279             return $error;
280           }
281         } elsif ( ! $export_svc && $exportnums->{$exportnum} ) {
282           $export_svc = new FS::export_svc ( $hashref );
283           $error = $export_svc->insert;
284           if ( $error ) {
285             $dbh->rollback if $oldAutoCommit;
286             return $error;
287           }
288         }
289         
290       }
291
292     }
293
294   } else {
295     $dbh->rollback if $oldAutoCommit;
296     return 'non-1.3-COMPAT interface not yet written';
297     #not yet implemented
298   }
299
300   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
301
302   '';
303 }
304
305 =item check
306
307 Checks all fields to make sure this is a valid service definition.  If there is
308 an error, returns the error, otherwise returns false.  Called by the insert
309 and replace methods.
310
311 =cut
312
313 sub check {
314   my $self = shift;
315   my $recref = $self->hashref;
316
317   my $error;
318   $error=
319     $self->ut_numbern('svcpart')
320     || $self->ut_text('svc')
321     || $self->ut_alpha('svcdb')
322     || $self->ut_enum('disabled', [ '', 'Y' ] )
323   ;
324   return $error if $error;
325
326   my @fields = eval { fields( $recref->{svcdb} ) }; #might die
327   return "Unknown svcdb!" unless @fields;
328
329   $self->SUPER::check;
330 }
331
332 =item part_svc_column COLUMNNAME
333
334 Returns the part_svc_column object (see L<FS::part_svc_column>) for the given
335 COLUMNNAME, or a new part_svc_column object if none exists.
336
337 =cut
338
339 sub part_svc_column {
340   my( $self, $columnname) = @_;
341   $self->svcpart &&
342     qsearchs('part_svc_column',  {
343                                    'svcpart'    => $self->svcpart,
344                                    'columnname' => $columnname,
345                                  }
346   ) or new FS::part_svc_column {
347                                  'svcpart'    => $self->svcpart,
348                                  'columnname' => $columnname,
349                                };
350 }
351
352 =item all_part_svc_column
353
354 =cut
355
356 sub all_part_svc_column {
357   my $self = shift;
358   qsearch('part_svc_column', { 'svcpart' => $self->svcpart } );
359 }
360
361 =item part_export [ EXPORTTYPE ]
362
363 Returns all exports (see L<FS::part_export>) for this service, or, if an
364 export type is specified, only returns exports of the given type.
365
366 =cut
367
368 sub part_export {
369   my $self = shift;
370   my %search;
371   $search{'exporttype'} = shift if @_;
372   map { qsearchs('part_export', { 'exportnum' => $_->exportnum, %search } ) }
373     qsearch('export_svc', { 'svcpart' => $self->svcpart } );
374 }
375
376 =item cust_svc
377
378 Returns a list of associated FS::cust_svc records.
379
380 =cut
381
382 sub cust_svc {
383   my $self = shift;
384   qsearch('cust_svc', { 'svcpart' => $self->svcpart } );
385 }
386
387 =item svc_x
388
389 Returns a list of associated FS::svc_* records.
390
391 =cut
392
393 sub svc_x {
394   my $self = shift;
395   map { $_->svc_x } $self->cust_svc;
396 }
397
398 =back
399
400 =head1 BUGS
401
402 Delete is unimplemented.
403
404 The list of svc_* tables is hardcoded.  When svc_acct_pop is renamed, this
405 should be fixed.
406
407 all_part_svc_column method should be documented
408
409 =head1 SEE ALSO
410
411 L<FS::Record>, L<FS::part_svc_column>, L<FS::part_pkg>, L<FS::pkg_svc>,
412 L<FS::cust_svc>, L<FS::svc_acct>, L<FS::svc_forward>, L<FS::svc_domain>,
413 schema.html from the base documentation.
414
415 =cut
416
417 1;
418