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