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