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