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