RT# 31208 Docs $FS::Record::qsearch_qualify_columns
[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
359 appropriate FS::pkg_svc record will be updated.
360
361 If I<options> is set to a hashref, the appropriate FS::part_pkg_option
362 records 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 Pass an I<$options> hashref that contains the values to be
675 inserted or updated for any FS::part_pkg::MODULE.pm.
676
677 For each key in I<$options>, validates the value by calling
678 the 'validate' subroutine defined for that option e.g.
679 FS::part_pkg::MODULE::plan_info()->{$KEY}->{validate}.  The
680 option validation function is only called when the hashkey for
681 that option exists in I<$options>.
682
683 Then the module validation function is called, from
684 FS::part_pkg::MODULE::plan_info()->{validate}
685
686 Returns error message, or empty string if valid.
687
688 Invoked by L</insert> and L</replace> via the equivalent
689 methods in L<FS::option_Common>.
690
691 =cut
692
693 sub check_options {
694   my ($self,$options) = @_;
695   foreach my $option (keys %$options) {
696     if (exists $plans{ $self->plan }->{fields}->{$option}) {
697       if (exists($plans{$self->plan}->{fields}->{$option}->{'validate'})) {
698         # pass option name for use in error message
699         # pass a reference to the $options value, so it can be cleaned up
700         my $error = &{$plans{$self->plan}->{fields}->{$option}->{'validate'}}($option,\($options->{$option}));
701         return $error if $error;
702       }
703     } # else "option does not exist" error?
704   }
705   if (exists($plans{$self->plan}->{'validate'})) {
706     my $error = &{$plans{$self->plan}->{'validate'}}($options);
707     return $error if $error;
708   }
709   return '';
710 }
711
712 =item supersede OLD [, OPTION => VALUE ... ]
713
714 Inserts this package as a successor to the package OLD.  All options are as
715 for C<insert>.  After inserting, disables OLD and sets the new package as its
716 successor.
717
718 =cut
719
720 sub supersede {
721   my ($new, $old, %options) = @_;
722   my $error;
723
724   $new->set('pkgpart' => '');
725   $new->set('family_pkgpart' => $old->family_pkgpart);
726   warn "    inserting successor package\n" if $DEBUG;
727   $error = $new->insert(%options);
728   return $error if $error;
729  
730   warn "    disabling superseded package\n" if $DEBUG; 
731   $old->set('successor' => $new->pkgpart);
732   $old->set('disabled' => 'Y');
733   $error = $old->SUPER::replace; # don't change its options/pkg_svc records
734   return $error if $error;
735
736   warn "  propagating changes to family" if $DEBUG;
737   $new->propagate($old);
738 }
739
740 =item propagate OLD
741
742 If any of certain fields have changed from OLD to this package, then,
743 for all packages in the same lineage as this one, sets those fields 
744 to their values in this package.
745
746 =cut
747
748 my @propagate_fields = (
749   qw( pkg classnum setup_cost recur_cost taxclass
750   setuptax recurtax pay_weight credit_weight
751   )
752 );
753
754 sub propagate {
755   my $new = shift;
756   my $old = shift;
757   my %fields = (
758     map { $_ => $new->get($_) }
759     grep { $new->get($_) ne $old->get($_) }
760     @propagate_fields
761   );
762
763   my @part_pkg = qsearch('part_pkg', { 
764       'family_pkgpart' => $new->family_pkgpart 
765   });
766   my @error;
767   foreach my $part_pkg ( @part_pkg ) {
768     my $pkgpart = $part_pkg->pkgpart;
769     next if $pkgpart == $new->pkgpart; # don't modify $new
770     warn "    propagating to pkgpart $pkgpart\n" if $DEBUG;
771     foreach ( keys %fields ) {
772       $part_pkg->set($_, $fields{$_});
773     }
774     # SUPER::replace to avoid changing non-core fields
775     my $error = $part_pkg->SUPER::replace;
776     push @error, "pkgpart $pkgpart: $error"
777       if $error;
778   }
779   join("\n", @error);
780 }
781
782 =item set_fcc_options HASHREF
783
784 Sets the FCC options on this package definition to the values specified
785 in HASHREF.
786
787 =cut
788
789 sub set_fcc_options {
790   my $self = shift;
791   my $pkgpart = $self->pkgpart;
792   my $options;
793   if (ref $_[0]) {
794     $options = shift;
795   } else {
796     $options = { @_ };
797   }
798
799   my %existing_num = map { $_->fccoptionname => $_->num }
800                      qsearch('part_pkg_fcc_option', { pkgpart => $pkgpart });
801
802   local $FS::Record::nowarn_identical = 1;
803   # set up params for process_o2m
804   my $i = 0;
805   my $params = {};
806   foreach my $name (keys %$options ) {
807     $params->{ "num$i" } = $existing_num{$name} || '';
808     $params->{ "num$i".'_fccoptionname' } = $name;
809     $params->{ "num$i".'_optionvalue'   } = $options->{$name};
810     $i++;
811   }
812
813   $self->process_o2m(
814     table   => 'part_pkg_fcc_option',
815     fields  => [qw( fccoptionname optionvalue )],
816     params  => $params,
817   );
818 }
819
820 =item pkg_locale LOCALE
821
822 Returns a customer-viewable string representing this package for the given
823 locale, from the part_pkg_msgcat table.  If the given locale is empty or no
824 localized string is found, returns the base pkg field.
825
826 =cut
827
828 sub pkg_locale {
829   my( $self, $locale ) = @_;
830   return $self->pkg unless $locale;
831   my $part_pkg_msgcat = $self->part_pkg_msgcat($locale) or return $self->pkg;
832   $part_pkg_msgcat->pkg;
833 }
834
835 =item part_pkg_msgcat LOCALE
836
837 Like pkg_locale, but returns the FS::part_pkg_msgcat object itself.
838
839 =cut
840
841 sub part_pkg_msgcat {
842   my( $self, $locale ) = @_;
843   qsearchs( 'part_pkg_msgcat', {
844     pkgpart => $self->pkgpart,
845     locale  => $locale,
846   });
847 }
848
849 =item pkg_comment [ OPTION => VALUE... ]
850
851 Returns an (internal) string representing this package.  Currently,
852 "pkgpart: pkg - comment", is returned.  "pkg - comment" may be returned in the
853 future, omitting pkgpart.  The comment will have '(CUSTOM) ' prepended if
854 custom is Y.
855
856 If the option nopkgpart is true then the "pkgpart: ' is omitted.
857
858 =cut
859
860 sub pkg_comment {
861   my $self = shift;
862   my %opt = @_;
863
864   #$self->pkg. ' - '. $self->comment;
865   #$self->pkg. ' ('. $self->comment. ')';
866   my $pre = $opt{nopkgpart} ? '' : $self->pkgpart. ': ';
867   my $custom_comment = $self->custom_comment(%opt);
868   $pre. $self->pkg. ( $custom_comment ? " - $custom_comment" : '' );
869 }
870
871 #without price info (so without hitting the DB again)
872 sub pkg_comment_only {
873   my $self = shift;
874   my %opt = @_;
875
876   my $pre = $opt{nopkgpart} ? '' : $self->pkgpart. ': ';
877   my $comment = $self->comment;
878   $pre. $self->pkg. ( $comment ? " - $comment" : '' );
879 }
880
881 sub price_info { # safety, in case a part_pkg hasn't defined price_info
882     '';
883 }
884
885 sub custom_comment {
886   my $self = shift;
887   my $price_info = $self->price_info(@_);
888   ( $self->custom ? '(CUSTOM) ' : '' ).
889     $self->comment.
890     ( ( ($self->custom || $self->comment) && $price_info ) ? ' - ' : '' ).
891     $price_info;
892 }
893
894 sub pkg_price_info {
895   my $self = shift;
896   $self->pkg. ' - '. ($self->price_info || 'No charge');
897 }
898
899 =item pkg_class
900
901 Returns the package class, as an FS::pkg_class object, or the empty string
902 if there is no package class.
903
904 =cut
905
906 sub pkg_class {
907   my $self = shift;
908   if ( $self->classnum ) {
909     qsearchs('pkg_class', { 'classnum' => $self->classnum } );
910   } else {
911     return '';
912   }
913 }
914
915 =item addon_pkg_class
916
917 Returns the add-on package class, as an FS::pkg_class object, or the empty
918 string if there is no add-on package class.
919
920 =cut
921
922 sub addon_pkg_class {
923   my $self = shift;
924   if ( $self->addon_classnum ) {
925     qsearchs('pkg_class', { 'classnum' => $self->addon_classnum } );
926   } else {
927     return '';
928   }
929 }
930
931 =item categoryname 
932
933 Returns the package category name, or the empty string if there is no package
934 category.
935
936 =cut
937
938 sub categoryname {
939   my $self = shift;
940   my $pkg_class = $self->pkg_class;
941   $pkg_class
942     ? $pkg_class->categoryname
943     : '';
944 }
945
946 =item classname 
947
948 Returns the package class name, or the empty string if there is no package
949 class.
950
951 =cut
952
953 sub classname {
954   my $self = shift;
955   my $pkg_class = $self->pkg_class;
956   $pkg_class
957     ? $pkg_class->classname
958     : '';
959 }
960
961 =item addon_classname 
962
963 Returns the add-on package class name, or the empty string if there is no
964 add-on package class.
965
966 =cut
967
968 sub addon_classname {
969   my $self = shift;
970   my $pkg_class = $self->addon_pkg_class;
971   $pkg_class
972     ? $pkg_class->classname
973     : '';
974 }
975
976 =item agent 
977
978 Returns the associated agent for this event, if any, as an FS::agent object.
979
980 =cut
981
982 sub agent {
983   my $self = shift;
984   qsearchs('agent', { 'agentnum' => $self->agentnum } );
985 }
986
987 =item pkg_svc [ HASHREF | OPTION => VALUE ]
988
989 Returns all FS::pkg_svc objects (see L<FS::pkg_svc>) for this package
990 definition (with non-zero quantity).
991
992 One option is available, I<disable_linked>.  If set true it will return the
993 services for this package definition alone, omitting services from any add-on
994 packages.
995
996 =cut
997
998 =item type_pkgs
999
1000 Returns all FS::type_pkgs objects (see L<FS::type_pkgs>) for this package
1001 definition.
1002
1003 =cut
1004
1005 sub type_pkgs {
1006   my $self = shift;
1007   qsearch('type_pkgs', { 'pkgpart' => $self->pkgpart } );
1008 }
1009
1010 sub pkg_svc {
1011   my $self = shift;
1012
1013   return @{ $cache_pkg_svc{$self->pkgpart} }
1014     if $cache_enabled && $cache_pkg_svc{$self->pkgpart};
1015
1016 #  #sort { $b->primary cmp $a->primary } 
1017 #    grep { $_->quantity }
1018 #      qsearch( 'pkg_svc', { 'pkgpart' => $self->pkgpart } );
1019
1020   my $opt = ref($_[0]) ? $_[0] : { @_ };
1021   my %pkg_svc = map  { $_->svcpart => $_ } $self->_pkg_svc;
1022
1023   unless ( $opt->{disable_linked} ) {
1024     foreach my $dst_pkg ( map $_->dst_pkg, $self->svc_part_pkg_link ) {
1025       my @pkg_svc = $dst_pkg->_pkg_svc;
1026       foreach my $pkg_svc ( @pkg_svc ) {
1027         if ( $pkg_svc{$pkg_svc->svcpart} ) {
1028           my $quantity = $pkg_svc{$pkg_svc->svcpart}->quantity;
1029           $pkg_svc{$pkg_svc->svcpart}->quantity($quantity + $pkg_svc->quantity);
1030         } else {
1031           $pkg_svc{$pkg_svc->svcpart} = $pkg_svc;
1032         }
1033       }
1034     }
1035   }
1036
1037   my @pkg_svc = values(%pkg_svc);
1038
1039   $cache_pkg_svc{$self->pkgpart} = \@pkg_svc if $cache_enabled;
1040
1041   @pkg_svc;
1042
1043 }
1044
1045 sub _pkg_svc {
1046   my $self = shift;
1047   grep { $_->quantity }
1048     qsearch({
1049       'select'    => 'pkg_svc.*, part_svc.*',
1050       'table'     => 'pkg_svc',
1051       'addl_from' => 'LEFT JOIN part_svc USING ( svcpart )',
1052       'hashref'   => { 'pkgpart' => $self->pkgpart },
1053     });
1054 }
1055
1056 =item svcpart [ SVCDB ]
1057
1058 Returns the svcpart of the primary service definition (see L<FS::part_svc>)
1059 associated with this package definition (see L<FS::pkg_svc>).  Returns
1060 false if there not a primary service definition or exactly one service
1061 definition with quantity 1, or if SVCDB is specified and does not match the
1062 svcdb of the service definition.  SVCDB can be specified as a scalar table
1063 name, such as 'svc_acct', or as an arrayref of possible table names.
1064
1065 =cut
1066
1067 sub svcpart {
1068   my $pkg_svc = shift->_primary_pkg_svc(@_);
1069   $pkg_svc ? $pkg_svc->svcpart : '';
1070 }
1071
1072 =item part_svc [ SVCDB ]
1073
1074 Like the B<svcpart> method, but returns the FS::part_svc object (see
1075 L<FS::part_svc>).
1076
1077 =cut
1078
1079 sub part_svc {
1080   my $pkg_svc = shift->_primary_pkg_svc(@_);
1081   $pkg_svc ? $pkg_svc->part_svc : '';
1082 }
1083
1084 sub _primary_pkg_svc {
1085   my $self = shift;
1086
1087   my $svcdb = scalar(@_) ? shift : [];
1088   $svcdb = ref($svcdb) ? $svcdb : [ $svcdb ];
1089   my %svcdb = map { $_=>1 } @$svcdb;
1090
1091   my @svcdb_pkg_svc =
1092     grep { !scalar(@$svcdb) || $svcdb{ $_->part_svc->svcdb } }
1093          $self->pkg_svc;
1094
1095   my @pkg_svc = grep { $_->primary_svc =~ /^Y/i } @svcdb_pkg_svc;
1096   @pkg_svc = grep {$_->quantity == 1 } @svcdb_pkg_svc
1097     unless @pkg_svc;
1098   return '' if scalar(@pkg_svc) != 1;
1099   $pkg_svc[0];
1100 }
1101
1102 =item svcpart_unique_svcdb SVCDB
1103
1104 Returns the svcpart of a service definition (see L<FS::part_svc>) matching
1105 SVCDB associated with this package definition (see L<FS::pkg_svc>).  Returns
1106 false if there not a primary service definition for SVCDB or there are multiple
1107 service definitions for SVCDB.
1108
1109 =cut
1110
1111 sub svcpart_unique_svcdb {
1112   my( $self, $svcdb ) = @_;
1113   my @svcdb_pkg_svc = grep { ( $svcdb eq $_->part_svc->svcdb ) } $self->pkg_svc;
1114   return '' if scalar(@svcdb_pkg_svc) != 1;
1115   $svcdb_pkg_svc[0]->svcpart;
1116 }
1117
1118 =item payby
1119
1120 Returns a list of the acceptable payment types for this package.  Eventually
1121 this should come out of a database table and be editable, but currently has the
1122 following logic instead:
1123
1124 If the package is free, the single item B<BILL> is
1125 returned, otherwise, the single item B<CARD> is returned.
1126
1127 (CHEK?  LEC?  Probably shouldn't accept those by default, prone to abuse)
1128
1129 =cut
1130
1131 sub payby {
1132   my $self = shift;
1133   if ( $self->is_free ) {
1134     ( 'BILL' );
1135   } else {
1136     ( 'CARD' );
1137   }
1138 }
1139
1140 =item is_free
1141
1142 Returns true if this package is free.  
1143
1144 =cut
1145
1146 sub is_free {
1147   my $self = shift;
1148   if ( $self->can('is_free_options') ) {
1149     not grep { $_ !~ /^\s*0*(\.0*)?\s*$/ }
1150          map { $self->option($_) } 
1151              $self->is_free_options;
1152   } else {
1153     warn "FS::part_pkg::is_free: FS::part_pkg::". $self->plan. " subclass ".
1154          "provides neither is_free_options nor is_free method; returning false";
1155     0;
1156   }
1157 }
1158
1159 # whether the plan allows discounts to be applied to this package
1160 sub can_discount { 0; }
1161
1162 # whether the plan allows changing the start date
1163 sub can_start_date {
1164   my $self = shift;
1165   $self->start_on_hold ? 0 : 1;
1166 }
1167
1168 # the delay start date if present
1169 sub delay_start_date {
1170   my $self = shift;
1171
1172   my $delay = $self->delay_start or return '';
1173
1174   # avoid timelocal silliness  
1175   my $dt = DateTime->today(time_zone => 'local');
1176   $dt->add(days => $delay);
1177   $dt->epoch;
1178 }
1179
1180 sub freqs_href {
1181   # moved to FS::Misc to make this accessible to other packages
1182   # at initialization
1183   FS::Misc::pkg_freqs();
1184 }
1185
1186 =item freq_pretty
1187
1188 Returns an english representation of the I<freq> field, such as "monthly",
1189 "weekly", "semi-annually", etc.
1190
1191 =cut
1192
1193 sub freq_pretty {
1194   my $self = shift;
1195   my $freq = $self->freq;
1196
1197   #my $freqs_href = $self->freqs_href;
1198   my $freqs_href = freqs_href();
1199
1200   if ( exists($freqs_href->{$freq}) ) {
1201     $freqs_href->{$freq};
1202   } else {
1203     my $interval = 'month';
1204     if ( $freq =~ /^(\d+)([hdw])$/ ) {
1205       my %interval = ( 'h' => 'hour', 'd'=>'day', 'w'=>'week' );
1206       $interval = $interval{$2};
1207     }
1208     if ( $1 == 1 ) {
1209       "every $interval";
1210     } else {
1211       "every $freq ${interval}s";
1212     }
1213   }
1214 }
1215
1216 =item add_freq TIMESTAMP [ FREQ ]
1217
1218 Adds a billing period of some frequency to the provided timestamp and 
1219 returns the resulting timestamp, or -1 if the frequency could not be 
1220 parsed (shouldn't happen).  By default, the frequency of this package 
1221 will be used; to override this, pass a different frequency as a second 
1222 argument.
1223
1224 =cut
1225
1226 sub add_freq {
1227   my( $self, $date, $freq ) = @_;
1228   $freq = $self->freq unless $freq;
1229
1230   #change this bit to use Date::Manip? CAREFUL with timezones (see
1231   # mailing list archive)
1232   my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($date) )[0,1,2,3,4,5];
1233
1234   if ( $freq =~ /^\d+$/ ) {
1235     $mon += $freq;
1236     until ( $mon < 12 ) { $mon -= 12; $year++; }
1237
1238     $mday = 28 if $mday > 28 && FS::Conf->new->exists('anniversary-rollback');
1239
1240   } elsif ( $freq =~ /^(\d+)w$/ ) {
1241     my $weeks = $1;
1242     $mday += $weeks * 7;
1243   } elsif ( $freq =~ /^(\d+)d$/ ) {
1244     my $days = $1;
1245     $mday += $days;
1246   } elsif ( $freq =~ /^(\d+)h$/ ) {
1247     my $hours = $1;
1248     $hour += $hours;
1249   } else {
1250     return -1;
1251   }
1252
1253   timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year);
1254 }
1255
1256 =item plandata
1257
1258 For backwards compatibility, returns the plandata field as well as all options
1259 from FS::part_pkg_option.
1260
1261 =cut
1262
1263 sub plandata {
1264   my $self = shift;
1265   carp "plandata is deprecated";
1266   if ( @_ ) {
1267     $self->SUPER::plandata(@_);
1268   } else {
1269     my $plandata = $self->get('plandata');
1270     my %options = $self->options;
1271     $plandata .= join('', map { "$_=$options{$_}\n" } keys %options );
1272     $plandata;
1273   }
1274 }
1275
1276 =item part_pkg_vendor
1277
1278 Returns all vendor/external package ids as FS::part_pkg_vendor objects (see
1279 L<FS::part_pkg_vendor>).
1280
1281 =cut
1282
1283 sub part_pkg_vendor {
1284   my $self = shift;
1285   qsearch('part_pkg_vendor', { 'pkgpart' => $self->pkgpart } );
1286 }
1287
1288 =item vendor_pkg_ids
1289
1290 Returns a list of vendor/external package ids by exportnum
1291
1292 =cut
1293
1294 sub vendor_pkg_ids {
1295   my $self = shift;
1296   map { $_->exportnum => $_->vendor_pkg_id } $self->part_pkg_vendor;
1297 }
1298
1299 =item part_pkg_option
1300
1301 Returns all options as FS::part_pkg_option objects (see
1302 L<FS::part_pkg_option>).
1303
1304 =cut
1305
1306 sub part_pkg_option {
1307   my $self = shift;
1308   qsearch('part_pkg_option', { 'pkgpart' => $self->pkgpart } );
1309 }
1310
1311 =item options 
1312
1313 Returns a list of option names and values suitable for assigning to a hash.
1314
1315 =cut
1316
1317 sub options {
1318   my $self = shift;
1319   map { $_->optionname => $_->optionvalue } $self->part_pkg_option;
1320 }
1321
1322 =item option OPTIONNAME [ QUIET ]
1323
1324 Returns the option value for the given name, or the empty string.  If a true
1325 value is passed as the second argument, warnings about missing the option
1326 will be suppressed.
1327
1328 =cut
1329
1330 sub option {
1331   my( $self, $opt, $ornull ) = @_;
1332
1333   #cache: was pulled up in the original part_pkg query
1334   return $self->hashref->{"_opt_$opt"}
1335     if exists $self->hashref->{"_opt_$opt"};
1336
1337   cluck "$self -> option: searching for $opt" if $DEBUG;
1338   my $part_pkg_option =
1339     qsearchs('part_pkg_option', {
1340       pkgpart    => $self->pkgpart,
1341       optionname => $opt,
1342   } );
1343   return $part_pkg_option->optionvalue if $part_pkg_option;
1344
1345   my %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); }
1346                      split("\n", $self->get('plandata') );
1347   return $plandata{$opt} if exists $plandata{$opt};
1348   cluck "WARNING: (pkgpart ". $self->pkgpart. ") Package def option $opt ".
1349         "not found in options or plandata!\n"
1350     unless $ornull;
1351
1352   '';
1353 }
1354
1355 =item fcc_option OPTIONNAME
1356
1357 Returns the FCC 477 report option value for the given name, or the empty 
1358 string.
1359
1360 =cut
1361
1362 sub fcc_option {
1363   my ($self, $name) = @_;
1364   my $part_pkg_fcc_option =
1365     qsearchs('part_pkg_fcc_option', {
1366         pkgpart => $self->pkgpart,
1367         fccoptionname => $name,
1368     });
1369   $part_pkg_fcc_option ? $part_pkg_fcc_option->optionvalue : '';
1370 }
1371
1372 =item fcc_options
1373
1374 Returns all FCC 477 report options for this package, as a hash-like list.
1375
1376 =cut
1377
1378 sub fcc_options {
1379   my $self = shift;
1380   map { $_->fccoptionname => $_->optionvalue }
1381     qsearch('part_pkg_fcc_option', { pkgpart => $self->pkgpart });
1382 }
1383
1384 =item bill_part_pkg_link
1385
1386 Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
1387
1388 =cut
1389
1390 sub bill_part_pkg_link {
1391   shift->_part_pkg_link('bill', @_);
1392 }
1393
1394 =item svc_part_pkg_link
1395
1396 Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
1397
1398 =cut
1399
1400 sub svc_part_pkg_link {
1401   shift->_part_pkg_link('svc', @_);
1402 }
1403
1404 =item supp_part_pkg_link
1405
1406 Returns the associated part_pkg_link records of type 'supp' (supplemental
1407 packages).
1408
1409 =cut
1410
1411 sub supp_part_pkg_link {
1412   shift->_part_pkg_link('supp', @_);
1413 }
1414
1415 sub _part_pkg_link {
1416   my( $self, $type ) = @_;
1417
1418   return @{ $cache_link{$type}->{$self->pkgpart} }
1419     if $cache_enabled && $cache_link{$type}->{$self->pkgpart};
1420
1421   cluck $type.'_part_pkg_link called' if $DEBUG;
1422
1423   my @ppl = 
1424     qsearch({ table    => 'part_pkg_link',
1425               hashref  => { src_pkgpart => $self->pkgpart,
1426                             link_type   => $type,
1427                             #protection against infinite recursive links
1428                             dst_pkgpart => { op=>'!=', value=> $self->pkgpart },
1429                           },
1430               order_by => "ORDER BY hidden",
1431            });
1432
1433   $cache_link{$type}->{$self->pkgpart} = \@ppl if $cache_enabled;
1434
1435   return @ppl;
1436 }
1437
1438 sub self_and_bill_linked {
1439   shift->_self_and_linked('bill', @_);
1440 }
1441
1442 sub self_and_svc_linked {
1443   shift->_self_and_linked('svc', @_);
1444 }
1445
1446 sub _self_and_linked {
1447   my( $self, $type, $hidden ) = @_;
1448   $hidden ||= '';
1449
1450   my @result = ();
1451   foreach ( ( $self, map { $_->dst_pkg->_self_and_linked($type, $_->hidden) }
1452                      $self->_part_pkg_link($type) ) )
1453   {
1454     $_->hidden($hidden) if $hidden;
1455     push @result, $_;
1456   }
1457
1458   (@result);
1459 }
1460
1461 =item part_pkg_taxoverride [ CLASS ]
1462
1463 Returns all associated FS::part_pkg_taxoverride objects (see
1464 L<FS::part_pkg_taxoverride>).  Limits the returned set to those
1465 of class CLASS if defined.  Class may be one of 'setup', 'recur',
1466 the empty string (default), or a usage class number (see L<FS::usage_class>).
1467 When a class is specified, the empty string class (default) is returned
1468 if no more specific values exist.
1469
1470 =cut
1471
1472 sub part_pkg_taxoverride {
1473   my $self = shift;
1474   my $class = shift;
1475
1476   my $hashref = { 'pkgpart' => $self->pkgpart };
1477   $hashref->{'usage_class'} = $class if defined($class);
1478   my @overrides = qsearch('part_pkg_taxoverride', $hashref );
1479
1480   unless ( scalar(@overrides) || !defined($class) || !$class ){
1481     $hashref->{'usage_class'} = '';
1482     @overrides = qsearch('part_pkg_taxoverride', $hashref );
1483   }
1484
1485   @overrides;
1486 }
1487
1488 =item has_taxproduct
1489
1490 Returns true if this package has any taxproduct associated with it.  
1491
1492 =cut
1493
1494 sub has_taxproduct {
1495   my $self = shift;
1496
1497   $self->taxproductnum ||
1498   scalar( grep { $_ =~/^usage_taxproductnum_/ && $self->option($_) } 
1499           keys %{ {$self->options} }
1500   )
1501
1502 }
1503
1504
1505 =item taxproduct [ CLASS ]
1506
1507 Returns the associated tax product for this package definition (see
1508 L<FS::part_pkg_taxproduct>).  CLASS may be one of 'setup', 'recur' or
1509 the usage classnum (see L<FS::usage_class>).  Returns the default
1510 tax product for this record if the more specific CLASS value does
1511 not exist.
1512
1513 =cut
1514
1515 sub taxproduct {
1516   my $self = shift;
1517   my $class = shift;
1518
1519   my $part_pkg_taxproduct;
1520
1521   my $taxproductnum = $self->taxproductnum;
1522   if ($class) { 
1523     my $class_taxproductnum = $self->option("usage_taxproductnum_$class", 1);
1524     $taxproductnum = $class_taxproductnum
1525       if $class_taxproductnum
1526   }
1527   
1528   $part_pkg_taxproduct =
1529     qsearchs( 'part_pkg_taxproduct', { 'taxproductnum' => $taxproductnum } );
1530
1531   unless ($part_pkg_taxproduct || $taxproductnum eq $self->taxproductnum ) {
1532     $taxproductnum = $self->taxproductnum;
1533     $part_pkg_taxproduct =
1534       qsearchs( 'part_pkg_taxproduct', { 'taxproductnum' => $taxproductnum } );
1535   }
1536
1537   $part_pkg_taxproduct;
1538 }
1539
1540 =item taxproduct_description [ CLASS ]
1541
1542 Returns the description of the associated tax product for this package
1543 definition (see L<FS::part_pkg_taxproduct>).
1544
1545 =cut
1546
1547 sub taxproduct_description {
1548   my $self = shift;
1549   my $part_pkg_taxproduct = $self->taxproduct(@_);
1550   $part_pkg_taxproduct ? $part_pkg_taxproduct->description : '';
1551 }
1552
1553
1554 =item tax_rates DATA_PROVIDER, GEOCODE, [ CLASS ]
1555
1556 Returns the tax table entries (L<FS::tax_rate> objects) that apply to this
1557 package in the location specified by GEOCODE, for usage class CLASS (one of
1558 'setup', 'recur', null, or a C<usage_class> number).
1559
1560 =cut
1561
1562 sub tax_rates {
1563   my $self = shift;
1564   my ($vendor, $geocode, $class) = @_;
1565   # if this part_pkg is overridden into a specific taxclass, get that class
1566   my @taxclassnums = map { $_->taxclassnum } 
1567                      $self->part_pkg_taxoverride($class);
1568   # otherwise, get its tax product category
1569   if (!@taxclassnums) {
1570     my $part_pkg_taxproduct = $self->taxproduct($class);
1571     # If this isn't defined, then the class has no taxproduct designation,
1572     # so return no tax rates.
1573     return () if !$part_pkg_taxproduct;
1574
1575     # convert the taxproduct to the tax classes that might apply to it in 
1576     # $geocode
1577     @taxclassnums = map { $_->taxclassnum }
1578                     grep { $_->taxable eq 'Y' } # why do we need this?
1579                     $part_pkg_taxproduct->part_pkg_taxrate($geocode);
1580   }
1581   return unless @taxclassnums;
1582
1583   # then look up the actual tax_rate entries
1584   warn "Found taxclassnum values of ". join(',', @taxclassnums) ."\n"
1585       if $DEBUG;
1586   my $extra_sql = "AND taxclassnum IN (". join(',', @taxclassnums) . ")";
1587   my @taxes = qsearch({ 'table'     => 'tax_rate',
1588                         'hashref'   => { 'geocode'     => $geocode,
1589                                          'data_vendor' => $vendor,
1590                                          'disabled'    => '' },
1591                         'extra_sql' => $extra_sql,
1592                       });
1593   warn "Found taxes ". join(',', map {$_->taxnum} @taxes) ."\n"
1594       if $DEBUG;
1595
1596   return @taxes;
1597 }
1598
1599 =item part_pkg_discount
1600
1601 Returns the package to discount m2m records (see L<FS::part_pkg_discount>)
1602 for this package.
1603
1604 =cut
1605
1606 sub part_pkg_discount {
1607   my $self = shift;
1608   qsearch('part_pkg_discount', { 'pkgpart' => $self->pkgpart });
1609 }
1610
1611 =item part_pkg_usage
1612
1613 Returns the voice usage pools (see L<FS::part_pkg_usage>) defined for 
1614 this package.
1615
1616 =cut
1617
1618 sub part_pkg_usage {
1619   my $self = shift;
1620   qsearch('part_pkg_usage', { 'pkgpart' => $self->pkgpart });
1621 }
1622
1623 =item _rebless
1624
1625 Reblesses the object into the FS::part_pkg::PLAN class (if available), where
1626 PLAN is the object's I<plan> field.  There should be better docs
1627 on how to create new price plans, but until then, see L</NEW PLAN CLASSES>.
1628
1629 =cut
1630
1631 sub _rebless {
1632   my $self = shift;
1633   my $plan = $self->plan;
1634   unless ( $plan ) {
1635     cluck "no price plan found for pkgpart ". $self->pkgpart. "\n"
1636       if $DEBUG;
1637     return $self;
1638   }
1639   return $self if ref($self) =~ /::$plan$/; #already blessed into plan subclass
1640   my $class = ref($self). "::$plan";
1641   warn "reblessing $self into $class" if $DEBUG > 1;
1642   eval "use $class;";
1643   die $@ if $@;
1644   bless($self, $class) unless $@;
1645   $self;
1646 }
1647
1648 #fatal fallbacks
1649 sub calc_setup { die 'no calc_setup for '. shift->plan. "\n"; }
1650 sub calc_recur { die 'no calc_recur for '. shift->plan. "\n"; }
1651
1652 #fallback that return 0 for old legacy packages with no plan
1653 sub calc_remain { 0; }
1654 sub calc_units  { 0; }
1655
1656 #fallback for everything not based on flat.pm
1657 sub recur_temporality { 'upcoming'; }
1658 sub calc_cancel { 0; }
1659
1660 #fallback for everything except bulk.pm
1661 sub hide_svc_detail { 0; }
1662
1663 #fallback for packages that can't/won't summarize usage
1664 sub sum_usage { 0; }
1665
1666 =item recur_cost_permonth CUST_PKG
1667
1668 recur_cost divided by freq (only supported for monthly and longer frequencies)
1669
1670 =cut
1671
1672 sub recur_cost_permonth {
1673   my($self, $cust_pkg) = @_;
1674   return 0 unless $self->freq =~ /^\d+$/ && $self->freq > 0;
1675   sprintf('%.2f', ($self->recur_cost || 0) / $self->freq );
1676 }
1677
1678 =item cust_bill_pkg_recur CUST_PKG
1679
1680 Actual recurring charge for the specified customer package from customer's most
1681 recent invoice
1682
1683 =cut
1684
1685 sub cust_bill_pkg_recur {
1686   my($self, $cust_pkg) = @_;
1687   my $cust_bill_pkg = qsearchs({
1688     'table'     => 'cust_bill_pkg',
1689     'addl_from' => 'LEFT JOIN cust_bill USING ( invnum )',
1690     'hashref'   => { 'pkgnum' => $cust_pkg->pkgnum,
1691                      'recur'  => { op=>'>', value=>'0' },
1692                    },
1693     'order_by'  => 'ORDER BY cust_bill._date     DESC,
1694                              cust_bill_pkg.sdate DESC
1695                      LIMIT 1
1696                    ',
1697   }) or return 0; #die "use cust_bill_pkg_recur credits with once_perinv condition";
1698   $cust_bill_pkg->recur;
1699 }
1700
1701 =item unit_setup CUST_PKG
1702
1703 Returns the setup fee for one unit of the package.
1704
1705 =cut
1706
1707 sub unit_setup {
1708   my ($self, $cust_pkg) = @_;
1709   $self->option('setup_fee') || 0;
1710 }
1711
1712 =item setup_margin
1713
1714 unit_setup minus setup_cost
1715
1716 =cut
1717
1718 sub setup_margin {
1719   my $self = shift;
1720   $self->unit_setup(@_) - ($self->setup_cost || 0);
1721 }
1722
1723 =item recur_margin_permonth
1724
1725 base_recur_permonth minus recur_cost_permonth
1726
1727 =cut
1728
1729 sub recur_margin_permonth {
1730   my $self = shift;
1731   $self->base_recur_permonth(@_) - $self->recur_cost_permonth(@_);
1732 }
1733
1734 =item intro_end PACKAGE
1735
1736 Takes an L<FS::cust_pkg> object.  If this plan has an introductory rate,
1737 returns the expected date the intro period will end. If there is no intro
1738 rate, returns zero.
1739
1740 =cut
1741
1742 sub intro_end {
1743   0;
1744 }
1745
1746 =item format OPTION DATA
1747
1748 Returns data formatted according to the function 'format' described
1749 in the plan info.  Returns DATA if no such function exists.
1750
1751 =cut
1752
1753 sub format {
1754   my ($self, $option, $data) = (shift, shift, shift);
1755   if (exists($plans{$self->plan}->{fields}->{$option}{format})) {
1756     &{$plans{$self->plan}->{fields}->{$option}{format}}($data);
1757   }else{
1758     $data;
1759   }
1760 }
1761
1762 =item parse OPTION DATA
1763
1764 Returns data parsed according to the function 'parse' described
1765 in the plan info.  Returns DATA if no such function exists.
1766
1767 =cut
1768
1769 sub parse {
1770   my ($self, $option, $data) = (shift, shift, shift);
1771   if (exists($plans{$self->plan}->{fields}->{$option}{parse})) {
1772     &{$plans{$self->plan}->{fields}->{$option}{parse}}($data);
1773   }else{
1774     $data;
1775   }
1776 }
1777
1778 =back
1779
1780 =cut
1781
1782 =head1 CLASS METHODS
1783
1784 =over 4
1785
1786 =cut
1787
1788 # _upgrade_data
1789 #
1790 # Used by FS::Upgrade to migrate to a new database.
1791
1792 sub _upgrade_data { # class method
1793   my($class, %opts) = @_;
1794
1795   warn "[FS::part_pkg] upgrading $class\n" if $DEBUG;
1796
1797   my @part_pkg = qsearch({
1798     'table'     => 'part_pkg',
1799     'extra_sql' => "WHERE ". join(' OR ',
1800                      'plan IS NULL', "plan = '' ",
1801                    ),
1802   });
1803
1804   foreach my $part_pkg (@part_pkg) {
1805
1806     unless ( $part_pkg->plan ) {
1807       $part_pkg->plan('flat');
1808     }
1809
1810     $part_pkg->replace;
1811
1812   }
1813   # the rest can be done asynchronously
1814 }
1815
1816 sub queueable_upgrade {
1817   # now upgrade to the explicit custom flag
1818
1819   my $search = FS::Cursor->new({
1820     'table'     => 'part_pkg',
1821     'hashref'   => { disabled => 'Y', custom => '' },
1822     'extra_sql' => "AND comment LIKE '(CUSTOM) %'",
1823   });
1824   my $dbh = dbh;
1825
1826   while (my $part_pkg = $search->fetch) {
1827     my $new = new FS::part_pkg { $part_pkg->hash };
1828     $new->custom('Y');
1829     my $comment = $part_pkg->comment;
1830     $comment =~ s/^\(CUSTOM\) //;
1831     $comment = '(none)' unless $comment =~ /\S/;
1832     $new->comment($comment);
1833
1834     my $pkg_svc = { map { $_->svcpart => $_->quantity } $part_pkg->pkg_svc };
1835     my $primary = $part_pkg->svcpart;
1836     my $options = { $part_pkg->options };
1837
1838     my $error = $new->replace( $part_pkg,
1839                                'pkg_svc'     => $pkg_svc,
1840                                'primary_svc' => $primary,
1841                                'options'     => $options,
1842                              );
1843     if ($error) {
1844       warn "pkgpart#".$part_pkg->pkgpart.": $error\n";
1845       $dbh->rollback;
1846     } else {
1847       $dbh->commit;
1848     }
1849   }
1850
1851   # set family_pkgpart on any packages that don't have it
1852   $search = FS::Cursor->new('part_pkg', { 'family_pkgpart' => '' });
1853   while (my $part_pkg = $search->fetch) {
1854     $part_pkg->set('family_pkgpart' => $part_pkg->pkgpart);
1855     my $error = $part_pkg->SUPER::replace;
1856     if ($error) {
1857       warn "pkgpart#".$part_pkg->pkgpart.": $error\n";
1858       $dbh->rollback;
1859     } else {
1860       $dbh->commit;
1861     }
1862   }
1863
1864   my @part_pkg_option = qsearch('part_pkg_option',
1865     { 'optionname'  => 'unused_credit',
1866       'optionvalue' => 1,
1867     });
1868   foreach my $old_opt (@part_pkg_option) {
1869     my $pkgpart = $old_opt->pkgpart;
1870     my $error = $old_opt->delete;
1871     die $error if $error;
1872
1873     foreach (qw(unused_credit_cancel unused_credit_change)) {
1874       my $new_opt = new FS::part_pkg_option {
1875         'pkgpart'     => $pkgpart,
1876         'optionname'  => $_,
1877         'optionvalue' => 1,
1878       };
1879       $error = $new_opt->insert;
1880       die $error if $error;
1881     }
1882   }
1883
1884   # migrate use_disposition_taqua and use_disposition to disposition_in
1885   @part_pkg_option = qsearch('part_pkg_option',
1886     { 'optionname'  => { op => 'LIKE',
1887                          value => 'use_disposition%',
1888                        },
1889       'optionvalue' => 1,
1890     });
1891   my %newopts = map { $_->pkgpart => $_ } 
1892     qsearch('part_pkg_option',  { 'optionname'  => 'disposition_in', } );
1893   foreach my $old_opt (@part_pkg_option) {
1894         my $pkgpart = $old_opt->pkgpart;
1895         my $newval = $old_opt->optionname eq 'use_disposition_taqua' ? '100' 
1896                                                                   : 'ANSWERED';
1897         my $error = $old_opt->delete;
1898         die $error if $error;
1899
1900         if ( exists($newopts{$pkgpart}) ) {
1901             my $opt = $newopts{$pkgpart};
1902             $opt->optionvalue($opt->optionvalue.",$newval");
1903             $error = $opt->replace;
1904             die $error if $error;
1905         } else {
1906             my $new_opt = new FS::part_pkg_option {
1907                 'pkgpart'     => $pkgpart,
1908                 'optionname'  => 'disposition_in',
1909                 'optionvalue' => $newval,
1910               };
1911               $error = $new_opt->insert;
1912               die $error if $error;
1913               $newopts{$pkgpart} = $new_opt;
1914         }
1915   }
1916
1917   # set any package with FCC voice lines to the "VoIP with broadband" category
1918   # for backward compatibility
1919   #
1920   # recover from a bad upgrade bug
1921   my $upgrade = 'part_pkg_fcc_voip_class_FIX';
1922   if (!FS::upgrade_journal->is_done($upgrade)) {
1923     my $bad_upgrade = qsearchs('upgrade_journal', 
1924       { upgrade => 'part_pkg_fcc_voip_class' }
1925     );
1926     if ( $bad_upgrade ) {
1927       my $where = 'WHERE history_date <= '.$bad_upgrade->_date.
1928                   ' AND  history_date >  '.($bad_upgrade->_date - 3600);
1929       my @h_part_pkg_option = map { FS::part_pkg_option->new($_->hashref) }
1930         qsearch({
1931           'select'    => '*',
1932           'table'     => 'h_part_pkg_option',
1933           'hashref'   => {},
1934           'extra_sql' => "$where AND history_action = 'delete'",
1935           'order_by'  => 'ORDER BY history_date ASC',
1936         });
1937       my @h_pkg_svc = map { FS::pkg_svc->new($_->hashref) }
1938         qsearch({
1939           'select'    => '*',
1940           'table'     => 'h_pkg_svc',
1941           'hashref'   => {},
1942           'extra_sql' => "$where AND history_action = 'replace_old'",
1943           'order_by'  => 'ORDER BY history_date ASC',
1944         });
1945       my %opt;
1946       foreach my $deleted (@h_part_pkg_option, @h_pkg_svc) {
1947         my $pkgpart ||= $deleted->pkgpart;
1948         $opt{$pkgpart} ||= {
1949           options => {},
1950           pkg_svc => {},
1951           primary_svc => '',
1952           hidden_svc => {},
1953         };
1954         if ( $deleted->isa('FS::part_pkg_option') ) {
1955           $opt{$pkgpart}{options}{ $deleted->optionname } = $deleted->optionvalue;
1956         } else { # pkg_svc
1957           my $svcpart = $deleted->svcpart;
1958           $opt{$pkgpart}{pkg_svc}{$svcpart} = $deleted->quantity;
1959           $opt{$pkgpart}{hidden_svc}{$svcpart} ||= $deleted->hidden;
1960           $opt{$pkgpart}{primary_svc} = $svcpart if $deleted->primary_svc;
1961         }
1962       }
1963       foreach my $pkgpart (keys %opt) {
1964         my $part_pkg = FS::part_pkg->by_key($pkgpart);
1965         my $error = $part_pkg->replace( $part_pkg->replace_old, $opt{$pkgpart} );
1966         if ( $error ) {
1967           die "error recovering damaged pkgpart $pkgpart:\n$error\n";
1968         }
1969       }
1970     } # $bad_upgrade exists
1971     else { # do the original upgrade, but correctly this time
1972       my @part_pkg = qsearch('part_pkg', {
1973           fcc_ds0s        => { op => '>', value => 0 },
1974           fcc_voip_class  => ''
1975       });
1976       foreach my $part_pkg (@part_pkg) {
1977         $part_pkg->set(fcc_voip_class => 2);
1978         my @pkg_svc = $part_pkg->pkg_svc;
1979         my %quantity = map {$_->svcpart, $_->quantity} @pkg_svc;
1980         my %hidden   = map {$_->svcpart, $_->hidden  } @pkg_svc;
1981         my $error = $part_pkg->replace(
1982           $part_pkg->replace_old,
1983           options     => { $part_pkg->options },
1984           pkg_svc     => \%quantity,
1985           hidden_svc  => \%hidden,
1986           primary_svc => ($part_pkg->svcpart || ''),
1987         );
1988         die $error if $error;
1989       }
1990     }
1991     FS::upgrade_journal->set_done($upgrade);
1992   }
1993
1994   # remove custom flag from one-time charge packages that were accidentally
1995   # flagged as custom
1996   $search = FS::Cursor->new({
1997     'table'   => 'part_pkg',
1998     'hashref' => { 'freq'   => '0',
1999                    'custom' => 'Y',
2000                    'family_pkgpart' => { op => '!=', value => '' },
2001                  },
2002     'addl_from' => ' JOIN
2003   (select pkgpart from cust_pkg group by pkgpart having count(*) = 1)
2004     AS singular_pkg USING (pkgpart)',
2005   });
2006   my @fields = grep {     $_ ne 'pkgpart'
2007                       and $_ ne 'custom'
2008                       and $_ ne 'disabled' } FS::part_pkg->fields;
2009   PKGPART: while (my $part_pkg = $search->fetch) {
2010     # can't merge the package back into its parent (too late for that)
2011     # but we can remove the custom flag if it's not actually customized,
2012     # i.e. nothing has been changed.
2013
2014     my $family_pkgpart = $part_pkg->family_pkgpart;
2015     next PKGPART if $family_pkgpart == $part_pkg->pkgpart;
2016     my $parent_pkg = FS::part_pkg->by_key($family_pkgpart);
2017     foreach my $field (@fields) {
2018       if ($part_pkg->get($field) ne $parent_pkg->get($field)) {
2019         next PKGPART;
2020       }
2021     }
2022     # options have to be identical too
2023     # but links, FCC options, discount plans, and usage packages can't be
2024     # changed through the "modify charge" UI, so skip them
2025     my %newopt = $part_pkg->options;
2026     my %oldopt = $parent_pkg->options;
2027     OPTION: foreach my $option (keys %newopt) {
2028       if (delete $newopt{$option} ne delete $oldopt{$option}) {
2029         next PKGPART;
2030       }
2031     }
2032     if (keys(%newopt) or keys(%oldopt)) {
2033       next PKGPART;
2034     }
2035     # okay, now replace it
2036     warn "Removing custom flag from part_pkg#".$part_pkg->pkgpart."\n";
2037     $part_pkg->set('custom', '');
2038     my $error = $part_pkg->replace;
2039     die $error if $error;
2040   } # $search->fetch
2041
2042   return;
2043 }
2044
2045 =item curuser_pkgs_sql
2046
2047 Returns an SQL fragment for searching for packages the current user can
2048 use, either via part_pkg.agentnum directly, or via agent type (see
2049 L<FS::type_pkgs>).
2050
2051 =cut
2052
2053 sub curuser_pkgs_sql {
2054   my $class = shift;
2055
2056   $class->_pkgs_sql( $FS::CurrentUser::CurrentUser->agentnums );
2057
2058 }
2059
2060 =item agent_pkgs_sql AGENT | AGENTNUM, ...
2061
2062 Returns an SQL fragment for searching for packages the provided agent or agents
2063 can use, either via part_pkg.agentnum directly, or via agent type (see
2064 L<FS::type_pkgs>).
2065
2066 =cut
2067
2068 sub agent_pkgs_sql {
2069   my $class = shift;  #i'm a class method, not a sub (the question is... why??)
2070   my @agentnums = map { ref($_) ? $_->agentnum : $_ } @_;
2071
2072   $class->_pkgs_sql(@agentnums); #is this why
2073
2074 }
2075
2076 sub _pkgs_sql {
2077   my( $class, @agentnums ) = @_;
2078   my $agentnums = join(',', @agentnums);
2079
2080   "
2081     (
2082       ( agentnum IS NOT NULL AND agentnum IN ($agentnums) )
2083       OR ( agentnum IS NULL
2084            AND EXISTS ( SELECT 1
2085                           FROM type_pkgs
2086                             LEFT JOIN agent_type USING ( typenum )
2087                             LEFT JOIN agent AS typeagent USING ( typenum )
2088                           WHERE type_pkgs.pkgpart = part_pkg.pkgpart
2089                             AND typeagent.agentnum IN ($agentnums)
2090                       )
2091          )
2092     )
2093   ";
2094
2095 }
2096
2097 =back
2098
2099 =head1 SUBROUTINES
2100
2101 =over 4
2102
2103 =item plan_info
2104
2105 =cut
2106
2107 #false laziness w/part_export & cdr
2108 my %info;
2109 foreach my $INC ( @INC ) {
2110   warn "globbing $INC/FS/part_pkg/[a-z]*.pm\n" if $DEBUG;
2111   foreach my $file ( glob("$INC/FS/part_pkg/[a-z]*.pm") ) {
2112     warn "attempting to load plan info from $file\n" if $DEBUG;
2113     $file =~ /\/(\w+)\.pm$/ or do {
2114       warn "unrecognized file in $INC/FS/part_pkg/: $file\n";
2115       next;
2116     };
2117     my $mod = $1;
2118     my $info = eval "use FS::part_pkg::$mod; ".
2119                     "\\%FS::part_pkg::$mod\::info;";
2120     if ( $@ ) {
2121       die "error using FS::part_pkg::$mod (skipping): $@\n" if $@;
2122       next;
2123     }
2124     unless ( keys %$info ) {
2125       warn "no %info hash found in FS::part_pkg::$mod, skipping\n";
2126       next;
2127     }
2128     warn "got plan info from FS::part_pkg::$mod: $info\n" if $DEBUG;
2129     #if ( exists($info->{'disabled'}) && $info->{'disabled'} ) {
2130     #  warn "skipping disabled plan FS::part_pkg::$mod" if $DEBUG;
2131     #  next;
2132     #}
2133     $info{$mod} = $info;
2134     $info->{'weight'} ||= 0; # quiet warnings
2135   }
2136 }
2137
2138 # copy one level deep to allow replacement of fields and fieldorder
2139 tie %plans, 'Tie::IxHash',
2140   map  { my %infohash = %{ $info{$_} }; 
2141           $_ => \%infohash }
2142   sort { $info{$a}->{'weight'} <=> $info{$b}->{'weight'} }
2143   keys %info;
2144
2145 # inheritance of plan options
2146 foreach my $name (keys(%info)) {
2147   if (exists($info{$name}->{'disabled'}) and $info{$name}->{'disabled'}) {
2148     warn "skipping disabled plan FS::part_pkg::$name" if $DEBUG;
2149     delete $plans{$name};
2150     next;
2151   }
2152   my $parents = $info{$name}->{'inherit_fields'} || [];
2153   my (%fields, %field_exists, @fieldorder);
2154   foreach my $parent ($name, @$parents) {
2155     if ( !exists($info{$parent}) ) {
2156       warn "$name tried to inherit from nonexistent '$parent'\n";
2157       next;
2158     }
2159     %fields = ( # avoid replacing existing fields
2160       %{ $info{$parent}->{'fields'} || {} },
2161       %fields
2162     );
2163     foreach (@{ $info{$parent}->{'fieldorder'} || [] }) {
2164       # avoid duplicates
2165       next if $field_exists{$_};
2166       $field_exists{$_} = 1;
2167       # allow inheritors to remove inherited fields from the fieldorder
2168       push @fieldorder, $_ if !exists($fields{$_}) or
2169                               !exists($fields{$_}->{'disabled'});
2170     }
2171   }
2172   $plans{$name}->{'fields'} = \%fields;
2173   $plans{$name}->{'fieldorder'} = \@fieldorder;
2174 }
2175
2176 sub plan_info {
2177   \%plans;
2178 }
2179
2180
2181 =back
2182
2183 =head1 NEW PLAN CLASSES
2184
2185 A module should be added in FS/FS/part_pkg/  Eventually, an example may be
2186 found in eg/plan_template.pm.  Until then, it is suggested that you use the
2187 other modules in FS/FS/part_pkg/ as a guide.
2188
2189 =head1 BUGS
2190
2191 The delete method is unimplemented.
2192
2193 setup and recur semantics are not yet defined (and are implemented in
2194 FS::cust_bill.  hmm.).  now they're deprecated and need to go.
2195
2196 plandata should go
2197
2198 part_pkg_taxrate is Pg specific
2199
2200 replace should be smarter about managing the related tables (options, pkg_svc)
2201
2202 =head1 SEE ALSO
2203
2204 L<FS::Record>, L<FS::cust_pkg>, L<FS::type_pkgs>, L<FS::pkg_svc>, L<Safe>.
2205 schema.html from the base documentation.
2206
2207 =cut
2208
2209 1;