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