498daf6497e7227aa19b6351952bc6d3b06d3bfe
[freeside.git] / FS / FS / part_pkg.pm
1 package FS::part_pkg;
2 use base qw( FS::m2m_Common FS::o2m_Common FS::option_Common );
3
4 use strict;
5 use vars qw( %plans $DEBUG $setup_hack $skip_pkg_svc_hack
6              $cache_enabled %cache_link %cache_pkg_svc
7            );
8 use Carp qw(carp cluck confess);
9 use Scalar::Util qw( blessed );
10 use DateTime;
11 use Time::Local qw( timelocal timelocal_nocheck ); # eventually replace with DateTime
12 use Tie::IxHash;
13 use FS::Conf;
14 use FS::Record qw( qsearch qsearchs dbh dbdef );
15 use FS::Cursor; # for upgrade
16 use FS::pkg_svc;
17 use FS::part_svc;
18 use FS::cust_pkg;
19 use FS::agent_type;
20 use FS::type_pkgs;
21 use FS::part_pkg_option;
22 use FS::part_pkg_fcc_option;
23 use FS::pkg_class;
24 use FS::agent;
25 use FS::part_pkg_msgcat;
26 use FS::part_pkg_taxrate;
27 use FS::part_pkg_taxoverride;
28 use FS::part_pkg_taxproduct;
29 use FS::part_pkg_link;
30 use FS::part_pkg_discount;
31 use FS::part_pkg_usage;
32 use FS::part_pkg_vendor;
33
34 $DEBUG = 0;
35
36 $setup_hack = 0;
37 $skip_pkg_svc_hack = 0;
38
39 $cache_enabled = 0;
40 %cache_link = ();
41 %cache_pkg_svc = ();
42
43 =head1 NAME
44
45 FS::part_pkg - Object methods for part_pkg objects
46
47 =head1 SYNOPSIS
48
49   use FS::part_pkg;
50
51   $record = new FS::part_pkg \%hash
52   $record = new FS::part_pkg { 'column' => 'value' };
53
54   $custom_record = $template_record->clone;
55
56   $error = $record->insert;
57
58   $error = $new_record->replace($old_record);
59
60   $error = $record->delete;
61
62   $error = $record->check;
63
64   @pkg_svc = $record->pkg_svc;
65
66   $svcnum = $record->svcpart;
67   $svcnum = $record->svcpart( 'svc_acct' );
68
69 =head1 DESCRIPTION
70
71 An FS::part_pkg object represents a package definition.  FS::part_pkg
72 inherits from FS::Record.  The following fields are currently supported:
73
74 =over 4
75
76 =item pkgpart - primary key (assigned automatically for new package definitions)
77
78 =item pkg - Text name of this package definition (customer-viewable)
79
80 =item comment - Text name of this package definition (non-customer-viewable)
81
82 =item classnum - Optional package class (see L<FS::pkg_class>)
83
84 =item promo_code - Promotional code
85
86 =item setup - Setup fee expression (deprecated)
87
88 =item freq - Frequency of recurring fee
89
90 =item recur - Recurring fee expression (deprecated)
91
92 =item setuptax - Setup fee tax exempt flag, empty or `Y'
93
94 =item recurtax - Recurring fee tax exempt flag, empty or `Y'
95
96 =item taxclass - Tax class 
97
98 =item plan - Price plan
99
100 =item plandata - Price plan data (deprecated - see L<FS::part_pkg_option> instead)
101
102 =item disabled - Disabled flag, empty or `Y'
103
104 =item custom - Custom flag, empty or `Y'
105
106 =item setup_cost - for cost tracking
107
108 =item recur_cost - for cost tracking
109
110 =item pay_weight - Weight (relative to credit_weight and other package definitions) that controls payment application to specific line items.
111
112 =item credit_weight - Weight (relative to other package definitions) that controls credit application to specific line items.
113
114 =item agentnum - Optional agentnum (see L<FS::agent>)
115
116 =item fcc_ds0s - Optional DS0 equivalency number for FCC form 477
117
118 =item fcc_voip_class - Which column of FCC form 477 part II.B this package 
119 belongs in.
120
121 =item successor - Foreign key for the part_pkg that replaced this record.
122 If this record is not obsolete, will be null.
123
124 =item family_pkgpart - Foreign key for the part_pkg that was the earliest
125 ancestor of this record.  If this record is not a successor to another 
126 part_pkg, will be equal to pkgpart.
127
128 =item delay_start - Number of days to delay package start, by default
129
130 =item start_on_hold - 'Y' to suspend this package immediately when it is 
131 ordered. The package will not start billing or have a setup fee charged 
132 until it is manually unsuspended.
133
134 =back
135
136 =head1 METHODS
137
138 =over 4 
139
140 =item new HASHREF
141
142 Creates a new package definition.  To add the package definition to
143 the database, see L<"insert">.
144
145 =cut
146
147 sub table { 'part_pkg'; }
148
149 =item clone
150
151 An alternate constructor.  Creates a new package definition by duplicating
152 an existing definition.  A new pkgpart is assigned and the custom flag is
153 set to Y.  To add the package definition to the database, see L<"insert">.
154
155 =cut
156
157 sub clone {
158   my $self = shift;
159   my $class = ref($self);
160   my %hash = $self->hash;
161   $hash{'pkgpart'} = '';
162   $hash{'custom'} = 'Y';
163   #new FS::part_pkg ( \%hash ); # ?
164   new $class ( \%hash ); # ?
165 }
166
167 =item insert [ , OPTION => VALUE ... ]
168
169 Adds this package definition to the database.  If there is an error,
170 returns the error, otherwise returns false.
171
172 Currently available options are: I<pkg_svc>, I<primary_svc>, I<cust_pkg>, 
173 I<custnum_ref> and I<options>.
174
175 If I<pkg_svc> is set to a hashref with svcparts as keys and quantities as
176 values, appropriate FS::pkg_svc records will be inserted.  I<hidden_svc> can 
177 be set to a hashref of svcparts and flag values ('Y' or '') to set the 
178 'hidden' field in these records, and I<provision_hold> can be set similarly
179 for the 'provision_hold' field in these records.
180
181 If I<primary_svc> is set to the svcpart of the primary service, the appropriate
182 FS::pkg_svc record will be updated.
183
184 If I<cust_pkg> is set to a pkgnum of a FS::cust_pkg record (or the FS::cust_pkg
185 record itself), the object will be updated to point to this package definition.
186
187 In conjunction with I<cust_pkg>, if I<custnum_ref> is set to a scalar reference,
188 the scalar will be updated with the custnum value from the cust_pkg record.
189
190 If I<tax_overrides> is set to a hashref with usage classes as keys and comma
191 separated tax class numbers as values, appropriate FS::part_pkg_taxoverride
192 records will be inserted.
193
194 If I<options> is set to a hashref of options, appropriate FS::part_pkg_option
195 records will be inserted.
196
197 =cut
198
199 sub insert {
200   my $self = shift;
201   my %options = @_;
202   warn "FS::part_pkg::insert called on $self with options ".
203        join(', ', map "$_=>$options{$_}", keys %options)
204     if $DEBUG;
205
206   local $SIG{HUP} = 'IGNORE';
207   local $SIG{INT} = 'IGNORE';
208   local $SIG{QUIT} = 'IGNORE';
209   local $SIG{TERM} = 'IGNORE';
210   local $SIG{TSTP} = 'IGNORE';
211   local $SIG{PIPE} = 'IGNORE';
212
213   my $oldAutoCommit = $FS::UID::AutoCommit;
214   local $FS::UID::AutoCommit = 0;
215   my $dbh = dbh;
216
217   warn "  inserting part_pkg record" if $DEBUG;
218   my $error = $self->SUPER::insert( $options{options} );
219   if ( $error ) {
220     $dbh->rollback if $oldAutoCommit;
221     return $error;
222   }
223
224   # set family_pkgpart
225   if ( $self->get('family_pkgpart') eq '' ) {
226     $self->set('family_pkgpart' => $self->pkgpart);
227     $error = $self->SUPER::replace;
228     if ( $error ) {
229       $dbh->rollback if $oldAutoCommit;
230       return $error;
231     }
232   }
233
234   warn "  inserting part_pkg_taxoverride records" if $DEBUG;
235   my %overrides = %{ $options{'tax_overrides'} || {} };
236   foreach my $usage_class ( keys %overrides ) {
237     my $override =
238       ( exists($overrides{$usage_class}) && defined($overrides{$usage_class}) )
239         ? $overrides{$usage_class}
240         : '';
241     my @overrides = (grep "$_", split(',', $override) );
242     my $error = $self->process_m2m (
243                   'link_table'   => 'part_pkg_taxoverride',
244                   'target_table' => 'tax_class',
245                   'hashref'      => { 'usage_class' => $usage_class },
246                   'params'       => \@overrides,
247                 );
248     if ( $error ) {
249       $dbh->rollback if $oldAutoCommit;
250       return $error;
251     }
252   }
253
254   unless ( $skip_pkg_svc_hack ) {
255
256     warn "  inserting pkg_svc records" if $DEBUG;
257     my $pkg_svc = $options{'pkg_svc'} || {};
258     my $hidden_svc = $options{'hidden_svc'} || {};
259     my $provision_hold = $options{'provision_hold'} || {};
260     foreach my $part_svc ( qsearch('part_svc', {} ) ) {
261       my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
262       my $primary_svc =
263         ( $options{'primary_svc'} && $options{'primary_svc'}==$part_svc->svcpart )
264           ? 'Y'
265           : '';
266
267       my $pkg_svc = new FS::pkg_svc( {
268         'pkgpart'     => $self->pkgpart,
269         'svcpart'     => $part_svc->svcpart,
270         'quantity'    => $quantity, 
271         'primary_svc' => $primary_svc,
272         'hidden'      => $hidden_svc->{$part_svc->svcpart},
273         'provision_hold' => $provision_hold->{$part_svc->svcpart},
274       } );
275       my $error = $pkg_svc->insert;
276       if ( $error ) {
277         $dbh->rollback if $oldAutoCommit;
278         return $error;
279       }
280     }
281
282   }
283
284   if ( $options{'cust_pkg'} ) {
285     warn "  updating cust_pkg record " if $DEBUG;
286     my $old_cust_pkg =
287       ref($options{'cust_pkg'})
288         ? $options{'cust_pkg'}
289         : qsearchs('cust_pkg', { pkgnum => $options{'cust_pkg'} } );
290     ${ $options{'custnum_ref'} } = $old_cust_pkg->custnum
291       if $options{'custnum_ref'};
292     my %hash = $old_cust_pkg->hash;
293     $hash{'pkgpart'} = $self->pkgpart,
294     my $new_cust_pkg = new FS::cust_pkg \%hash;
295     local($FS::cust_pkg::disable_agentcheck) = 1;
296     my $error = $new_cust_pkg->replace($old_cust_pkg);
297     if ( $error ) {
298       $dbh->rollback if $oldAutoCommit;
299       return "Error modifying cust_pkg record: $error";
300     }
301   }
302
303   if ( $options{'part_pkg_vendor'} ) {
304       while ( my ($exportnum, $vendor_pkg_id) =
305                 each %{ $options{part_pkg_vendor} }
306             )
307       {
308             my $ppv = new FS::part_pkg_vendor( {
309                     'pkgpart' => $self->pkgpart,
310                     'exportnum' => $exportnum,
311                     'vendor_pkg_id' => $vendor_pkg_id, 
312                 } );
313             my $error = $ppv->insert;
314             if ( $error ) {
315               $dbh->rollback if $oldAutoCommit;
316               return "Error inserting part_pkg_vendor record: $error";
317             }
318       }
319   }
320
321   if ( $options{fcc_options} ) {
322     warn "  updating fcc options " if $DEBUG;
323     $self->set_fcc_options( $options{fcc_options} );
324   }
325
326   warn "  committing transaction" if $DEBUG and $oldAutoCommit;
327   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
328
329   '';
330 }
331
332 =item delete
333
334 Currently unimplemented.
335
336 =cut
337
338 sub delete {
339   return "Can't (yet?) delete package definitions.";
340 # check & make sure the pkgpart isn't in cust_pkg or type_pkgs?
341 }
342
343 =item replace OLD_RECORD [ , OPTION => VALUE ... ]
344
345 Replaces OLD_RECORD with this one in the database.  If there is an error,
346 returns the error, otherwise returns false.
347
348 Currently available options are: I<pkg_svc>, I<hidden_svc>, I<primary_svc>,
349 I<provision_hold> and I<options>
350
351 If I<pkg_svc> is set to a hashref with svcparts as keys and quantities as
352 values, the appropriate FS::pkg_svc records will be replaced.  I<hidden_svc>
353 can be set to a hashref of svcparts and flag values ('Y' or '') to set the 
354 'hidden' field in these records.  I<provision_hold> can be set 
355 to a hashref of svcparts and flag values ('Y' or '') to set the field 
356 in those records.
357
358 If I<primary_svc> is set to the svcpart of the primary service, the appropriate
359 FS::pkg_svc record will be updated.
360
361 If I<options> is set to a hashref, the appropriate FS::part_pkg_option records
362 will be replaced.
363
364 =cut
365
366 sub replace {
367   my $new = shift;
368
369   my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
370               ? shift
371               : $new->replace_old;
372
373   my $options = 
374     ( ref($_[0]) eq 'HASH' )
375       ? shift
376       : { @_ };
377
378   $options->{options} = { $old->options } unless defined($options->{options});
379
380   warn "FS::part_pkg::replace called on $new to replace $old with options".
381        join(', ', map "$_ => ". $options->{$_}, keys %$options)
382     if $DEBUG;
383
384   local $SIG{HUP} = 'IGNORE';
385   local $SIG{INT} = 'IGNORE';
386   local $SIG{QUIT} = 'IGNORE';
387   local $SIG{TERM} = 'IGNORE';
388   local $SIG{TSTP} = 'IGNORE';
389   local $SIG{PIPE} = 'IGNORE';
390
391   my $oldAutoCommit = $FS::UID::AutoCommit;
392   local $FS::UID::AutoCommit = 0;
393   my $dbh = dbh;
394   
395   my $conf = new FS::Conf;
396   if ( $conf->exists('part_pkg-lineage') ) {
397     if ( grep { $options->{options}->{$_} ne $old->option($_, 1) }
398           qw(setup_fee recur_fee) #others? config?
399         ) { 
400     
401       warn "  superseding package" if $DEBUG;
402
403       my $error = $new->supersede($old, %$options);
404       if ( $error ) {
405         $dbh->rollback if $oldAutoCommit;
406         return $error;
407       }
408       else {
409         warn "  committing transaction" if $DEBUG and $oldAutoCommit;
410         $dbh->commit if $oldAutoCommit;
411         return $error;
412       }
413     }
414     #else nothing
415   }
416
417   #plandata shit stays in replace for upgrades until after 2.0 (or edit
418   #_upgrade_data)
419   warn "  saving legacy plandata" if $DEBUG;
420   my $plandata = $new->get('plandata');
421   $new->set('plandata', '');
422
423   warn "  deleting old part_pkg_option records" if $DEBUG;
424   foreach my $part_pkg_option ( $old->part_pkg_option ) {
425     my $error = $part_pkg_option->delete;
426     if ( $error ) {
427       $dbh->rollback if $oldAutoCommit;
428       return $error;
429     }
430   }
431
432   warn "  replacing part_pkg record" if $DEBUG;
433   my $error = $new->SUPER::replace($old, $options->{options} );
434   if ( $error ) {
435     $dbh->rollback if $oldAutoCommit;
436     return $error;
437   }
438
439   warn "  inserting part_pkg_option records for plandata: $plandata|" if $DEBUG;
440   foreach my $part_pkg_option ( 
441     map { /^(\w+)=(.*)$/ or do { $dbh->rollback if $oldAutoCommit;
442                                  return "illegal plandata: $plandata";
443                                };
444           new FS::part_pkg_option {
445             'pkgpart'     => $new->pkgpart,
446             'optionname'  => $1,
447             'optionvalue' => $2,
448           };
449         }
450     split("\n", $plandata)
451   ) {
452     my $error = $part_pkg_option->insert;
453     if ( $error ) {
454       $dbh->rollback if $oldAutoCommit;
455       return $error;
456     }
457   }
458
459   warn "  replacing pkg_svc records" if $DEBUG;
460   my $pkg_svc = $options->{'pkg_svc'};
461   my $hidden_svc = $options->{'hidden_svc'} || {};
462   my $provision_hold = $options->{'provision_hold'} || {};
463   if ( $pkg_svc ) { # if it wasn't passed, don't change existing pkg_svcs
464     foreach my $part_svc ( qsearch('part_svc', {} ) ) {
465       my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
466       my $hidden = $hidden_svc->{$part_svc->svcpart} || '';
467       my $provision_hold = $provision_hold->{$part_svc->svcpart} || '';
468       my $primary_svc =
469         ( defined($options->{'primary_svc'}) && $options->{'primary_svc'}
470           && $options->{'primary_svc'} == $part_svc->svcpart
471         )
472           ? 'Y'
473           : '';
474
475       my $old_pkg_svc = qsearchs('pkg_svc', {
476           'pkgpart' => $old->pkgpart,
477           'svcpart' => $part_svc->svcpart,
478         }
479       );
480       my $old_quantity = 0;
481       my $old_primary_svc = '';
482       my $old_hidden = '';
483       my $old_provision_hold = '';
484       if ( $old_pkg_svc ) {
485         $old_quantity = $old_pkg_svc->quantity;
486         $old_primary_svc = $old_pkg_svc->primary_svc 
487           if $old_pkg_svc->dbdef_table->column('primary_svc'); # is this needed?
488         $old_hidden = $old_pkg_svc->hidden;
489         $old_provision_hold = $old_pkg_svc->provision_hold;
490       }
491    
492       next unless $old_quantity != $quantity || 
493                   $old_primary_svc ne $primary_svc ||
494                   $old_hidden ne $hidden ||
495                   $old_provision_hold ne $provision_hold;
496     
497       my $new_pkg_svc = new FS::pkg_svc( {
498         'pkgsvcnum'   => ( $old_pkg_svc ? $old_pkg_svc->pkgsvcnum : '' ),
499         'pkgpart'     => $new->pkgpart,
500         'svcpart'     => $part_svc->svcpart,
501         'quantity'    => $quantity, 
502         'primary_svc' => $primary_svc,
503         'hidden'      => $hidden,
504         'provision_hold' => $provision_hold,
505       } );
506       my $error = $old_pkg_svc
507                     ? $new_pkg_svc->replace($old_pkg_svc)
508                     : $new_pkg_svc->insert;
509       if ( $error ) {
510         $dbh->rollback if $oldAutoCommit;
511         return $error;
512       }
513     } #foreach $part_svc
514   } #if $options->{pkg_svc}
515   
516   my @part_pkg_vendor = $old->part_pkg_vendor;
517   my @current_exportnum = ();
518   if ( $options->{'part_pkg_vendor'} ) {
519       my($exportnum,$vendor_pkg_id);
520       while ( ($exportnum,$vendor_pkg_id) 
521                                 = each %{$options->{'part_pkg_vendor'}} ) {
522           my $noinsert = 0;
523           foreach my $part_pkg_vendor ( @part_pkg_vendor ) {
524             if($exportnum == $part_pkg_vendor->exportnum
525                 && $vendor_pkg_id ne $part_pkg_vendor->vendor_pkg_id) {
526                 $part_pkg_vendor->vendor_pkg_id($vendor_pkg_id);
527                 my $error = $part_pkg_vendor->replace;
528                 if ( $error ) {
529                   $dbh->rollback if $oldAutoCommit;
530                   return "Error replacing part_pkg_vendor record: $error";
531                 }
532                 $noinsert = 1;
533                 last;
534             }
535             elsif($exportnum == $part_pkg_vendor->exportnum
536                 && $vendor_pkg_id eq $part_pkg_vendor->vendor_pkg_id) {
537                 $noinsert = 1;
538                 last;
539             }
540           }
541           unless ( $noinsert ) {
542             my $ppv = new FS::part_pkg_vendor( {
543                     'pkgpart' => $new->pkgpart,
544                     'exportnum' => $exportnum,
545                     'vendor_pkg_id' => $vendor_pkg_id, 
546                 } );
547             my $error = $ppv->insert;
548             if ( $error ) {
549               $dbh->rollback if $oldAutoCommit;
550               return "Error inserting part_pkg_vendor record: $error";
551             }
552           }
553           push @current_exportnum, $exportnum;
554       }
555   }
556   foreach my $part_pkg_vendor ( @part_pkg_vendor ) {
557       unless ( grep($_ eq $part_pkg_vendor->exportnum, @current_exportnum) ) {
558         my $error = $part_pkg_vendor->delete;
559         if ( $error ) {
560           $dbh->rollback if $oldAutoCommit;
561           return "Error deleting part_pkg_vendor record: $error";
562         }
563       }
564   }
565   
566   # propagate changes to certain core fields
567   if ( $conf->exists('part_pkg-lineage') ) {
568     warn "  propagating changes to family" if $DEBUG;
569     my $error = $new->propagate($old);
570     if ( $error ) {
571       $dbh->rollback if $oldAutoCommit;
572       return $error;
573     }
574   }
575
576   if ( $options->{fcc_options} ) {
577     warn "  updating fcc options " if $DEBUG;
578     $new->set_fcc_options( $options->{fcc_options} );
579   }
580
581   warn "  committing transaction" if $DEBUG and $oldAutoCommit;
582   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
583   '';
584 }
585
586 =item check
587
588 Checks all fields to make sure this is a valid package definition.  If
589 there is an error, returns the error, otherwise returns false.  Called by the
590 insert and replace methods.
591
592 =cut
593
594 sub check {
595   my $self = shift;
596   warn "FS::part_pkg::check called on $self" if $DEBUG;
597
598   for (qw(setup recur plandata)) {
599     #$self->set($_=>0) if $self->get($_) =~ /^\s*$/; }
600     return "Use of $_ field is deprecated; set a plan and options: ".
601            $self->get($_)
602       if length($self->get($_));
603     $self->set($_, '');
604   }
605
606   if ( $self->dbdef_table->column('freq')->type =~ /(int)/i ) {
607     my $error = $self->ut_number('freq');
608     return $error if $error;
609   } else {
610     $self->freq =~ /^(\d+[hdw]?)$/
611       or return "Illegal or empty freq: ". $self->freq;
612     $self->freq($1);
613   }
614
615   my @null_agentnum_right = ( 'Edit global package definitions' );
616   push @null_agentnum_right, 'One-time charge'
617     if $self->freq =~ /^0/;
618   push @null_agentnum_right, 'Customize customer package'
619     if $self->disabled eq 'Y'; #good enough
620
621   my $error = $self->ut_numbern('pkgpart')
622     || $self->ut_text('pkg')
623     || $self->ut_textn('comment')
624     || $self->ut_textn('promo_code')
625     || $self->ut_alphan('plan')
626     || $self->ut_flag('setuptax')
627     || $self->ut_flag('recurtax')
628     || $self->ut_textn('taxclass')
629     || $self->ut_flag('disabled')
630     || $self->ut_flag('custom')
631     || $self->ut_flag('no_auto')
632     || $self->ut_flag('recur_show_zero')
633     || $self->ut_flag('setup_show_zero')
634     || $self->ut_flag('start_on_hold')
635     #|| $self->ut_moneyn('setup_cost')
636     #|| $self->ut_moneyn('recur_cost')
637     || $self->ut_floatn('setup_cost')
638     || $self->ut_floatn('recur_cost')
639     || $self->ut_floatn('pay_weight')
640     || $self->ut_floatn('credit_weight')
641     || $self->ut_numbern('taxproductnum')
642     || $self->ut_foreign_keyn('classnum',       'pkg_class', 'classnum')
643     || $self->ut_foreign_keyn('addon_classnum', 'pkg_class', 'classnum')
644     || $self->ut_foreign_keyn('taxproductnum',
645                               'part_pkg_taxproduct',
646                               'taxproductnum'
647                              )
648     || ( $setup_hack
649            ? $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum' )
650            : $self->ut_agentnum_acl('agentnum', \@null_agentnum_right)
651        )
652     || $self->ut_numbern('fcc_ds0s')
653     || $self->ut_numbern('fcc_voip_class')
654     || $self->ut_numbern('delay_start')
655     || $self->ut_foreign_keyn('successor', 'part_pkg', 'pkgpart')
656     || $self->ut_foreign_keyn('family_pkgpart', 'part_pkg', 'pkgpart')
657     || $self->ut_alphan('agent_pkgpartid')
658     || $self->SUPER::check
659   ;
660   return $error if $error;
661
662   return 'Unknown plan '. $self->plan
663     unless exists($plans{$self->plan});
664
665   my $conf = new FS::Conf;
666   return 'Taxclass is required'
667     if ! $self->taxclass && $conf->exists('require_taxclasses');
668
669   '';
670 }
671
672 =item check_options
673
674 For a passed I<$options> hashref, validates any options that
675 have 'validate' subroutines defined in the info hash, 
676 then validates the entire hashref if the price plan has 
677 its own 'validate' subroutine defined in the info hash 
678 (I<$options> values might be altered.)  
679
680 Returns error message, or empty string if valid.
681
682 Invoked by L</insert> and L</replace> via the equivalent
683 methods in L<FS::option_Common>.
684
685 =cut
686
687 sub check_options {
688   my ($self,$options) = @_;
689   foreach my $option (keys %$options) {
690     if (exists $plans{ $self->plan }->{fields}->{$option}) {
691       if (exists($plans{$self->plan}->{fields}->{$option}->{'validate'})) {
692         # pass option name for use in error message
693         # pass a reference to the $options value, so it can be cleaned up
694         my $error = &{$plans{$self->plan}->{fields}->{$option}->{'validate'}}($option,\($options->{$option}));
695         return $error if $error;
696       }
697     } # else "option does not exist" error?
698   }
699   if (exists($plans{$self->plan}->{'validate'})) {
700     my $error = &{$plans{$self->plan}->{'validate'}}($options);
701     return $error if $error;
702   }
703   return '';
704 }
705
706 =item supersede OLD [, OPTION => VALUE ... ]
707
708 Inserts this package as a successor to the package OLD.  All options are as
709 for C<insert>.  After inserting, disables OLD and sets the new package as its
710 successor.
711
712 =cut
713
714 sub supersede {
715   my ($new, $old, %options) = @_;
716   my $error;
717
718   $new->set('pkgpart' => '');
719   $new->set('family_pkgpart' => $old->family_pkgpart);
720   warn "    inserting successor package\n" if $DEBUG;
721   $error = $new->insert(%options);
722   return $error if $error;
723  
724   warn "    disabling superseded package\n" if $DEBUG; 
725   $old->set('successor' => $new->pkgpart);
726   $old->set('disabled' => 'Y');
727   $error = $old->SUPER::replace; # don't change its options/pkg_svc records
728   return $error if $error;
729
730   warn "  propagating changes to family" if $DEBUG;
731   $new->propagate($old);
732 }
733
734 =item propagate OLD
735
736 If any of certain fields have changed from OLD to this package, then,
737 for all packages in the same lineage as this one, sets those fields 
738 to their values in this package.
739
740 =cut
741
742 my @propagate_fields = (
743   qw( pkg classnum setup_cost recur_cost taxclass
744   setuptax recurtax pay_weight credit_weight
745   )
746 );
747
748 sub propagate {
749   my $new = shift;
750   my $old = shift;
751   my %fields = (
752     map { $_ => $new->get($_) }
753     grep { $new->get($_) ne $old->get($_) }
754     @propagate_fields
755   );
756
757   my @part_pkg = qsearch('part_pkg', { 
758       'family_pkgpart' => $new->family_pkgpart 
759   });
760   my @error;
761   foreach my $part_pkg ( @part_pkg ) {
762     my $pkgpart = $part_pkg->pkgpart;
763     next if $pkgpart == $new->pkgpart; # don't modify $new
764     warn "    propagating to pkgpart $pkgpart\n" if $DEBUG;
765     foreach ( keys %fields ) {
766       $part_pkg->set($_, $fields{$_});
767     }
768     # SUPER::replace to avoid changing non-core fields
769     my $error = $part_pkg->SUPER::replace;
770     push @error, "pkgpart $pkgpart: $error"
771       if $error;
772   }
773   join("\n", @error);
774 }
775
776 =item set_fcc_options HASHREF
777
778 Sets the FCC options on this package definition to the values specified
779 in HASHREF.
780
781 =cut
782
783 sub set_fcc_options {
784   my $self = shift;
785   my $pkgpart = $self->pkgpart;
786   my $options;
787   if (ref $_[0]) {
788     $options = shift;
789   } else {
790     $options = { @_ };
791   }
792
793   my %existing_num = map { $_->fccoptionname => $_->num }
794                      qsearch('part_pkg_fcc_option', { pkgpart => $pkgpart });
795
796   local $FS::Record::nowarn_identical = 1;
797   # set up params for process_o2m
798   my $i = 0;
799   my $params = {};
800   foreach my $name (keys %$options ) {
801     $params->{ "num$i" } = $existing_num{$name} || '';
802     $params->{ "num$i".'_fccoptionname' } = $name;
803     $params->{ "num$i".'_optionvalue'   } = $options->{$name};
804     $i++;
805   }
806
807   $self->process_o2m(
808     table   => 'part_pkg_fcc_option',
809     fields  => [qw( fccoptionname optionvalue )],
810     params  => $params,
811   );
812 }
813
814 =item pkg_locale LOCALE
815
816 Returns a customer-viewable string representing this package for the given
817 locale, from the part_pkg_msgcat table.  If the given locale is empty or no
818 localized string is found, returns the base pkg field.
819
820 =cut
821
822 sub pkg_locale {
823   my( $self, $locale ) = @_;
824   return $self->pkg unless $locale;
825   my $part_pkg_msgcat = $self->part_pkg_msgcat($locale) or return $self->pkg;
826   $part_pkg_msgcat->pkg;
827 }
828
829 =item part_pkg_msgcat LOCALE
830
831 Like pkg_locale, but returns the FS::part_pkg_msgcat object itself.
832
833 =cut
834
835 sub part_pkg_msgcat {
836   my( $self, $locale ) = @_;
837   qsearchs( 'part_pkg_msgcat', {
838     pkgpart => $self->pkgpart,
839     locale  => $locale,
840   });
841 }
842
843 =item pkg_comment [ OPTION => VALUE... ]
844
845 Returns an (internal) string representing this package.  Currently,
846 "pkgpart: pkg - comment", is returned.  "pkg - comment" may be returned in the
847 future, omitting pkgpart.  The comment will have '(CUSTOM) ' prepended if
848 custom is Y.
849
850 If the option nopkgpart is true then the "pkgpart: ' is omitted.
851
852 =cut
853
854 sub pkg_comment {
855   my $self = shift;
856   my %opt = @_;
857
858   #$self->pkg. ' - '. $self->comment;
859   #$self->pkg. ' ('. $self->comment. ')';
860   my $pre = $opt{nopkgpart} ? '' : $self->pkgpart. ': ';
861   my $custom_comment = $self->custom_comment(%opt);
862   $pre. $self->pkg. ( $custom_comment ? " - $custom_comment" : '' );
863 }
864
865 #without price info (so without hitting the DB again)
866 sub pkg_comment_only {
867   my $self = shift;
868   my %opt = @_;
869
870   my $pre = $opt{nopkgpart} ? '' : $self->pkgpart. ': ';
871   my $comment = $self->comment;
872   $pre. $self->pkg. ( $comment ? " - $comment" : '' );
873 }
874
875 sub price_info { # safety, in case a part_pkg hasn't defined price_info
876     '';
877 }
878
879 sub custom_comment {
880   my $self = shift;
881   my $price_info = $self->price_info(@_);
882   ( $self->custom ? '(CUSTOM) ' : '' ).
883     $self->comment.
884     ( ( ($self->custom || $self->comment) && $price_info ) ? ' - ' : '' ).
885     $price_info;
886 }
887
888 sub pkg_price_info {
889   my $self = shift;
890   $self->pkg. ' - '. ($self->price_info || 'No charge');
891 }
892
893 =item pkg_class
894
895 Returns the package class, as an FS::pkg_class object, or the empty string
896 if there is no package class.
897
898 =cut
899
900 sub pkg_class {
901   my $self = shift;
902   if ( $self->classnum ) {
903     qsearchs('pkg_class', { 'classnum' => $self->classnum } );
904   } else {
905     return '';
906   }
907 }
908
909 =item addon_pkg_class
910
911 Returns the add-on package class, as an FS::pkg_class object, or the empty
912 string if there is no add-on package class.
913
914 =cut
915
916 sub addon_pkg_class {
917   my $self = shift;
918   if ( $self->addon_classnum ) {
919     qsearchs('pkg_class', { 'classnum' => $self->addon_classnum } );
920   } else {
921     return '';
922   }
923 }
924
925 =item categoryname 
926
927 Returns the package category name, or the empty string if there is no package
928 category.
929
930 =cut
931
932 sub categoryname {
933   my $self = shift;
934   my $pkg_class = $self->pkg_class;
935   $pkg_class
936     ? $pkg_class->categoryname
937     : '';
938 }
939
940 =item classname 
941
942 Returns the package class name, or the empty string if there is no package
943 class.
944
945 =cut
946
947 sub classname {
948   my $self = shift;
949   my $pkg_class = $self->pkg_class;
950   $pkg_class
951     ? $pkg_class->classname
952     : '';
953 }
954
955 =item addon_classname 
956
957 Returns the add-on package class name, or the empty string if there is no
958 add-on package class.
959
960 =cut
961
962 sub addon_classname {
963   my $self = shift;
964   my $pkg_class = $self->addon_pkg_class;
965   $pkg_class
966     ? $pkg_class->classname
967     : '';
968 }
969
970 =item agent 
971
972 Returns the associated agent for this event, if any, as an FS::agent object.
973
974 =cut
975
976 sub agent {
977   my $self = shift;
978   qsearchs('agent', { 'agentnum' => $self->agentnum } );
979 }
980
981 =item pkg_svc [ HASHREF | OPTION => VALUE ]
982
983 Returns all FS::pkg_svc objects (see L<FS::pkg_svc>) for this package
984 definition (with non-zero quantity).
985
986 One option is available, I<disable_linked>.  If set true it will return the
987 services for this package definition alone, omitting services from any add-on
988 packages.
989
990 =cut
991
992 =item type_pkgs
993
994 Returns all FS::type_pkgs objects (see L<FS::type_pkgs>) for this package
995 definition.
996
997 =cut
998
999 sub type_pkgs {
1000   my $self = shift;
1001   qsearch('type_pkgs', { 'pkgpart' => $self->pkgpart } );
1002 }
1003
1004 sub pkg_svc {
1005   my $self = shift;
1006
1007   return @{ $cache_pkg_svc{$self->pkgpart} }
1008     if $cache_enabled && $cache_pkg_svc{$self->pkgpart};
1009
1010 #  #sort { $b->primary cmp $a->primary } 
1011 #    grep { $_->quantity }
1012 #      qsearch( 'pkg_svc', { 'pkgpart' => $self->pkgpart } );
1013
1014   my $opt = ref($_[0]) ? $_[0] : { @_ };
1015   my %pkg_svc = map  { $_->svcpart => $_ } $self->_pkg_svc;
1016
1017   unless ( $opt->{disable_linked} ) {
1018     foreach my $dst_pkg ( map $_->dst_pkg, $self->svc_part_pkg_link ) {
1019       my @pkg_svc = $dst_pkg->_pkg_svc;
1020       foreach my $pkg_svc ( @pkg_svc ) {
1021         if ( $pkg_svc{$pkg_svc->svcpart} ) {
1022           my $quantity = $pkg_svc{$pkg_svc->svcpart}->quantity;
1023           $pkg_svc{$pkg_svc->svcpart}->quantity($quantity + $pkg_svc->quantity);
1024         } else {
1025           $pkg_svc{$pkg_svc->svcpart} = $pkg_svc;
1026         }
1027       }
1028     }
1029   }
1030
1031   my @pkg_svc = values(%pkg_svc);
1032
1033   $cache_pkg_svc{$self->pkgpart} = \@pkg_svc if $cache_enabled;
1034
1035   @pkg_svc;
1036
1037 }
1038
1039 sub _pkg_svc {
1040   my $self = shift;
1041   grep { $_->quantity }
1042     qsearch({
1043       'select'    => 'pkg_svc.*, part_svc.*',
1044       'table'     => 'pkg_svc',
1045       'addl_from' => 'LEFT JOIN part_svc USING ( svcpart )',
1046       'hashref'   => { 'pkgpart' => $self->pkgpart },
1047     });
1048 }
1049
1050 =item svcpart [ SVCDB ]
1051
1052 Returns the svcpart of the primary service definition (see L<FS::part_svc>)
1053 associated with this package definition (see L<FS::pkg_svc>).  Returns
1054 false if there not a primary service definition or exactly one service
1055 definition with quantity 1, or if SVCDB is specified and does not match the
1056 svcdb of the service definition.  SVCDB can be specified as a scalar table
1057 name, such as 'svc_acct', or as an arrayref of possible table names.
1058
1059 =cut
1060
1061 sub svcpart {
1062   my $pkg_svc = shift->_primary_pkg_svc(@_);
1063   $pkg_svc ? $pkg_svc->svcpart : '';
1064 }
1065
1066 =item part_svc [ SVCDB ]
1067
1068 Like the B<svcpart> method, but returns the FS::part_svc object (see
1069 L<FS::part_svc>).
1070
1071 =cut
1072
1073 sub part_svc {
1074   my $pkg_svc = shift->_primary_pkg_svc(@_);
1075   $pkg_svc ? $pkg_svc->part_svc : '';
1076 }
1077
1078 sub _primary_pkg_svc {
1079   my $self = shift;
1080
1081   my $svcdb = scalar(@_) ? shift : [];
1082   $svcdb = ref($svcdb) ? $svcdb : [ $svcdb ];
1083   my %svcdb = map { $_=>1 } @$svcdb;
1084
1085   my @svcdb_pkg_svc =
1086     grep { !scalar(@$svcdb) || $svcdb{ $_->part_svc->svcdb } }
1087          $self->pkg_svc;
1088
1089   my @pkg_svc = grep { $_->primary_svc =~ /^Y/i } @svcdb_pkg_svc;
1090   @pkg_svc = grep {$_->quantity == 1 } @svcdb_pkg_svc
1091     unless @pkg_svc;
1092   return '' if scalar(@pkg_svc) != 1;
1093   $pkg_svc[0];
1094 }
1095
1096 =item svcpart_unique_svcdb SVCDB
1097
1098 Returns the svcpart of a service definition (see L<FS::part_svc>) matching
1099 SVCDB associated with this package definition (see L<FS::pkg_svc>).  Returns
1100 false if there not a primary service definition for SVCDB or there are multiple
1101 service definitions for SVCDB.
1102
1103 =cut
1104
1105 sub svcpart_unique_svcdb {
1106   my( $self, $svcdb ) = @_;
1107   my @svcdb_pkg_svc = grep { ( $svcdb eq $_->part_svc->svcdb ) } $self->pkg_svc;
1108   return '' if scalar(@svcdb_pkg_svc) != 1;
1109   $svcdb_pkg_svc[0]->svcpart;
1110 }
1111
1112 =item payby
1113
1114 Returns a list of the acceptable payment types for this package.  Eventually
1115 this should come out of a database table and be editable, but currently has the
1116 following logic instead:
1117
1118 If the package is free, the single item B<BILL> is
1119 returned, otherwise, the single item B<CARD> is returned.
1120
1121 (CHEK?  LEC?  Probably shouldn't accept those by default, prone to abuse)
1122
1123 =cut
1124
1125 sub payby {
1126   my $self = shift;
1127   if ( $self->is_free ) {
1128     ( 'BILL' );
1129   } else {
1130     ( 'CARD' );
1131   }
1132 }
1133
1134 =item is_free
1135
1136 Returns true if this package is free.  
1137
1138 =cut
1139
1140 sub is_free {
1141   my $self = shift;
1142   if ( $self->can('is_free_options') ) {
1143     not grep { $_ !~ /^\s*0*(\.0*)?\s*$/ }
1144          map { $self->option($_) } 
1145              $self->is_free_options;
1146   } else {
1147     warn "FS::part_pkg::is_free: FS::part_pkg::". $self->plan. " subclass ".
1148          "provides neither is_free_options nor is_free method; returning false";
1149     0;
1150   }
1151 }
1152
1153 # whether the plan allows discounts to be applied to this package
1154 sub can_discount { 0; }
1155
1156 # whether the plan allows changing the start date
1157 sub can_start_date {
1158   my $self = shift;
1159   $self->start_on_hold ? 0 : 1;
1160 }
1161
1162 # the delay start date if present
1163 sub delay_start_date {
1164   my $self = shift;
1165
1166   my $delay = $self->delay_start or return '';
1167
1168   # avoid timelocal silliness  
1169   my $dt = DateTime->today(time_zone => 'local');
1170   $dt->add(days => $delay);
1171   $dt->epoch;
1172 }
1173
1174 sub freqs_href {
1175   # moved to FS::Misc to make this accessible to other packages
1176   # at initialization
1177   FS::Misc::pkg_freqs();
1178 }
1179
1180 =item freq_pretty
1181
1182 Returns an english representation of the I<freq> field, such as "monthly",
1183 "weekly", "semi-annually", etc.
1184
1185 =cut
1186
1187 sub freq_pretty {
1188   my $self = shift;
1189   my $freq = $self->freq;
1190
1191   #my $freqs_href = $self->freqs_href;
1192   my $freqs_href = freqs_href();
1193
1194   if ( exists($freqs_href->{$freq}) ) {
1195     $freqs_href->{$freq};
1196   } else {
1197     my $interval = 'month';
1198     if ( $freq =~ /^(\d+)([hdw])$/ ) {
1199       my %interval = ( 'h' => 'hour', 'd'=>'day', 'w'=>'week' );
1200       $interval = $interval{$2};
1201     }
1202     if ( $1 == 1 ) {
1203       "every $interval";
1204     } else {
1205       "every $freq ${interval}s";
1206     }
1207   }
1208 }
1209
1210 =item add_freq TIMESTAMP [ FREQ ]
1211
1212 Adds a billing period of some frequency to the provided timestamp and 
1213 returns the resulting timestamp, or -1 if the frequency could not be 
1214 parsed (shouldn't happen).  By default, the frequency of this package 
1215 will be used; to override this, pass a different frequency as a second 
1216 argument.
1217
1218 =cut
1219
1220 sub add_freq {
1221   my( $self, $date, $freq ) = @_;
1222   $freq = $self->freq unless $freq;
1223
1224   #change this bit to use Date::Manip? CAREFUL with timezones (see
1225   # mailing list archive)
1226   my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($date) )[0,1,2,3,4,5];
1227
1228   if ( $freq =~ /^\d+$/ ) {
1229     $mon += $freq;
1230     until ( $mon < 12 ) { $mon -= 12; $year++; }
1231
1232     $mday = 28 if $mday > 28 && FS::Conf->new->exists('anniversary-rollback');
1233
1234   } elsif ( $freq =~ /^(\d+)w$/ ) {
1235     my $weeks = $1;
1236     $mday += $weeks * 7;
1237   } elsif ( $freq =~ /^(\d+)d$/ ) {
1238     my $days = $1;
1239     $mday += $days;
1240   } elsif ( $freq =~ /^(\d+)h$/ ) {
1241     my $hours = $1;
1242     $hour += $hours;
1243   } else {
1244     return -1;
1245   }
1246
1247   timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year);
1248 }
1249
1250 =item plandata
1251
1252 For backwards compatibility, returns the plandata field as well as all options
1253 from FS::part_pkg_option.
1254
1255 =cut
1256
1257 sub plandata {
1258   my $self = shift;
1259   carp "plandata is deprecated";
1260   if ( @_ ) {
1261     $self->SUPER::plandata(@_);
1262   } else {
1263     my $plandata = $self->get('plandata');
1264     my %options = $self->options;
1265     $plandata .= join('', map { "$_=$options{$_}\n" } keys %options );
1266     $plandata;
1267   }
1268 }
1269
1270 =item part_pkg_vendor
1271
1272 Returns all vendor/external package ids as FS::part_pkg_vendor objects (see
1273 L<FS::part_pkg_vendor>).
1274
1275 =cut
1276
1277 sub part_pkg_vendor {
1278   my $self = shift;
1279   qsearch('part_pkg_vendor', { 'pkgpart' => $self->pkgpart } );
1280 }
1281
1282 =item vendor_pkg_ids
1283
1284 Returns a list of vendor/external package ids by exportnum
1285
1286 =cut
1287
1288 sub vendor_pkg_ids {
1289   my $self = shift;
1290   map { $_->exportnum => $_->vendor_pkg_id } $self->part_pkg_vendor;
1291 }
1292
1293 =item part_pkg_option
1294
1295 Returns all options as FS::part_pkg_option objects (see
1296 L<FS::part_pkg_option>).
1297
1298 =cut
1299
1300 sub part_pkg_option {
1301   my $self = shift;
1302   qsearch('part_pkg_option', { 'pkgpart' => $self->pkgpart } );
1303 }
1304
1305 =item options 
1306
1307 Returns a list of option names and values suitable for assigning to a hash.
1308
1309 =cut
1310
1311 sub options {
1312   my $self = shift;
1313   map { $_->optionname => $_->optionvalue } $self->part_pkg_option;
1314 }
1315
1316 =item option OPTIONNAME [ QUIET ]
1317
1318 Returns the option value for the given name, or the empty string.  If a true
1319 value is passed as the second argument, warnings about missing the option
1320 will be suppressed.
1321
1322 =cut
1323
1324 sub option {
1325   my( $self, $opt, $ornull ) = @_;
1326
1327   #cache: was pulled up in the original part_pkg query
1328   return $self->hashref->{"_opt_$opt"}
1329     if exists $self->hashref->{"_opt_$opt"};
1330
1331   cluck "$self -> option: searching for $opt" if $DEBUG;
1332   my $part_pkg_option =
1333     qsearchs('part_pkg_option', {
1334       pkgpart    => $self->pkgpart,
1335       optionname => $opt,
1336   } );
1337   return $part_pkg_option->optionvalue if $part_pkg_option;
1338
1339   my %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); }
1340                      split("\n", $self->get('plandata') );
1341   return $plandata{$opt} if exists $plandata{$opt};
1342   cluck "WARNING: (pkgpart ". $self->pkgpart. ") Package def option $opt ".
1343         "not found in options or plandata!\n"
1344     unless $ornull;
1345
1346   '';
1347 }
1348
1349 =item fcc_option OPTIONNAME
1350
1351 Returns the FCC 477 report option value for the given name, or the empty 
1352 string.
1353
1354 =cut
1355
1356 sub fcc_option {
1357   my ($self, $name) = @_;
1358   my $part_pkg_fcc_option =
1359     qsearchs('part_pkg_fcc_option', {
1360         pkgpart => $self->pkgpart,
1361         fccoptionname => $name,
1362     });
1363   $part_pkg_fcc_option ? $part_pkg_fcc_option->optionvalue : '';
1364 }
1365
1366 =item fcc_options
1367
1368 Returns all FCC 477 report options for this package, as a hash-like list.
1369
1370 =cut
1371
1372 sub fcc_options {
1373   my $self = shift;
1374   map { $_->fccoptionname => $_->optionvalue }
1375     qsearch('part_pkg_fcc_option', { pkgpart => $self->pkgpart });
1376 }
1377
1378 =item bill_part_pkg_link
1379
1380 Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
1381
1382 =cut
1383
1384 sub bill_part_pkg_link {
1385   shift->_part_pkg_link('bill', @_);
1386 }
1387
1388 =item svc_part_pkg_link
1389
1390 Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
1391
1392 =cut
1393
1394 sub svc_part_pkg_link {
1395   shift->_part_pkg_link('svc', @_);
1396 }
1397
1398 =item supp_part_pkg_link
1399
1400 Returns the associated part_pkg_link records of type 'supp' (supplemental
1401 packages).
1402
1403 =cut
1404
1405 sub supp_part_pkg_link {
1406   shift->_part_pkg_link('supp', @_);
1407 }
1408
1409 sub _part_pkg_link {
1410   my( $self, $type ) = @_;
1411
1412   return @{ $cache_link{$type}->{$self->pkgpart} }
1413     if $cache_enabled && $cache_link{$type}->{$self->pkgpart};
1414
1415   cluck $type.'_part_pkg_link called' if $DEBUG;
1416
1417   my @ppl = 
1418     qsearch({ table    => 'part_pkg_link',
1419               hashref  => { src_pkgpart => $self->pkgpart,
1420                             link_type   => $type,
1421                             #protection against infinite recursive links
1422                             dst_pkgpart => { op=>'!=', value=> $self->pkgpart },
1423                           },
1424               order_by => "ORDER BY hidden",
1425            });
1426
1427   $cache_link{$type}->{$self->pkgpart} = \@ppl if $cache_enabled;
1428
1429   return @ppl;
1430 }
1431
1432 sub self_and_bill_linked {
1433   shift->_self_and_linked('bill', @_);
1434 }
1435
1436 sub self_and_svc_linked {
1437   shift->_self_and_linked('svc', @_);
1438 }
1439
1440 sub _self_and_linked {
1441   my( $self, $type, $hidden ) = @_;
1442   $hidden ||= '';
1443
1444   my @result = ();
1445   foreach ( ( $self, map { $_->dst_pkg->_self_and_linked($type, $_->hidden) }
1446                      $self->_part_pkg_link($type) ) )
1447   {
1448     $_->hidden($hidden) if $hidden;
1449     push @result, $_;
1450   }
1451
1452   (@result);
1453 }
1454
1455 =item part_pkg_taxoverride [ CLASS ]
1456
1457 Returns all associated FS::part_pkg_taxoverride objects (see
1458 L<FS::part_pkg_taxoverride>).  Limits the returned set to those
1459 of class CLASS if defined.  Class may be one of 'setup', 'recur',
1460 the empty string (default), or a usage class number (see L<FS::usage_class>).
1461 When a class is specified, the empty string class (default) is returned
1462 if no more specific values exist.
1463
1464 =cut
1465
1466 sub part_pkg_taxoverride {
1467   my $self = shift;
1468   my $class = shift;
1469
1470   my $hashref = { 'pkgpart' => $self->pkgpart };
1471   $hashref->{'usage_class'} = $class if defined($class);
1472   my @overrides = qsearch('part_pkg_taxoverride', $hashref );
1473
1474   unless ( scalar(@overrides) || !defined($class) || !$class ){
1475     $hashref->{'usage_class'} = '';
1476     @overrides = qsearch('part_pkg_taxoverride', $hashref );
1477   }
1478
1479   @overrides;
1480 }
1481
1482 =item has_taxproduct
1483
1484 Returns true if this package has any taxproduct associated with it.  
1485
1486 =cut
1487
1488 sub has_taxproduct {
1489   my $self = shift;
1490
1491   $self->taxproductnum ||
1492   scalar( grep { $_ =~/^usage_taxproductnum_/ && $self->option($_) } 
1493           keys %{ {$self->options} }
1494   )
1495
1496 }
1497
1498
1499 =item taxproduct [ CLASS ]
1500
1501 Returns the associated tax product for this package definition (see
1502 L<FS::part_pkg_taxproduct>).  CLASS may be one of 'setup', 'recur' or
1503 the usage classnum (see L<FS::usage_class>).  Returns the default
1504 tax product for this record if the more specific CLASS value does
1505 not exist.
1506
1507 =cut
1508
1509 sub taxproduct {
1510   my $self = shift;
1511   my $class = shift;
1512
1513   my $part_pkg_taxproduct;
1514
1515   my $taxproductnum = $self->taxproductnum;
1516   if ($class) { 
1517     my $class_taxproductnum = $self->option("usage_taxproductnum_$class", 1);
1518     $taxproductnum = $class_taxproductnum
1519       if $class_taxproductnum
1520   }
1521   
1522   $part_pkg_taxproduct =
1523     qsearchs( 'part_pkg_taxproduct', { 'taxproductnum' => $taxproductnum } );
1524
1525   unless ($part_pkg_taxproduct || $taxproductnum eq $self->taxproductnum ) {
1526     $taxproductnum = $self->taxproductnum;
1527     $part_pkg_taxproduct =
1528       qsearchs( 'part_pkg_taxproduct', { 'taxproductnum' => $taxproductnum } );
1529   }
1530
1531   $part_pkg_taxproduct;
1532 }
1533
1534 =item taxproduct_description [ CLASS ]
1535
1536 Returns the description of the associated tax product for this package
1537 definition (see L<FS::part_pkg_taxproduct>).
1538
1539 =cut
1540
1541 sub taxproduct_description {
1542   my $self = shift;
1543   my $part_pkg_taxproduct = $self->taxproduct(@_);
1544   $part_pkg_taxproduct ? $part_pkg_taxproduct->description : '';
1545 }
1546
1547
1548 =item tax_rates DATA_PROVIDER, GEOCODE, [ CLASS ]
1549
1550 Returns the tax table entries (L<FS::tax_rate> objects) that apply to this
1551 package in the location specified by GEOCODE, for usage class CLASS (one of
1552 'setup', 'recur', null, or a C<usage_class> number).
1553
1554 =cut
1555
1556 sub tax_rates {
1557   my $self = shift;
1558   my ($vendor, $geocode, $class) = @_;
1559   # if this part_pkg is overridden into a specific taxclass, get that class
1560   my @taxclassnums = map { $_->taxclassnum } 
1561                      $self->part_pkg_taxoverride($class);
1562   # otherwise, get its tax product category
1563   if (!@taxclassnums) {
1564     my $part_pkg_taxproduct = $self->taxproduct($class);
1565     # If this isn't defined, then the class has no taxproduct designation,
1566     # so return no tax rates.
1567     return () if !$part_pkg_taxproduct;
1568
1569     # convert the taxproduct to the tax classes that might apply to it in 
1570     # $geocode
1571     @taxclassnums = map { $_->taxclassnum }
1572                     grep { $_->taxable eq 'Y' } # why do we need this?
1573                     $part_pkg_taxproduct->part_pkg_taxrate($geocode);
1574   }
1575   return unless @taxclassnums;
1576
1577   # then look up the actual tax_rate entries
1578   warn "Found taxclassnum values of ". join(',', @taxclassnums) ."\n"
1579       if $DEBUG;
1580   my $extra_sql = "AND taxclassnum IN (". join(',', @taxclassnums) . ")";
1581   my @taxes = qsearch({ 'table'     => 'tax_rate',
1582                         'hashref'   => { 'geocode'     => $geocode,
1583                                          'data_vendor' => $vendor,
1584                                          'disabled'    => '' },
1585                         'extra_sql' => $extra_sql,
1586                       });
1587   warn "Found taxes ". join(',', map {$_->taxnum} @taxes) ."\n"
1588       if $DEBUG;
1589
1590   return @taxes;
1591 }
1592
1593 =item part_pkg_discount
1594
1595 Returns the package to discount m2m records (see L<FS::part_pkg_discount>)
1596 for this package.
1597
1598 =cut
1599
1600 sub part_pkg_discount {
1601   my $self = shift;
1602   qsearch('part_pkg_discount', { 'pkgpart' => $self->pkgpart });
1603 }
1604
1605 =item part_pkg_usage
1606
1607 Returns the voice usage pools (see L<FS::part_pkg_usage>) defined for 
1608 this package.
1609
1610 =cut
1611
1612 sub part_pkg_usage {
1613   my $self = shift;
1614   qsearch('part_pkg_usage', { 'pkgpart' => $self->pkgpart });
1615 }
1616
1617 =item _rebless
1618
1619 Reblesses the object into the FS::part_pkg::PLAN class (if available), where
1620 PLAN is the object's I<plan> field.  There should be better docs
1621 on how to create new price plans, but until then, see L</NEW PLAN CLASSES>.
1622
1623 =cut
1624
1625 sub _rebless {
1626   my $self = shift;
1627   my $plan = $self->plan;
1628   unless ( $plan ) {
1629     cluck "no price plan found for pkgpart ". $self->pkgpart. "\n"
1630       if $DEBUG;
1631     return $self;
1632   }
1633   return $self if ref($self) =~ /::$plan$/; #already blessed into plan subclass
1634   my $class = ref($self). "::$plan";
1635   warn "reblessing $self into $class" if $DEBUG > 1;
1636   eval "use $class;";
1637   die $@ if $@;
1638   bless($self, $class) unless $@;
1639   $self;
1640 }
1641
1642 #fatal fallbacks
1643 sub calc_setup { die 'no calc_setup for '. shift->plan. "\n"; }
1644 sub calc_recur { die 'no calc_recur for '. shift->plan. "\n"; }
1645
1646 #fallback that return 0 for old legacy packages with no plan
1647 sub calc_remain { 0; }
1648 sub calc_units  { 0; }
1649
1650 #fallback for everything not based on flat.pm
1651 sub recur_temporality { 'upcoming'; }
1652 sub calc_cancel { 0; }
1653
1654 #fallback for everything except bulk.pm
1655 sub hide_svc_detail { 0; }
1656
1657 #fallback for packages that can't/won't summarize usage
1658 sub sum_usage { 0; }
1659
1660 =item recur_cost_permonth CUST_PKG
1661
1662 recur_cost divided by freq (only supported for monthly and longer frequencies)
1663
1664 =cut
1665
1666 sub recur_cost_permonth {
1667   my($self, $cust_pkg) = @_;
1668   return 0 unless $self->freq =~ /^\d+$/ && $self->freq > 0;
1669   sprintf('%.2f', ($self->recur_cost || 0) / $self->freq );
1670 }
1671
1672 =item cust_bill_pkg_recur CUST_PKG
1673
1674 Actual recurring charge for the specified customer package from customer's most
1675 recent invoice
1676
1677 =cut
1678
1679 sub cust_bill_pkg_recur {
1680   my($self, $cust_pkg) = @_;
1681   my $cust_bill_pkg = qsearchs({
1682     'table'     => 'cust_bill_pkg',
1683     'addl_from' => 'LEFT JOIN cust_bill USING ( invnum )',
1684     'hashref'   => { 'pkgnum' => $cust_pkg->pkgnum,
1685                      'recur'  => { op=>'>', value=>'0' },
1686                    },
1687     'order_by'  => 'ORDER BY cust_bill._date     DESC,
1688                              cust_bill_pkg.sdate DESC
1689                      LIMIT 1
1690                    ',
1691   }) or return 0; #die "use cust_bill_pkg_recur credits with once_perinv condition";
1692   $cust_bill_pkg->recur;
1693 }
1694
1695 =item unit_setup CUST_PKG
1696
1697 Returns the setup fee for one unit of the package.
1698
1699 =cut
1700
1701 sub unit_setup {
1702   my ($self, $cust_pkg) = @_;
1703   $self->option('setup_fee') || 0;
1704 }
1705
1706 =item setup_margin
1707
1708 unit_setup minus setup_cost
1709
1710 =cut
1711
1712 sub setup_margin {
1713   my $self = shift;
1714   $self->unit_setup(@_) - ($self->setup_cost || 0);
1715 }
1716
1717 =item recur_margin_permonth
1718
1719 base_recur_permonth minus recur_cost_permonth
1720
1721 =cut
1722
1723 sub recur_margin_permonth {
1724   my $self = shift;
1725   $self->base_recur_permonth(@_) - $self->recur_cost_permonth(@_);
1726 }
1727
1728 =item intro_end PACKAGE
1729
1730 Takes an L<FS::cust_pkg> object.  If this plan has an introductory rate,
1731 returns the expected date the intro period will end. If there is no intro
1732 rate, returns zero.
1733
1734 =cut
1735
1736 sub intro_end {
1737   0;
1738 }
1739
1740 =item format OPTION DATA
1741
1742 Returns data formatted according to the function 'format' described
1743 in the plan info.  Returns DATA if no such function exists.
1744
1745 =cut
1746
1747 sub format {
1748   my ($self, $option, $data) = (shift, shift, shift);
1749   if (exists($plans{$self->plan}->{fields}->{$option}{format})) {
1750     &{$plans{$self->plan}->{fields}->{$option}{format}}($data);
1751   }else{
1752     $data;
1753   }
1754 }
1755
1756 =item parse OPTION DATA
1757
1758 Returns data parsed according to the function 'parse' described
1759 in the plan info.  Returns DATA if no such function exists.
1760
1761 =cut
1762
1763 sub parse {
1764   my ($self, $option, $data) = (shift, shift, shift);
1765   if (exists($plans{$self->plan}->{fields}->{$option}{parse})) {
1766     &{$plans{$self->plan}->{fields}->{$option}{parse}}($data);
1767   }else{
1768     $data;
1769   }
1770 }
1771
1772 =back
1773
1774 =cut
1775
1776 =head1 CLASS METHODS
1777
1778 =over 4
1779
1780 =cut
1781
1782 # _upgrade_data
1783 #
1784 # Used by FS::Upgrade to migrate to a new database.
1785
1786 sub _upgrade_data { # class method
1787   my($class, %opts) = @_;
1788
1789   warn "[FS::part_pkg] upgrading $class\n" if $DEBUG;
1790
1791   my @part_pkg = qsearch({
1792     'table'     => 'part_pkg',
1793     'extra_sql' => "WHERE ". join(' OR ',
1794                      'plan IS NULL', "plan = '' ",
1795                    ),
1796   });
1797
1798   foreach my $part_pkg (@part_pkg) {
1799
1800     unless ( $part_pkg->plan ) {
1801       $part_pkg->plan('flat');
1802     }
1803
1804     $part_pkg->replace;
1805
1806   }
1807   # the rest can be done asynchronously
1808 }
1809
1810 sub queueable_upgrade {
1811   # now upgrade to the explicit custom flag
1812
1813   my $search = FS::Cursor->new({
1814     'table'     => 'part_pkg',
1815     'hashref'   => { disabled => 'Y', custom => '' },
1816     'extra_sql' => "AND comment LIKE '(CUSTOM) %'",
1817   });
1818   my $dbh = dbh;
1819
1820   while (my $part_pkg = $search->fetch) {
1821     my $new = new FS::part_pkg { $part_pkg->hash };
1822     $new->custom('Y');
1823     my $comment = $part_pkg->comment;
1824     $comment =~ s/^\(CUSTOM\) //;
1825     $comment = '(none)' unless $comment =~ /\S/;
1826     $new->comment($comment);
1827
1828     my $pkg_svc = { map { $_->svcpart => $_->quantity } $part_pkg->pkg_svc };
1829     my $primary = $part_pkg->svcpart;
1830     my $options = { $part_pkg->options };
1831
1832     my $error = $new->replace( $part_pkg,
1833                                'pkg_svc'     => $pkg_svc,
1834                                'primary_svc' => $primary,
1835                                'options'     => $options,
1836                              );
1837     if ($error) {
1838       warn "pkgpart#".$part_pkg->pkgpart.": $error\n";
1839       $dbh->rollback;
1840     } else {
1841       $dbh->commit;
1842     }
1843   }
1844
1845   # set family_pkgpart on any packages that don't have it
1846   $search = FS::Cursor->new('part_pkg', { 'family_pkgpart' => '' });
1847   while (my $part_pkg = $search->fetch) {
1848     $part_pkg->set('family_pkgpart' => $part_pkg->pkgpart);
1849     my $error = $part_pkg->SUPER::replace;
1850     if ($error) {
1851       warn "pkgpart#".$part_pkg->pkgpart.": $error\n";
1852       $dbh->rollback;
1853     } else {
1854       $dbh->commit;
1855     }
1856   }
1857
1858   my @part_pkg_option = qsearch('part_pkg_option',
1859     { 'optionname'  => 'unused_credit',
1860       'optionvalue' => 1,
1861     });
1862   foreach my $old_opt (@part_pkg_option) {
1863     my $pkgpart = $old_opt->pkgpart;
1864     my $error = $old_opt->delete;
1865     die $error if $error;
1866
1867     foreach (qw(unused_credit_cancel unused_credit_change)) {
1868       my $new_opt = new FS::part_pkg_option {
1869         'pkgpart'     => $pkgpart,
1870         'optionname'  => $_,
1871         'optionvalue' => 1,
1872       };
1873       $error = $new_opt->insert;
1874       die $error if $error;
1875     }
1876   }
1877
1878   # migrate use_disposition_taqua and use_disposition to disposition_in
1879   @part_pkg_option = qsearch('part_pkg_option',
1880     { 'optionname'  => { op => 'LIKE',
1881                          value => 'use_disposition%',
1882                        },
1883       'optionvalue' => 1,
1884     });
1885   my %newopts = map { $_->pkgpart => $_ } 
1886     qsearch('part_pkg_option',  { 'optionname'  => 'disposition_in', } );
1887   foreach my $old_opt (@part_pkg_option) {
1888         my $pkgpart = $old_opt->pkgpart;
1889         my $newval = $old_opt->optionname eq 'use_disposition_taqua' ? '100' 
1890                                                                   : 'ANSWERED';
1891         my $error = $old_opt->delete;
1892         die $error if $error;
1893
1894         if ( exists($newopts{$pkgpart}) ) {
1895             my $opt = $newopts{$pkgpart};
1896             $opt->optionvalue($opt->optionvalue.",$newval");
1897             $error = $opt->replace;
1898             die $error if $error;
1899         } else {
1900             my $new_opt = new FS::part_pkg_option {
1901                 'pkgpart'     => $pkgpart,
1902                 'optionname'  => 'disposition_in',
1903                 'optionvalue' => $newval,
1904               };
1905               $error = $new_opt->insert;
1906               die $error if $error;
1907               $newopts{$pkgpart} = $new_opt;
1908         }
1909   }
1910
1911   # set any package with FCC voice lines to the "VoIP with broadband" category
1912   # for backward compatibility
1913   #
1914   # recover from a bad upgrade bug
1915   my $upgrade = 'part_pkg_fcc_voip_class_FIX';
1916   if (!FS::upgrade_journal->is_done($upgrade)) {
1917     my $bad_upgrade = qsearchs('upgrade_journal', 
1918       { upgrade => 'part_pkg_fcc_voip_class' }
1919     );
1920     if ( $bad_upgrade ) {
1921       my $where = 'WHERE history_date <= '.$bad_upgrade->_date.
1922                   ' AND  history_date >  '.($bad_upgrade->_date - 3600);
1923       my @h_part_pkg_option = map { FS::part_pkg_option->new($_->hashref) }
1924         qsearch({
1925           'select'    => '*',
1926           'table'     => 'h_part_pkg_option',
1927           'hashref'   => {},
1928           'extra_sql' => "$where AND history_action = 'delete'",
1929           'order_by'  => 'ORDER BY history_date ASC',
1930         });
1931       my @h_pkg_svc = map { FS::pkg_svc->new($_->hashref) }
1932         qsearch({
1933           'select'    => '*',
1934           'table'     => 'h_pkg_svc',
1935           'hashref'   => {},
1936           'extra_sql' => "$where AND history_action = 'replace_old'",
1937           'order_by'  => 'ORDER BY history_date ASC',
1938         });
1939       my %opt;
1940       foreach my $deleted (@h_part_pkg_option, @h_pkg_svc) {
1941         my $pkgpart ||= $deleted->pkgpart;
1942         $opt{$pkgpart} ||= {
1943           options => {},
1944           pkg_svc => {},
1945           primary_svc => '',
1946           hidden_svc => {},
1947         };
1948         if ( $deleted->isa('FS::part_pkg_option') ) {
1949           $opt{$pkgpart}{options}{ $deleted->optionname } = $deleted->optionvalue;
1950         } else { # pkg_svc
1951           my $svcpart = $deleted->svcpart;
1952           $opt{$pkgpart}{pkg_svc}{$svcpart} = $deleted->quantity;
1953           $opt{$pkgpart}{hidden_svc}{$svcpart} ||= $deleted->hidden;
1954           $opt{$pkgpart}{primary_svc} = $svcpart if $deleted->primary_svc;
1955         }
1956       }
1957       foreach my $pkgpart (keys %opt) {
1958         my $part_pkg = FS::part_pkg->by_key($pkgpart);
1959         my $error = $part_pkg->replace( $part_pkg->replace_old, $opt{$pkgpart} );
1960         if ( $error ) {
1961           die "error recovering damaged pkgpart $pkgpart:\n$error\n";
1962         }
1963       }
1964     } # $bad_upgrade exists
1965     else { # do the original upgrade, but correctly this time
1966       my @part_pkg = qsearch('part_pkg', {
1967           fcc_ds0s        => { op => '>', value => 0 },
1968           fcc_voip_class  => ''
1969       });
1970       foreach my $part_pkg (@part_pkg) {
1971         $part_pkg->set(fcc_voip_class => 2);
1972         my @pkg_svc = $part_pkg->pkg_svc;
1973         my %quantity = map {$_->svcpart, $_->quantity} @pkg_svc;
1974         my %hidden   = map {$_->svcpart, $_->hidden  } @pkg_svc;
1975         my $error = $part_pkg->replace(
1976           $part_pkg->replace_old,
1977           options     => { $part_pkg->options },
1978           pkg_svc     => \%quantity,
1979           hidden_svc  => \%hidden,
1980           primary_svc => ($part_pkg->svcpart || ''),
1981         );
1982         die $error if $error;
1983       }
1984     }
1985     FS::upgrade_journal->set_done($upgrade);
1986   }
1987
1988 }
1989
1990 =item curuser_pkgs_sql
1991
1992 Returns an SQL fragment for searching for packages the current user can
1993 use, either via part_pkg.agentnum directly, or via agent type (see
1994 L<FS::type_pkgs>).
1995
1996 =cut
1997
1998 sub curuser_pkgs_sql {
1999   my $class = shift;
2000
2001   $class->_pkgs_sql( $FS::CurrentUser::CurrentUser->agentnums );
2002
2003 }
2004
2005 =item agent_pkgs_sql AGENT | AGENTNUM, ...
2006
2007 Returns an SQL fragment for searching for packages the provided agent or agents
2008 can use, either via part_pkg.agentnum directly, or via agent type (see
2009 L<FS::type_pkgs>).
2010
2011 =cut
2012
2013 sub agent_pkgs_sql {
2014   my $class = shift;  #i'm a class method, not a sub (the question is... why??)
2015   my @agentnums = map { ref($_) ? $_->agentnum : $_ } @_;
2016
2017   $class->_pkgs_sql(@agentnums); #is this why
2018
2019 }
2020
2021 sub _pkgs_sql {
2022   my( $class, @agentnums ) = @_;
2023   my $agentnums = join(',', @agentnums);
2024
2025   "
2026     (
2027       ( agentnum IS NOT NULL AND agentnum IN ($agentnums) )
2028       OR ( agentnum IS NULL
2029            AND EXISTS ( SELECT 1
2030                           FROM type_pkgs
2031                             LEFT JOIN agent_type USING ( typenum )
2032                             LEFT JOIN agent AS typeagent USING ( typenum )
2033                           WHERE type_pkgs.pkgpart = part_pkg.pkgpart
2034                             AND typeagent.agentnum IN ($agentnums)
2035                       )
2036          )
2037     )
2038   ";
2039
2040 }
2041
2042 =back
2043
2044 =head1 SUBROUTINES
2045
2046 =over 4
2047
2048 =item plan_info
2049
2050 =cut
2051
2052 #false laziness w/part_export & cdr
2053 my %info;
2054 foreach my $INC ( @INC ) {
2055   warn "globbing $INC/FS/part_pkg/[a-z]*.pm\n" if $DEBUG;
2056   foreach my $file ( glob("$INC/FS/part_pkg/[a-z]*.pm") ) {
2057     warn "attempting to load plan info from $file\n" if $DEBUG;
2058     $file =~ /\/(\w+)\.pm$/ or do {
2059       warn "unrecognized file in $INC/FS/part_pkg/: $file\n";
2060       next;
2061     };
2062     my $mod = $1;
2063     my $info = eval "use FS::part_pkg::$mod; ".
2064                     "\\%FS::part_pkg::$mod\::info;";
2065     if ( $@ ) {
2066       die "error using FS::part_pkg::$mod (skipping): $@\n" if $@;
2067       next;
2068     }
2069     unless ( keys %$info ) {
2070       warn "no %info hash found in FS::part_pkg::$mod, skipping\n";
2071       next;
2072     }
2073     warn "got plan info from FS::part_pkg::$mod: $info\n" if $DEBUG;
2074     #if ( exists($info->{'disabled'}) && $info->{'disabled'} ) {
2075     #  warn "skipping disabled plan FS::part_pkg::$mod" if $DEBUG;
2076     #  next;
2077     #}
2078     $info{$mod} = $info;
2079     $info->{'weight'} ||= 0; # quiet warnings
2080   }
2081 }
2082
2083 # copy one level deep to allow replacement of fields and fieldorder
2084 tie %plans, 'Tie::IxHash',
2085   map  { my %infohash = %{ $info{$_} }; 
2086           $_ => \%infohash }
2087   sort { $info{$a}->{'weight'} <=> $info{$b}->{'weight'} }
2088   keys %info;
2089
2090 # inheritance of plan options
2091 foreach my $name (keys(%info)) {
2092   if (exists($info{$name}->{'disabled'}) and $info{$name}->{'disabled'}) {
2093     warn "skipping disabled plan FS::part_pkg::$name" if $DEBUG;
2094     delete $plans{$name};
2095     next;
2096   }
2097   my $parents = $info{$name}->{'inherit_fields'} || [];
2098   my (%fields, %field_exists, @fieldorder);
2099   foreach my $parent ($name, @$parents) {
2100     if ( !exists($info{$parent}) ) {
2101       warn "$name tried to inherit from nonexistent '$parent'\n";
2102       next;
2103     }
2104     %fields = ( # avoid replacing existing fields
2105       %{ $info{$parent}->{'fields'} || {} },
2106       %fields
2107     );
2108     foreach (@{ $info{$parent}->{'fieldorder'} || [] }) {
2109       # avoid duplicates
2110       next if $field_exists{$_};
2111       $field_exists{$_} = 1;
2112       # allow inheritors to remove inherited fields from the fieldorder
2113       push @fieldorder, $_ if !exists($fields{$_}) or
2114                               !exists($fields{$_}->{'disabled'});
2115     }
2116   }
2117   $plans{$name}->{'fields'} = \%fields;
2118   $plans{$name}->{'fieldorder'} = \@fieldorder;
2119 }
2120
2121 sub plan_info {
2122   \%plans;
2123 }
2124
2125
2126 =back
2127
2128 =head1 NEW PLAN CLASSES
2129
2130 A module should be added in FS/FS/part_pkg/  Eventually, an example may be
2131 found in eg/plan_template.pm.  Until then, it is suggested that you use the
2132 other modules in FS/FS/part_pkg/ as a guide.
2133
2134 =head1 BUGS
2135
2136 The delete method is unimplemented.
2137
2138 setup and recur semantics are not yet defined (and are implemented in
2139 FS::cust_bill.  hmm.).  now they're deprecated and need to go.
2140
2141 plandata should go
2142
2143 part_pkg_taxrate is Pg specific
2144
2145 replace should be smarter about managing the related tables (options, pkg_svc)
2146
2147 =head1 SEE ALSO
2148
2149 L<FS::Record>, L<FS::cust_pkg>, L<FS::type_pkgs>, L<FS::pkg_svc>, L<Safe>.
2150 schema.html from the base documentation.
2151
2152 =cut
2153
2154 1;
2155