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