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