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