silence many spurious warnings from part_pkg options
[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   FS::part_svc_link->by_agentnum( shift->agentnum, @_ );
801 }
802
803 =item supersede OLD [, OPTION => VALUE ... ]
804
805 Inserts this package as a successor to the package OLD.  All options are as
806 for C<insert>.  After inserting, disables OLD and sets the new package as its
807 successor.
808
809 =cut
810
811 sub supersede {
812   my ($new, $old, %options) = @_;
813   my $error;
814
815   $new->set('pkgpart' => '');
816   $new->set('family_pkgpart' => $old->family_pkgpart);
817   warn "    inserting successor package\n" if $DEBUG;
818   $error = $new->insert(%options);
819   return $error if $error;
820  
821   warn "    disabling superseded package\n" if $DEBUG; 
822   $old->set('successor' => $new->pkgpart);
823   $old->set('disabled' => 'Y');
824   $error = $old->SUPER::replace; # don't change its options/pkg_svc records
825   return $error if $error;
826
827   warn "  propagating changes to family" if $DEBUG;
828   $new->propagate($old);
829 }
830
831 =item propagate OLD
832
833 If any of certain fields have changed from OLD to this package, then,
834 for all packages in the same lineage as this one, sets those fields 
835 to their values in this package.
836
837 =cut
838
839 my @propagate_fields = (
840   qw( pkg classnum setup_cost recur_cost taxclass
841   setuptax recurtax pay_weight credit_weight
842   )
843 );
844
845 sub propagate {
846   my $new = shift;
847   my $old = shift;
848   my %fields = (
849     map { $_ => $new->get($_) }
850     grep { $new->get($_) ne $old->get($_) }
851     @propagate_fields
852   );
853
854   my @part_pkg = qsearch('part_pkg', { 
855       'family_pkgpart' => $new->family_pkgpart 
856   });
857   my @error;
858   foreach my $part_pkg ( @part_pkg ) {
859     my $pkgpart = $part_pkg->pkgpart;
860     next if $pkgpart == $new->pkgpart; # don't modify $new
861     warn "    propagating to pkgpart $pkgpart\n" if $DEBUG;
862     foreach ( keys %fields ) {
863       $part_pkg->set($_, $fields{$_});
864     }
865     # SUPER::replace to avoid changing non-core fields
866     my $error = $part_pkg->SUPER::replace;
867     push @error, "pkgpart $pkgpart: $error"
868       if $error;
869   }
870   join("\n", @error);
871 }
872
873 =item set_fcc_options HASHREF
874
875 Sets the FCC options on this package definition to the values specified
876 in HASHREF.
877
878 =cut
879
880 sub set_fcc_options {
881   my $self = shift;
882   my $pkgpart = $self->pkgpart;
883   my $options;
884   if (ref $_[0]) {
885     $options = shift;
886   } else {
887     $options = { @_ };
888   }
889
890   my %existing_num = map { $_->fccoptionname => $_->num }
891                      qsearch('part_pkg_fcc_option', { pkgpart => $pkgpart });
892
893   local $FS::Record::nowarn_identical = 1;
894   # set up params for process_o2m
895   my $i = 0;
896   my $params = {};
897   foreach my $name (keys %$options ) {
898     $params->{ "num$i" } = $existing_num{$name} || '';
899     $params->{ "num$i".'_fccoptionname' } = $name;
900     $params->{ "num$i".'_optionvalue'   } = $options->{$name};
901     $i++;
902   }
903
904   $self->process_o2m(
905     table   => 'part_pkg_fcc_option',
906     fields  => [qw( fccoptionname optionvalue )],
907     params  => $params,
908   );
909 }
910
911 =item pkg_locale LOCALE
912
913 Returns a customer-viewable string representing this package for the given
914 locale, from the part_pkg_msgcat table.  If the given locale is empty or no
915 localized string is found, returns the base pkg field.
916
917 =cut
918
919 sub pkg_locale {
920   my( $self, $locale ) = @_;
921   return $self->pkg unless $locale;
922   my $part_pkg_msgcat = $self->part_pkg_msgcat($locale) or return $self->pkg;
923   $part_pkg_msgcat->pkg;
924 }
925
926 =item part_pkg_msgcat LOCALE
927
928 Like pkg_locale, but returns the FS::part_pkg_msgcat object itself.
929
930 =cut
931
932 sub part_pkg_msgcat {
933   my( $self, $locale ) = @_;
934   qsearchs( 'part_pkg_msgcat', {
935     pkgpart => $self->pkgpart,
936     locale  => $locale,
937   });
938 }
939
940 =item pkg_comment [ OPTION => VALUE... ]
941
942 Returns an (internal) string representing this package.  Currently,
943 "pkgpart: pkg - comment", is returned.  "pkg - comment" may be returned in the
944 future, omitting pkgpart.  The comment will have '(CUSTOM) ' prepended if
945 custom is Y.
946
947 If the option nopkgpart is true then the "pkgpart: ' is omitted.
948
949 =cut
950
951 sub pkg_comment {
952   my $self = shift;
953   my %opt = @_;
954
955   #$self->pkg. ' - '. $self->comment;
956   #$self->pkg. ' ('. $self->comment. ')';
957   my $pre = $opt{nopkgpart} ? '' : $self->pkgpart. ': ';
958   my $custom_comment = $self->custom_comment(%opt);
959   $pre. $self->pkg. ( $custom_comment ? " - $custom_comment" : '' );
960 }
961
962 #without price info (so without hitting the DB again)
963 sub pkg_comment_only {
964   my $self = shift;
965   my %opt = @_;
966
967   my $pre = $opt{nopkgpart} ? '' : $self->pkgpart. ': ';
968   my $comment = $self->comment;
969   $pre. $self->pkg. ( $comment ? " - $comment" : '' );
970 }
971
972 sub price_info { # safety, in case a part_pkg hasn't defined price_info
973     '';
974 }
975
976 sub custom_comment {
977   my $self = shift;
978   my $price_info = $self->price_info(@_);
979   ( $self->custom ? '(CUSTOM) ' : '' ).
980     $self->comment.
981     ( ($self->custom || $self->comment) ? ' - ' : '' ).
982     ($price_info || 'No charge');
983 }
984
985 sub pkg_price_info {
986   my $self = shift;
987   $self->pkg. ' - '. ($self->price_info || 'No charge');
988 }
989
990 =item pkg_class
991
992 Returns the package class, as an FS::pkg_class object, or the empty string
993 if there is no package class.
994
995 =item addon_pkg_class
996
997 Returns the add-on package class, as an FS::pkg_class object, or the empty
998 string if there is no add-on package class.
999
1000 =cut
1001
1002 sub addon_pkg_class {
1003   my $self = shift;
1004   if ( $self->addon_classnum ) {
1005     qsearchs('pkg_class', { 'classnum' => $self->addon_classnum } );
1006   } else {
1007     return '';
1008   }
1009 }
1010
1011 =item categoryname 
1012
1013 Returns the package category name, or the empty string if there is no package
1014 category.
1015
1016 =cut
1017
1018 sub categoryname {
1019   my $self = shift;
1020   my $pkg_class = $self->pkg_class;
1021   $pkg_class
1022     ? $pkg_class->categoryname
1023     : '';
1024 }
1025
1026 =item classname 
1027
1028 Returns the package class name, or the empty string if there is no package
1029 class.
1030
1031 =cut
1032
1033 sub classname {
1034   my $self = shift;
1035   my $pkg_class = $self->pkg_class;
1036   $pkg_class
1037     ? $pkg_class->classname
1038     : '';
1039 }
1040
1041 =item addon_classname 
1042
1043 Returns the add-on package class name, or the empty string if there is no
1044 add-on package class.
1045
1046 =cut
1047
1048 sub addon_classname {
1049   my $self = shift;
1050   my $pkg_class = $self->addon_pkg_class;
1051   $pkg_class
1052     ? $pkg_class->classname
1053     : '';
1054 }
1055
1056 =item agent 
1057
1058 Returns the associated agent for this event, if any, as an FS::agent object.
1059
1060 =item pkg_svc [ HASHREF | OPTION => VALUE ]
1061
1062 Returns all FS::pkg_svc objects (see L<FS::pkg_svc>) for this package
1063 definition (with non-zero quantity).
1064
1065 One option is available, I<disable_linked>.  If set true it will return the
1066 services for this package definition alone, omitting services from any add-on
1067 packages.
1068
1069 =cut
1070
1071 =item type_pkgs
1072
1073 Returns all FS::type_pkgs objects (see L<FS::type_pkgs>) for this package
1074 definition.
1075
1076 =cut
1077
1078 sub pkg_svc {
1079   my $self = shift;
1080
1081 #  #sort { $b->primary cmp $a->primary } 
1082 #    grep { $_->quantity }
1083 #      qsearch( 'pkg_svc', { 'pkgpart' => $self->pkgpart } );
1084
1085   my $opt = ref($_[0]) ? $_[0] : { @_ };
1086   my %pkg_svc = map  { $_->svcpart => $_ }
1087                 grep { $_->quantity }
1088                 qsearch( 'pkg_svc', { 'pkgpart' => $self->pkgpart } );
1089
1090   unless ( $opt->{disable_linked} ) {
1091     foreach my $dst_pkg ( map $_->dst_pkg, $self->svc_part_pkg_link ) {
1092       my @pkg_svc = grep { $_->quantity }
1093                     qsearch( 'pkg_svc', { pkgpart=>$dst_pkg->pkgpart } );
1094       foreach my $pkg_svc ( @pkg_svc ) {
1095         if ( $pkg_svc{$pkg_svc->svcpart} ) {
1096           my $quantity = $pkg_svc{$pkg_svc->svcpart}->quantity;
1097           $pkg_svc{$pkg_svc->svcpart}->quantity($quantity + $pkg_svc->quantity);
1098         } else {
1099           $pkg_svc{$pkg_svc->svcpart} = $pkg_svc;
1100         }
1101       }
1102     }
1103   }
1104
1105   values(%pkg_svc);
1106
1107 }
1108
1109 =item svcpart [ SVCDB ]
1110
1111 Returns the svcpart of the primary service definition (see L<FS::part_svc>)
1112 associated with this package definition (see L<FS::pkg_svc>).  Returns
1113 false if there not a primary service definition or exactly one service
1114 definition with quantity 1, or if SVCDB is specified and does not match the
1115 svcdb of the service definition.  SVCDB can be specified as a scalar table
1116 name, such as 'svc_acct', or as an arrayref of possible table names.
1117
1118 =cut
1119
1120 sub svcpart {
1121   my $pkg_svc = shift->_primary_pkg_svc(@_);
1122   $pkg_svc ? $pkg_svc->svcpart : '';
1123 }
1124
1125 =item part_svc [ SVCDB ]
1126
1127 Like the B<svcpart> method, but returns the FS::part_svc object (see
1128 L<FS::part_svc>).
1129
1130 =cut
1131
1132 sub part_svc {
1133   my $pkg_svc = shift->_primary_pkg_svc(@_);
1134   $pkg_svc ? $pkg_svc->part_svc : '';
1135 }
1136
1137 sub _primary_pkg_svc {
1138   my $self = shift;
1139
1140   my $svcdb = scalar(@_) ? shift : [];
1141   $svcdb = ref($svcdb) ? $svcdb : [ $svcdb ];
1142   my %svcdb = map { $_=>1 } @$svcdb;
1143
1144   my @svcdb_pkg_svc =
1145     grep { !scalar(@$svcdb) || $svcdb{ $_->part_svc->svcdb } }
1146          $self->pkg_svc;
1147
1148   my @pkg_svc = grep { $_->primary_svc =~ /^Y/i } @svcdb_pkg_svc;
1149   @pkg_svc = grep {$_->quantity == 1 } @svcdb_pkg_svc
1150     unless @pkg_svc;
1151   return '' if scalar(@pkg_svc) != 1;
1152   $pkg_svc[0];
1153 }
1154
1155 =item svcpart_unique_svcdb SVCDB
1156
1157 Returns the svcpart of a service definition (see L<FS::part_svc>) matching
1158 SVCDB associated with this package definition (see L<FS::pkg_svc>).  Returns
1159 false if there not a primary service definition for SVCDB or there are multiple
1160 service definitions for SVCDB.
1161
1162 =cut
1163
1164 sub svcpart_unique_svcdb {
1165   my( $self, $svcdb ) = @_;
1166   my @svcdb_pkg_svc = grep { ( $svcdb eq $_->part_svc->svcdb ) } $self->pkg_svc;
1167   return '' if scalar(@svcdb_pkg_svc) != 1;
1168   $svcdb_pkg_svc[0]->svcpart;
1169 }
1170
1171 =item payby
1172
1173 Returns a list of the acceptable payment types for this package.  Eventually
1174 this should come out of a database table and be editable, but currently has the
1175 following logic instead:
1176
1177 If the package is free, the single item B<BILL> is
1178 returned, otherwise, the single item B<CARD> is returned.
1179
1180 (CHEK?  LEC?  Probably shouldn't accept those by default, prone to abuse)
1181
1182 =cut
1183
1184 sub payby {
1185   my $self = shift;
1186   if ( $self->is_free ) {
1187     ( 'BILL' );
1188   } else {
1189     ( 'CARD' );
1190   }
1191 }
1192
1193 =item is_free
1194
1195 Returns true if this package is free.  
1196
1197 =cut
1198
1199 sub is_free {
1200   my $self = shift;
1201   if ( $self->can('is_free_options') ) {
1202     not grep { $_ !~ /^\s*0*(\.0*)?\s*$/ }
1203          map { $self->option($_) } 
1204              $self->is_free_options;
1205   } else {
1206     warn "FS::part_pkg::is_free: FS::part_pkg::". $self->plan. " subclass ".
1207          "provides neither is_free_options nor is_free method; returning false";
1208     0;
1209   }
1210 }
1211
1212 # whether the plan allows discounts to be applied to this package
1213 sub can_discount { 0; }
1214  
1215 # whether the plan allows changing the start date
1216 sub can_start_date {
1217   my $self = shift;
1218   $self->start_on_hold ? 0 : 1;
1219 }
1220
1221 # whether the plan supports part_pkg_usageprice add-ons (a specific kind of
1222 #  pre-selectable usage pricing, there's others this doesn't refer to)
1223 sub can_usageprice { 0; }
1224   
1225 # the delay start date if present
1226 sub delay_start_date {
1227   my $self = shift;
1228
1229   my $delay = $self->delay_start or return '';
1230
1231   # avoid timelocal silliness  
1232   my $dt = DateTime->today(time_zone => 'local');
1233   $dt->add(days => $delay);
1234   $dt->epoch;
1235 }
1236
1237 sub can_currency_exchange { 0; }
1238
1239 sub freqs_href {
1240   # moved to FS::Misc to make this accessible to other packages
1241   # at initialization
1242   FS::Misc::pkg_freqs();
1243 }
1244
1245 =item freq_pretty
1246
1247 Returns an english representation of the I<freq> field, such as "monthly",
1248 "weekly", "semi-annually", etc.
1249
1250 =cut
1251
1252 sub freq_pretty {
1253   my $self = shift;
1254   my $freq = $self->freq;
1255
1256   #my $freqs_href = $self->freqs_href;
1257   my $freqs_href = freqs_href();
1258
1259   if ( exists($freqs_href->{$freq}) ) {
1260     $freqs_href->{$freq};
1261   } else {
1262     my $interval = 'month';
1263     if ( $freq =~ /^(\d+)([hdw])$/ ) {
1264       my %interval = ( 'h' => 'hour', 'd'=>'day', 'w'=>'week' );
1265       $interval = $interval{$2};
1266     }
1267     if ( $1 == 1 ) {
1268       "every $interval";
1269     } else {
1270       "every $freq ${interval}s";
1271     }
1272   }
1273 }
1274
1275 =item add_freq TIMESTAMP [ FREQ ]
1276
1277 Adds a billing period of some frequency to the provided timestamp and 
1278 returns the resulting timestamp, or -1 if the frequency could not be 
1279 parsed (shouldn't happen).  By default, the frequency of this package 
1280 will be used; to override this, pass a different frequency as a second 
1281 argument.
1282
1283 =cut
1284
1285 sub add_freq {
1286   my( $self, $date, $freq ) = @_;
1287   $freq = $self->freq unless $freq;
1288
1289   #change this bit to use Date::Manip? CAREFUL with timezones (see
1290   # mailing list archive)
1291   my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($date) )[0,1,2,3,4,5];
1292
1293   if ( $freq =~ /^\d+$/ ) {
1294     $mon += $freq;
1295     until ( $mon < 12 ) { $mon -= 12; $year++; }
1296
1297     $mday = 28 if $mday > 28 && FS::Conf->new->exists('anniversary-rollback');
1298
1299   } elsif ( $freq =~ /^(\d+)w$/ ) {
1300     my $weeks = $1;
1301     $mday += $weeks * 7;
1302   } elsif ( $freq =~ /^(\d+)d$/ ) {
1303     my $days = $1;
1304     $mday += $days;
1305   } elsif ( $freq =~ /^(\d+)h$/ ) {
1306     my $hours = $1;
1307     $hour += $hours;
1308   } else {
1309     return -1;
1310   }
1311
1312   timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year);
1313 }
1314
1315 =item plandata
1316
1317 For backwards compatibility, returns the plandata field as well as all options
1318 from FS::part_pkg_option.
1319
1320 =cut
1321
1322 sub plandata {
1323   my $self = shift;
1324   carp "plandata is deprecated";
1325   if ( @_ ) {
1326     $self->SUPER::plandata(@_);
1327   } else {
1328     my $plandata = $self->get('plandata');
1329     my %options = $self->options;
1330     $plandata .= join('', map { "$_=$options{$_}\n" } keys %options );
1331     $plandata;
1332   }
1333 }
1334
1335 =item part_pkg_vendor
1336
1337 Returns all vendor/external package ids as FS::part_pkg_vendor objects (see
1338 L<FS::part_pkg_vendor>).
1339
1340 =item vendor_pkg_ids
1341
1342 Returns a list of vendor/external package ids by exportnum
1343
1344 =cut
1345
1346 sub vendor_pkg_ids {
1347   my $self = shift;
1348   map { $_->exportnum => $_->vendor_pkg_id } $self->part_pkg_vendor;
1349 }
1350
1351 =item part_pkg_option
1352
1353 Returns all options as FS::part_pkg_option objects (see
1354 L<FS::part_pkg_option>).
1355
1356 =item options 
1357
1358 Returns a list of option names and values suitable for assigning to a hash.
1359
1360 =cut
1361
1362 sub options {
1363   my $self = shift;
1364   map { $_->optionname => $_->optionvalue } $self->part_pkg_option;
1365 }
1366
1367 =item option OPTIONNAME [ QUIET ]
1368
1369 Returns the option value for the given name, or the empty string.  If a true
1370 value is passed as the second argument, warnings about missing the option
1371 will be suppressed.
1372
1373 =cut
1374
1375 sub option {
1376   my( $self, $opt, $ornull ) = @_;
1377
1378   #cache: was pulled up in the original part_pkg query
1379   if ( $opt =~ /^(setup|recur)_fee$/ && defined($self->hashref->{"_$opt"}) ) {
1380     return $self->hashref->{"_$opt"};
1381   }
1382
1383   cluck "$self -> option: searching for $opt"
1384     if $DEBUG;
1385   my $part_pkg_option =
1386     qsearchs('part_pkg_option', {
1387       pkgpart    => $self->pkgpart,
1388       optionname => $opt,
1389   } );
1390   return $part_pkg_option->optionvalue if $part_pkg_option;
1391
1392   my %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); }
1393                      split("\n", $self->get('plandata') );
1394   return $plandata{$opt} if exists $plandata{$opt};
1395
1396   # check whether the option is defined in plan info (if so, don't warn)
1397   if (exists $plans{ $self->plan }->{fields}->{$opt}) {
1398     return '';
1399   }
1400   cluck "WARNING: (pkgpart ". $self->pkgpart. ") Package def option $opt ".
1401         "not found in options or plandata!\n"
1402     unless $ornull;
1403
1404   '';
1405 }
1406
1407 =item part_pkg_currency [ CURRENCY ]
1408
1409 Returns all currency options as FS::part_pkg_currency objects (see
1410 L<FS::part_pkg_currency>), or, if a currency is specified, only return the
1411 objects for that currency.
1412
1413 =cut
1414
1415 sub part_pkg_currency {
1416   my $self = shift;
1417   my %hash = ( 'pkgpart' => $self->pkgpart );
1418   $hash{'currency'} = shift if @_;
1419   qsearch('part_pkg_currency', \%hash );
1420 }
1421
1422 =item part_pkg_currency_options CURRENCY
1423
1424 Returns a list of option names and values from FS::part_pkg_currency for the
1425 specified currency.
1426
1427 =cut
1428
1429 sub part_pkg_currency_options {
1430   my $self = shift;
1431   map { $_->optionname => $_->optionvalue } $self->part_pkg_currency(shift);
1432 }
1433
1434 =item part_pkg_currency_option CURRENCY OPTIONNAME
1435
1436 Returns the option value for the given name and currency.
1437
1438 =cut
1439
1440 sub part_pkg_currency_option {
1441   my( $self, $currency, $optionname ) = @_; 
1442   my $part_pkg_currency =
1443     qsearchs('part_pkg_currency', { 'pkgpart'    => $self->pkgpart,
1444                                     'currency'   => $currency,
1445                                     'optionname' => $optionname,
1446                                   }
1447             )#;
1448   #fatal if not found?  that works for our use cases from
1449   #part_pkg/currency_fixed, but isn't how we would typically/expect the method
1450   #to behave.  have to catch it there if we change it here...
1451     or die "Unknown price for ". $self->pkg_comment. " in $currency\n";
1452
1453   $part_pkg_currency->optionvalue;
1454 }
1455
1456 =item fcc_option OPTIONNAME
1457
1458 Returns the FCC 477 report option value for the given name, or the empty 
1459 string.
1460
1461 =cut
1462
1463 sub fcc_option {
1464   my ($self, $name) = @_;
1465   my $part_pkg_fcc_option =
1466     qsearchs('part_pkg_fcc_option', {
1467         pkgpart => $self->pkgpart,
1468         fccoptionname => $name,
1469     });
1470   $part_pkg_fcc_option ? $part_pkg_fcc_option->optionvalue : '';
1471 }
1472
1473 =item fcc_options
1474
1475 Returns all FCC 477 report options for this package, as a hash-like list.
1476
1477 =cut
1478
1479 sub fcc_options {
1480   my $self = shift;
1481   map { $_->fccoptionname => $_->optionvalue }
1482     qsearch('part_pkg_fcc_option', { pkgpart => $self->pkgpart });
1483 }
1484
1485 =item bill_part_pkg_link
1486
1487 Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
1488
1489 =cut
1490
1491 sub bill_part_pkg_link {
1492   shift->_part_pkg_link('bill', @_);
1493 }
1494
1495 =item svc_part_pkg_link
1496
1497 Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
1498
1499 =cut
1500
1501 sub svc_part_pkg_link {
1502   shift->_part_pkg_link('svc', @_);
1503 }
1504
1505 =item supp_part_pkg_link
1506
1507 Returns the associated part_pkg_link records of type 'supp' (supplemental
1508 packages).
1509
1510 =cut
1511
1512 sub supp_part_pkg_link {
1513   shift->_part_pkg_link('supp', @_);
1514 }
1515
1516 sub _part_pkg_link {
1517   my( $self, $type ) = @_;
1518   qsearch({ table    => 'part_pkg_link',
1519             hashref  => { 'src_pkgpart' => $self->pkgpart,
1520                           'link_type'   => $type,
1521                           #protection against infinite recursive links
1522                           'dst_pkgpart' => { op=>'!=', value=> $self->pkgpart },
1523                         },
1524             order_by => "ORDER BY hidden",
1525          });
1526 }
1527
1528 sub self_and_bill_linked {
1529   shift->_self_and_linked('bill', @_);
1530 }
1531
1532 sub self_and_svc_linked {
1533   shift->_self_and_linked('svc', @_);
1534 }
1535
1536 sub _self_and_linked {
1537   my( $self, $type, $hidden ) = @_;
1538   $hidden ||= '';
1539
1540   my @result = ();
1541   foreach ( ( $self, map { $_->dst_pkg->_self_and_linked($type, $_->hidden) }
1542                      $self->_part_pkg_link($type) ) )
1543   {
1544     $_->hidden($hidden) if $hidden;
1545     push @result, $_;
1546   }
1547
1548   (@result);
1549 }
1550
1551 =item part_pkg_taxoverride [ CLASS ]
1552
1553 Returns all associated FS::part_pkg_taxoverride objects (see
1554 L<FS::part_pkg_taxoverride>).  Limits the returned set to those
1555 of class CLASS if defined.  Class may be one of 'setup', 'recur',
1556 the empty string (default), or a usage class number (see L<FS::usage_class>).
1557 When a class is specified, the empty string class (default) is returned
1558 if no more specific values exist.
1559
1560 =cut
1561
1562 sub part_pkg_taxoverride {
1563   my $self = shift;
1564   my $class = shift;
1565
1566   my $hashref = { 'pkgpart' => $self->pkgpart };
1567   $hashref->{'usage_class'} = $class if defined($class);
1568   my @overrides = qsearch('part_pkg_taxoverride', $hashref );
1569
1570   unless ( scalar(@overrides) || !defined($class) || !$class ){
1571     $hashref->{'usage_class'} = '';
1572     @overrides = qsearch('part_pkg_taxoverride', $hashref );
1573   }
1574
1575   @overrides;
1576 }
1577
1578 =item has_taxproduct
1579
1580 Returns true if this package has any taxproduct associated with it.  
1581
1582 =cut
1583
1584 sub has_taxproduct {
1585   my $self = shift;
1586
1587   $self->taxproductnum ||
1588   scalar( grep { $_ =~/^usage_taxproductnum_/ && $self->option($_) } 
1589           keys %{ {$self->options} }
1590   )
1591
1592 }
1593
1594
1595 =item taxproduct [ CLASS ]
1596
1597 Returns the associated tax product for this package definition (see
1598 L<FS::part_pkg_taxproduct>).  CLASS may be one of 'setup', 'recur' or
1599 the usage classnum (see L<FS::usage_class>).  Returns the default
1600 tax product for this record if the more specific CLASS value does
1601 not exist.
1602
1603 =cut
1604
1605 sub taxproduct {
1606   my $self = shift;
1607   my $class = shift;
1608
1609   my $part_pkg_taxproduct;
1610
1611   my $taxproductnum = $self->taxproductnum;
1612   if ($class) { 
1613     my $class_taxproductnum = $self->option("usage_taxproductnum_$class", 1);
1614     $taxproductnum = $class_taxproductnum
1615       if $class_taxproductnum
1616   }
1617   
1618   $part_pkg_taxproduct =
1619     qsearchs( 'part_pkg_taxproduct', { 'taxproductnum' => $taxproductnum } );
1620
1621   unless ($part_pkg_taxproduct || $taxproductnum eq $self->taxproductnum ) {
1622     $taxproductnum = $self->taxproductnum;
1623     $part_pkg_taxproduct =
1624       qsearchs( 'part_pkg_taxproduct', { 'taxproductnum' => $taxproductnum } );
1625   }
1626
1627   $part_pkg_taxproduct;
1628 }
1629
1630 =item taxproduct_description [ CLASS ]
1631
1632 Returns the description of the associated tax product for this package
1633 definition (see L<FS::part_pkg_taxproduct>).
1634
1635 =cut
1636
1637 sub taxproduct_description {
1638   my $self = shift;
1639   my $part_pkg_taxproduct = $self->taxproduct(@_);
1640   $part_pkg_taxproduct ? $part_pkg_taxproduct->description : '';
1641 }
1642
1643
1644 =item tax_rates DATA_PROVIDER, GEOCODE, [ CLASS ]
1645
1646 Returns the tax table entries (L<FS::tax_rate> objects) that apply to this
1647 package in the location specified by GEOCODE, for usage class CLASS (one of
1648 'setup', 'recur', null, or a C<usage_class> number).
1649
1650 =cut
1651
1652 sub tax_rates {
1653   my $self = shift;
1654   my ($vendor, $geocode, $class) = @_;
1655   # if this part_pkg is overridden into a specific taxclass, get that class
1656   my @taxclassnums = map { $_->taxclassnum } 
1657                      $self->part_pkg_taxoverride($class);
1658   # otherwise, get its tax product category
1659   if (!@taxclassnums) {
1660     my $part_pkg_taxproduct = $self->taxproduct($class);
1661     # If this isn't defined, then the class has no taxproduct designation,
1662     # so return no tax rates.
1663     return () if !$part_pkg_taxproduct;
1664
1665     # convert the taxproduct to the tax classes that might apply to it in 
1666     # $geocode
1667     @taxclassnums = map { $_->taxclassnum }
1668                     grep { $_->taxable eq 'Y' } # why do we need this?
1669                     $part_pkg_taxproduct->part_pkg_taxrate($geocode);
1670   }
1671   return unless @taxclassnums;
1672
1673   # then look up the actual tax_rate entries
1674   warn "Found taxclassnum values of ". join(',', @taxclassnums) ."\n"
1675       if $DEBUG;
1676   my $extra_sql = "AND taxclassnum IN (". join(',', @taxclassnums) . ")";
1677   my @taxes = qsearch({ 'table'     => 'tax_rate',
1678                         'hashref'   => { 'geocode'     => $geocode,
1679                                          'data_vendor' => $vendor,
1680                                          'disabled'    => '' },
1681                         'extra_sql' => $extra_sql,
1682                       });
1683   warn "Found taxes ". join(',', map {$_->taxnum} @taxes) ."\n"
1684       if $DEBUG;
1685
1686   return @taxes;
1687 }
1688
1689 =item part_pkg_discount
1690
1691 Returns the package to discount m2m records (see L<FS::part_pkg_discount>)
1692 for this package.
1693
1694 =item part_pkg_usage
1695
1696 Returns the voice usage pools (see L<FS::part_pkg_usage>) defined for 
1697 this package.
1698
1699 =item _rebless
1700
1701 Reblesses the object into the FS::part_pkg::PLAN class (if available), where
1702 PLAN is the object's I<plan> field.  There should be better docs
1703 on how to create new price plans, but until then, see L</NEW PLAN CLASSES>.
1704
1705 =cut
1706
1707 sub _rebless {
1708   my $self = shift;
1709   my $plan = $self->plan;
1710   unless ( $plan ) {
1711     cluck "no price plan found for pkgpart ". $self->pkgpart. "\n"
1712       if $DEBUG;
1713     return $self;
1714   }
1715   return $self if ref($self) =~ /::$plan$/; #already blessed into plan subclass
1716   my $class = ref($self). "::$plan";
1717   warn "reblessing $self into $class" if $DEBUG > 1;
1718   eval "use $class;";
1719   die $@ if $@;
1720   bless($self, $class) unless $@;
1721   $self;
1722 }
1723
1724 =item calc_setup CUST_PKG START_DATE DETAILS_ARRAYREF OPTIONS_HASHREF
1725
1726 =item calc_recur CUST_PKG START_DATE DETAILS_ARRAYREF OPTIONS_HASHREF
1727
1728 Calculates and returns the setup or recurring fees, respectively, for this
1729 package.  Implementation is in the FS::part_pkg:* module specific to this price
1730 plan.
1731
1732 Adds invoicing details to the passed-in DETAILS_ARRAYREF
1733
1734 Options are passed as a hashref.  Available options:
1735
1736 =over 4
1737
1738 =item freq_override
1739
1740 Frequency override (for calc_recur)
1741
1742 =item discounts
1743
1744 This option is filled in by the method rather than controlling its operation.
1745 It is an arrayref.  Applicable discounts will be added to the arrayref, as
1746 L<FS::cust_bill_pkg_discount|FS::cust_bill_pkg_discount records>.
1747
1748 =item real_pkgpart
1749
1750 For package add-ons, is the base L<FS::part_pkg|package definition>, otherwise
1751 no different than pkgpart.
1752
1753 =item precommit_hooks
1754
1755 This option is filled in by the method rather than controlling its operation.
1756 It is an arrayref.  Anonymous coderefs will be added to the arrayref.  They
1757 need to be called before completing the billing operation.  For calc_recur
1758 only.
1759
1760 =item increment_next_bill
1761
1762 Increment the next bill date (boolean, for calc_recur).  Typically true except
1763 for particular situations.
1764
1765 =item setup_fee
1766
1767 This option is filled in by the method rather than controlling its operation.
1768 It indicates a deferred setup fee that is billed at calc_recur time (see price
1769 plan option prorate_defer_bill).
1770
1771 =back
1772
1773 Note: Don't calculate prices when not actually billing the package.  For that,
1774 see the L</base_setup|base_setup> and L</base_recur|base_recur> methods.
1775
1776 =cut
1777
1778 #fatal fallbacks
1779 sub calc_setup { die 'no calc_setup for '. shift->plan. "\n"; }
1780 sub calc_recur { die 'no calc_recur for '. shift->plan. "\n"; }
1781
1782 =item calc_remain CUST_PKG [ OPTION => VALUE ... ]
1783
1784 Calculates and returns the remaining value to be credited upon package
1785 suspension, change, or cancellation, if enabled.
1786
1787 Options are passed as a list of keys and values.  Available options:
1788
1789 =over 4
1790
1791 =item time
1792
1793 Override for the current time
1794
1795 =item cust_credit_source_bill_pkg
1796
1797 This option is filled in by the method rather than controlling its operation.
1798 It is an arrayref.
1799 L<FS::cust_credit_source_bill_pkg|FS::cust_credit_source_bill_pkg> records will
1800 be added to the arrayref indicating the specific line items and amounts which
1801 are the source of this remaining credit.
1802
1803 =back
1804
1805 Note: Don't calculate prices when not actually suspending or cancelling the
1806 package.
1807
1808 =cut
1809
1810 #fallback that returns 0 for old legacy packages with no plan
1811 sub calc_remain { 0; }
1812
1813 =item calc_units CUST_PKG
1814
1815 This returns the number of provisioned svc_phone records, or, of the package
1816 count_available_phones option is set, the number available to be provisoined
1817 in the package.
1818
1819 =cut
1820
1821 #fallback that returns 0 for old legacy packages with no plan
1822 sub calc_units  { 0; }
1823
1824 #fallback for everything not based on flat.pm
1825 sub recur_temporality { 'upcoming'; }
1826
1827 =item calc_cancel START_DATE DETAILS_ARRAYREF OPTIONS_HASHREF
1828
1829 Runs any necessary billing on cancellation: another recurring cycle for
1830 recur_temporailty 'preceding' pacakges with the bill_recur_on_cancel option
1831 set (calc_recur), or, any outstanding usage for pacakges with the
1832 bill_usage_on_cancel option set (calc_usage).
1833
1834 =cut
1835
1836 #fallback for everything not based on flat.pm, doesn't do this yet (which is
1837 #okay, nothing of ours not based on flat.pm does usage-on-cancel billing
1838 sub calc_cancel { 0; }
1839
1840 #fallback for everything except bulk.pm
1841 sub hide_svc_detail { 0; }
1842
1843 #fallback for packages that can't/won't summarize usage
1844 sub sum_usage { 0; }
1845
1846 =item recur_cost_permonth CUST_PKG
1847
1848 recur_cost divided by freq (only supported for monthly and longer frequencies)
1849
1850 =cut
1851
1852 sub recur_cost_permonth {
1853   my($self, $cust_pkg) = @_;
1854   return 0 unless $self->freq =~ /^\d+$/ && $self->freq > 0;
1855   sprintf('%.2f', ($self->recur_cost || 0) / $self->freq );
1856 }
1857
1858 =item cust_bill_pkg_recur CUST_PKG
1859
1860 Actual recurring charge for the specified customer package from customer's most
1861 recent invoice
1862
1863 =cut
1864
1865 sub cust_bill_pkg_recur {
1866   my($self, $cust_pkg) = @_;
1867   my $cust_bill_pkg = qsearchs({
1868     'table'     => 'cust_bill_pkg',
1869     'addl_from' => 'LEFT JOIN cust_bill USING ( invnum )',
1870     'hashref'   => { 'pkgnum' => $cust_pkg->pkgnum,
1871                      'recur'  => { op=>'>', value=>'0' },
1872                    },
1873     'order_by'  => 'ORDER BY cust_bill._date     DESC,
1874                              cust_bill_pkg.sdate DESC
1875                      LIMIT 1
1876                    ',
1877   }) or return 0; #die "use cust_bill_pkg_recur credits with once_perinv condition";
1878   $cust_bill_pkg->recur;
1879 }
1880
1881 =item unit_setup CUST_PKG
1882
1883 Returns the setup fee for one unit of the package.
1884
1885 =cut
1886
1887 sub unit_setup {
1888   my ($self, $cust_pkg) = @_;
1889   $self->option('setup_fee') || 0;
1890 }
1891
1892 =item setup_margin
1893
1894 unit_setup minus setup_cost
1895
1896 =cut
1897
1898 sub setup_margin {
1899   my $self = shift;
1900   $self->unit_setup(@_) - ($self->setup_cost || 0);
1901 }
1902
1903 =item recur_margin_permonth
1904
1905 base_recur_permonth minus recur_cost_permonth
1906
1907 =cut
1908
1909 sub recur_margin_permonth {
1910   my $self = shift;
1911   $self->base_recur_permonth(@_) - $self->recur_cost_permonth(@_);
1912 }
1913
1914 =item format OPTION DATA
1915
1916 Returns data formatted according to the function 'format' described
1917 in the plan info.  Returns DATA if no such function exists.
1918
1919 =cut
1920
1921 sub format {
1922   my ($self, $option, $data) = (shift, shift, shift);
1923   if (exists($plans{$self->plan}->{fields}->{$option}{format})) {
1924     &{$plans{$self->plan}->{fields}->{$option}{format}}($data);
1925   }else{
1926     $data;
1927   }
1928 }
1929
1930 =item parse OPTION DATA
1931
1932 Returns data parsed according to the function 'parse' described
1933 in the plan info.  Returns DATA if no such function exists.
1934
1935 =cut
1936
1937 sub parse {
1938   my ($self, $option, $data) = (shift, shift, shift);
1939   if (exists($plans{$self->plan}->{fields}->{$option}{parse})) {
1940     &{$plans{$self->plan}->{fields}->{$option}{parse}}($data);
1941   }else{
1942     $data;
1943   }
1944 }
1945
1946 =back
1947
1948 =cut
1949
1950 =head1 CLASS METHODS
1951
1952 =over 4
1953
1954 =cut
1955
1956 # _upgrade_data
1957 #
1958 # Used by FS::Upgrade to migrate to a new database.
1959
1960 sub _upgrade_data { # class method
1961    my($class, %opts) = @_;
1962
1963   warn "[FS::part_pkg] upgrading $class\n" if $DEBUG;
1964
1965   my @part_pkg = qsearch({
1966     'table'     => 'part_pkg',
1967     'extra_sql' => "WHERE ". join(' OR ',
1968                      'plan IS NULL', "plan = '' ",
1969                    ),
1970   });
1971
1972   foreach my $part_pkg (@part_pkg) {
1973
1974     unless ( $part_pkg->plan ) {
1975       $part_pkg->plan('flat');
1976     }
1977
1978     $part_pkg->replace;
1979
1980   }
1981
1982   # Convert RADIUS accounting usage metrics from megabytes to gigabytes
1983   # (FS RT#28105)
1984   my $upgrade = 'part_pkg_gigabyte_usage';
1985   if (!FS::upgrade_journal->is_done($upgrade)) {
1986     foreach my $part_pkg (qsearch('part_pkg',
1987                                   { plan => 'sqlradacct_hour' })
1988                          ){
1989
1990       my $pkgpart = $part_pkg->pkgpart;
1991
1992       foreach my $opt (qsearch('part_pkg_option',
1993                                { 'optionname'  => { op => 'LIKE',
1994                                                     value => 'recur_included_%',
1995                                                   },
1996                                  pkgpart => $pkgpart,
1997                                })){
1998
1999         next if $opt->optionname eq 'recur_included_hours'; # unfortunately named field
2000         next if $opt->optionvalue == 0;
2001
2002         $opt->optionvalue($opt->optionvalue / 1024);
2003
2004         my $error = $opt->replace;
2005         die $error if $error;
2006       }
2007
2008       foreach my $opt (qsearch('part_pkg_option',
2009                                { 'optionname'  => { op => 'LIKE',
2010                                                     value => 'recur_%_charge',
2011                                                   },
2012                                  pkgpart => $pkgpart,
2013                                })){
2014         $opt->optionvalue($opt->optionvalue * 1024);
2015
2016         my $error = $opt->replace;
2017         die $error if $error;
2018       }
2019
2020     }
2021     FS::upgrade_journal->set_done($upgrade);
2022   }
2023
2024   # the rest can be done asynchronously
2025 }
2026
2027 sub queueable_upgrade {
2028   # now upgrade to the explicit custom flag
2029
2030   my $search = FS::Cursor->new({
2031     'table'     => 'part_pkg',
2032     'hashref'   => { disabled => 'Y', custom => '' },
2033     'extra_sql' => "AND comment LIKE '(CUSTOM) %'",
2034   });
2035   my $dbh = dbh;
2036
2037   while (my $part_pkg = $search->fetch) {
2038     my $new = new FS::part_pkg { $part_pkg->hash };
2039     $new->custom('Y');
2040     my $comment = $part_pkg->comment;
2041     $comment =~ s/^\(CUSTOM\) //;
2042     $comment = '(none)' unless $comment =~ /\S/;
2043     $new->comment($comment);
2044
2045     my $pkg_svc = { map { $_->svcpart => $_->quantity } $part_pkg->pkg_svc };
2046     my $primary = $part_pkg->svcpart;
2047     my $options = { $part_pkg->options };
2048
2049     my $error = $new->replace( $part_pkg,
2050                                'pkg_svc'     => $pkg_svc,
2051                                'primary_svc' => $primary,
2052                                'options'     => $options,
2053                              );
2054     if ($error) {
2055       warn "pkgpart#".$part_pkg->pkgpart.": $error\n";
2056       $dbh->rollback;
2057     } else {
2058       $dbh->commit;
2059     }
2060   }
2061
2062   # set family_pkgpart on any packages that don't have it
2063   $search = FS::Cursor->new('part_pkg', { 'family_pkgpart' => '' });
2064   while (my $part_pkg = $search->fetch) {
2065     $part_pkg->set('family_pkgpart' => $part_pkg->pkgpart);
2066     my $error = $part_pkg->SUPER::replace;
2067     if ($error) {
2068       warn "pkgpart#".$part_pkg->pkgpart.": $error\n";
2069       $dbh->rollback;
2070     } else {
2071       $dbh->commit;
2072     }
2073   }
2074
2075   my @part_pkg_option = qsearch('part_pkg_option',
2076     { 'optionname'  => 'unused_credit',
2077       'optionvalue' => 1,
2078     });
2079   foreach my $old_opt (@part_pkg_option) {
2080     my $pkgpart = $old_opt->pkgpart;
2081     my $error = $old_opt->delete;
2082     die $error if $error;
2083
2084     foreach (qw(unused_credit_cancel unused_credit_change)) {
2085       my $new_opt = new FS::part_pkg_option {
2086         'pkgpart'     => $pkgpart,
2087         'optionname'  => $_,
2088         'optionvalue' => 1,
2089       };
2090       $error = $new_opt->insert;
2091       die $error if $error;
2092     }
2093   }
2094
2095   # migrate use_disposition_taqua and use_disposition to disposition_in
2096   @part_pkg_option = qsearch('part_pkg_option',
2097     { 'optionname'  => { op => 'LIKE',
2098                          value => 'use_disposition%',
2099                        },
2100       'optionvalue' => 1,
2101     });
2102   my %newopts = map { $_->pkgpart => $_ } 
2103     qsearch('part_pkg_option',  { 'optionname'  => 'disposition_in', } );
2104   foreach my $old_opt (@part_pkg_option) {
2105         my $pkgpart = $old_opt->pkgpart;
2106         my $newval = $old_opt->optionname eq 'use_disposition_taqua' ? '100' 
2107                                                                   : 'ANSWERED';
2108         my $error = $old_opt->delete;
2109         die $error if $error;
2110
2111         if ( exists($newopts{$pkgpart}) ) {
2112             my $opt = $newopts{$pkgpart};
2113             $opt->optionvalue($opt->optionvalue.",$newval");
2114             $error = $opt->replace;
2115             die $error if $error;
2116         } else {
2117             my $new_opt = new FS::part_pkg_option {
2118                 'pkgpart'     => $pkgpart,
2119                 'optionname'  => 'disposition_in',
2120                 'optionvalue' => $newval,
2121               };
2122               $error = $new_opt->insert;
2123               die $error if $error;
2124               $newopts{$pkgpart} = $new_opt;
2125         }
2126   }
2127
2128   # set any package with FCC voice lines to the "VoIP with broadband" category
2129   # for backward compatibility
2130   #
2131   # recover from a bad upgrade bug
2132   my $upgrade = 'part_pkg_fcc_voip_class_FIX';
2133   if (!FS::upgrade_journal->is_done($upgrade)) {
2134     my $bad_upgrade = qsearchs('upgrade_journal', 
2135       { upgrade => 'part_pkg_fcc_voip_class' }
2136     );
2137     if ( $bad_upgrade ) {
2138       my $where = 'WHERE history_date <= '.$bad_upgrade->_date.
2139                   ' AND  history_date >  '.($bad_upgrade->_date - 3600);
2140       my @h_part_pkg_option = map { FS::part_pkg_option->new($_->hashref) }
2141         qsearch({
2142           'select'    => '*',
2143           'table'     => 'h_part_pkg_option',
2144           'hashref'   => {},
2145           'extra_sql' => "$where AND history_action = 'delete'",
2146           'order_by'  => 'ORDER BY history_date ASC',
2147         });
2148       my @h_pkg_svc = map { FS::pkg_svc->new($_->hashref) }
2149         qsearch({
2150           'select'    => '*',
2151           'table'     => 'h_pkg_svc',
2152           'hashref'   => {},
2153           'extra_sql' => "$where AND history_action = 'replace_old'",
2154           'order_by'  => 'ORDER BY history_date ASC',
2155         });
2156       my %opt;
2157       foreach my $deleted (@h_part_pkg_option, @h_pkg_svc) {
2158         my $pkgpart ||= $deleted->pkgpart;
2159         $opt{$pkgpart} ||= {
2160           options => {},
2161           pkg_svc => {},
2162           primary_svc => '',
2163           hidden_svc => {},
2164         };
2165         if ( $deleted->isa('FS::part_pkg_option') ) {
2166           $opt{$pkgpart}{options}{ $deleted->optionname } = $deleted->optionvalue;
2167         } else { # pkg_svc
2168           my $svcpart = $deleted->svcpart;
2169           $opt{$pkgpart}{pkg_svc}{$svcpart} = $deleted->quantity;
2170           $opt{$pkgpart}{hidden_svc}{$svcpart} ||= $deleted->hidden;
2171           $opt{$pkgpart}{primary_svc} = $svcpart if $deleted->primary_svc;
2172         }
2173       }
2174       foreach my $pkgpart (keys %opt) {
2175         my $part_pkg = FS::part_pkg->by_key($pkgpart);
2176         my $error = $part_pkg->replace( $part_pkg->replace_old, $opt{$pkgpart} );
2177         if ( $error ) {
2178           die "error recovering damaged pkgpart $pkgpart:\n$error\n";
2179         }
2180       }
2181     } # $bad_upgrade exists
2182     else { # do the original upgrade, but correctly this time
2183       my @part_pkg = qsearch('part_pkg', {
2184           fcc_ds0s        => { op => '>', value => 0 },
2185           fcc_voip_class  => ''
2186       });
2187       foreach my $part_pkg (@part_pkg) {
2188         $part_pkg->set(fcc_voip_class => 2);
2189         my @pkg_svc = $part_pkg->pkg_svc;
2190         my %quantity = map {$_->svcpart, $_->quantity} @pkg_svc;
2191         my %hidden   = map {$_->svcpart, $_->hidden  } @pkg_svc;
2192         my $error = $part_pkg->replace(
2193           $part_pkg->replace_old,
2194           options     => { $part_pkg->options },
2195           pkg_svc     => \%quantity,
2196           hidden_svc  => \%hidden,
2197           primary_svc => ($part_pkg->svcpart || ''),
2198         );
2199         die $error if $error;
2200       }
2201     }
2202     FS::upgrade_journal->set_done($upgrade);
2203   }
2204
2205 }
2206
2207 =item curuser_pkgs_sql
2208
2209 Returns an SQL fragment for searching for packages the current user can
2210 use, either via part_pkg.agentnum directly, or via agent type (see
2211 L<FS::type_pkgs>).
2212
2213 =cut
2214
2215 sub curuser_pkgs_sql {
2216   my $class = shift;
2217
2218   $class->_pkgs_sql( $FS::CurrentUser::CurrentUser->agentnums );
2219
2220 }
2221
2222 =item agent_pkgs_sql AGENT | AGENTNUM, ...
2223
2224 Returns an SQL fragment for searching for packages the provided agent or agents
2225 can use, either via part_pkg.agentnum directly, or via agent type (see
2226 L<FS::type_pkgs>).
2227
2228 =cut
2229
2230 sub agent_pkgs_sql {
2231   my $class = shift;  #i'm a class method, not a sub (the question is... why??)
2232   my @agentnums = map { ref($_) ? $_->agentnum : $_ } @_;
2233
2234   $class->_pkgs_sql(@agentnums); #is this why
2235
2236 }
2237
2238 sub _pkgs_sql {
2239   my( $class, @agentnums ) = @_;
2240   my $agentnums = join(',', @agentnums);
2241
2242   "
2243     (
2244       ( agentnum IS NOT NULL AND agentnum IN ($agentnums) )
2245       OR ( agentnum IS NULL
2246            AND EXISTS ( SELECT 1
2247                           FROM type_pkgs
2248                             LEFT JOIN agent_type USING ( typenum )
2249                             LEFT JOIN agent AS typeagent USING ( typenum )
2250                           WHERE type_pkgs.pkgpart = part_pkg.pkgpart
2251                             AND typeagent.agentnum IN ($agentnums)
2252                       )
2253          )
2254     )
2255   ";
2256
2257 }
2258
2259 =back
2260
2261 =head1 SUBROUTINES
2262
2263 =over 4
2264
2265 =item plan_info
2266
2267 =cut
2268
2269 #false laziness w/part_export & cdr
2270 my %info;
2271 foreach my $INC ( @INC ) {
2272   warn "globbing $INC/FS/part_pkg/[a-z]*.pm\n" if $DEBUG;
2273   foreach my $file ( glob("$INC/FS/part_pkg/[a-z]*.pm") ) {
2274     warn "attempting to load plan info from $file\n" if $DEBUG;
2275     $file =~ /\/(\w+)\.pm$/ or do {
2276       warn "unrecognized file in $INC/FS/part_pkg/: $file\n";
2277       next;
2278     };
2279     my $mod = $1;
2280     my $info = eval "use FS::part_pkg::$mod; ".
2281                     "\\%FS::part_pkg::$mod\::info;";
2282     if ( $@ ) {
2283       die "error using FS::part_pkg::$mod (skipping): $@\n" if $@;
2284       next;
2285     }
2286     unless ( keys %$info ) {
2287       warn "no %info hash found in FS::part_pkg::$mod, skipping\n";
2288       next;
2289     }
2290     warn "got plan info from FS::part_pkg::$mod: $info\n" if $DEBUG;
2291     #if ( exists($info->{'disabled'}) && $info->{'disabled'} ) {
2292     #  warn "skipping disabled plan FS::part_pkg::$mod" if $DEBUG;
2293     #  next;
2294     #}
2295     $info{$mod} = $info;
2296     $info->{'weight'} ||= 0; # quiet warnings
2297   }
2298 }
2299
2300 # copy one level deep to allow replacement of fields and fieldorder
2301 tie %plans, 'Tie::IxHash',
2302   map  { my %infohash = %{ $info{$_} }; 
2303           $_ => \%infohash }
2304   sort { $info{$a}->{'weight'} <=> $info{$b}->{'weight'} }
2305   keys %info;
2306
2307 # inheritance of plan options
2308 foreach my $name (keys(%info)) {
2309   if (exists($info{$name}->{'disabled'}) and $info{$name}->{'disabled'}) {
2310     warn "skipping disabled plan FS::part_pkg::$name" if $DEBUG;
2311     delete $plans{$name};
2312     next;
2313   }
2314   my $parents = $info{$name}->{'inherit_fields'} || [];
2315   my (%fields, %field_exists, @fieldorder);
2316   foreach my $parent ($name, @$parents) {
2317     if ( !exists($info{$parent}) ) {
2318       warn "$name tried to inherit from nonexistent '$parent'\n";
2319       next;
2320     }
2321     %fields = ( # avoid replacing existing fields
2322       %{ $info{$parent}->{'fields'} || {} },
2323       %fields
2324     );
2325     foreach (@{ $info{$parent}->{'fieldorder'} || [] }) {
2326       # avoid duplicates
2327       next if $field_exists{$_};
2328       $field_exists{$_} = 1;
2329       # allow inheritors to remove inherited fields from the fieldorder
2330       push @fieldorder, $_ if !exists($fields{$_}) or
2331                               !exists($fields{$_}->{'disabled'});
2332     }
2333   }
2334   $plans{$name}->{'fields'} = \%fields;
2335   $plans{$name}->{'fieldorder'} = \@fieldorder;
2336 }
2337
2338 sub plan_info {
2339   \%plans;
2340 }
2341
2342
2343 =back
2344
2345 =head1 NEW PLAN CLASSES
2346
2347 A module should be added in FS/FS/part_pkg/  Eventually, an example may be
2348 found in eg/plan_template.pm.  Until then, it is suggested that you use the
2349 other modules in FS/FS/part_pkg/ as a guide.
2350
2351 =head1 BUGS
2352
2353 The delete method is unimplemented.
2354
2355 setup and recur semantics are not yet defined (and are implemented in
2356 FS::cust_bill.  hmm.).  now they're deprecated and need to go.
2357
2358 plandata should go
2359
2360 part_pkg_taxrate is Pg specific
2361
2362 replace should be smarter about managing the related tables (options, pkg_svc)
2363
2364 =head1 SEE ALSO
2365
2366 L<FS::Record>, L<FS::cust_pkg>, L<FS::type_pkgs>, L<FS::pkg_svc>, L<Safe>.
2367 schema.html from the base documentation.
2368
2369 =cut
2370
2371 1;
2372