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