*** empty log message ***
[freeside.git] / FS / FS / part_pkg.pm
index 9392f4b..a5e3c21 100644 (file)
@@ -2,7 +2,7 @@ package FS::part_pkg;
 
 use strict;
 use vars qw( @ISA %freq %plans $DEBUG );
 
 use strict;
 use vars qw( @ISA %freq %plans $DEBUG );
-use Carp qw(carp cluck);
+use Carp qw(carp cluck confess);
 use Tie::IxHash;
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs dbh dbdef );
 use Tie::IxHash;
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs dbh dbdef );
@@ -13,7 +13,9 @@ use FS::agent_type;
 use FS::type_pkgs;
 use FS::part_pkg_option;
 
 use FS::type_pkgs;
 use FS::part_pkg_option;
 
-@ISA = qw( FS::Record );
+@ISA = qw( FS::Record ); # FS::option_Common ); # this can use option_Common
+                                                # when all the plandata bs is
+                                                # gone
 
 $DEBUG = 0;
 
 
 $DEBUG = 0;
 
@@ -116,8 +118,8 @@ sub clone {
 Adds this package definition to the database.  If there is an error,
 returns the error, otherwise returns false.
 
 Adds this package definition to the database.  If there is an error,
 returns the error, otherwise returns false.
 
-Currently available options are: I<pkg_svc>, I<primary_svc>, I<cust_pkg> and
-I<custnum_ref>.
+Currently available options are: I<pkg_svc>, I<primary_svc>, I<cust_pkg>
+I<custnum_ref> and I<options>.
 
 If I<pkg_svc> is set to a hashref with svcparts as keys and quantities as
 values, appropriate FS::pkg_svc records will be inserted.
 
 If I<pkg_svc> is set to a hashref with svcparts as keys and quantities as
 values, appropriate FS::pkg_svc records will be inserted.
@@ -131,12 +133,17 @@ record itself), the object will be updated to point to this package definition.
 In conjunction with I<cust_pkg>, if I<custnum_ref> is set to a scalar reference,
 the scalar will be updated with the custnum value from the cust_pkg record.
 
 In conjunction with I<cust_pkg>, if I<custnum_ref> is set to a scalar reference,
 the scalar will be updated with the custnum value from the cust_pkg record.
 
+If I<options> is set to a hashref of options, appropriate FS::part_pkg_option
+records will be inserted.
+
 =cut
 
 sub insert {
   my $self = shift;
   my %options = @_;
 =cut
 
 sub insert {
   my $self = shift;
   my %options = @_;
-  warn "FS::part_pkg::insert called on $self with options %options" if $DEBUG;
+  warn "FS::part_pkg::insert called on $self with options ".
+       join(', ', map "$_=>$options{$_}", keys %options)
+    if $DEBUG;
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
@@ -161,7 +168,8 @@ sub insert {
   }
 
   if ( $plandata ) {
   }
 
   if ( $plandata ) {
-  warn "  inserting part_pkg_option records for plandata" if $DEBUG;
+
+    warn "  inserting part_pkg_option records for plandata" if $DEBUG;
     foreach my $part_pkg_option ( 
       map { /^(\w+)=(.*)$/ or do { $dbh->rollback if $oldAutoCommit;
                                    return "illegal plandata: $plandata";
     foreach my $part_pkg_option ( 
       map { /^(\w+)=(.*)$/ or do { $dbh->rollback if $oldAutoCommit;
                                    return "illegal plandata: $plandata";
@@ -180,6 +188,27 @@ sub insert {
         return $error;
       }
     }
         return $error;
       }
     }
+
+  } elsif ( $options{'options'} ) {
+
+    warn "  inserting part_pkg_option records for options hashref" if $DEBUG;
+    foreach my $optionname ( keys %{$options{'options'}} ) {
+
+      my $part_pkg_option =
+        new FS::part_pkg_option {
+          'pkgpart'     => $self->pkgpart,
+          'optionname'  => $optionname,
+          'optionvalue' => $options{'options'}->{$optionname},
+        };
+
+      my $error = $part_pkg_option->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+
+    }
+
   }
 
   my $conf = new FS::Conf;
   }
 
   my $conf = new FS::Conf;
@@ -203,7 +232,10 @@ sub insert {
   my $pkg_svc = $options{'pkg_svc'} || {};
   foreach my $part_svc ( qsearch('part_svc', {} ) ) {
     my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
   my $pkg_svc = $options{'pkg_svc'} || {};
   foreach my $part_svc ( qsearch('part_svc', {} ) ) {
     my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
-    my $primary_svc = $options{'primary_svc'} == $part_svc->svcpart ? 'Y' : '';
+    my $primary_svc =
+      ( $options{'primary_svc'} && $options{'primary_svc'}==$part_svc->svcpart )
+        ? 'Y'
+        : '';
 
     my $pkg_svc = new FS::pkg_svc( {
       'pkgpart'     => $self->pkgpart,
 
     my $pkg_svc = new FS::pkg_svc( {
       'pkgpart'     => $self->pkgpart,
@@ -345,6 +377,7 @@ sub replace {
     next unless $old_quantity != $quantity || $old_primary_svc ne $primary_svc;
   
     my $new_pkg_svc = new FS::pkg_svc( {
     next unless $old_quantity != $quantity || $old_primary_svc ne $primary_svc;
   
     my $new_pkg_svc = new FS::pkg_svc( {
+      'pkgsvcnum'   => ( $old_pkg_svc ? $old_pkg_svc->pkgsvcnum : '' ),
       'pkgpart'     => $new->pkgpart,
       'svcpart'     => $part_svc->svcpart,
       'quantity'    => $quantity, 
       'pkgpart'     => $new->pkgpart,
       'svcpart'     => $part_svc->svcpart,
       'quantity'    => $quantity, 
@@ -387,7 +420,7 @@ sub check {
     my $error = $self->ut_number('freq');
     return $error if $error;
   } else {
     my $error = $self->ut_number('freq');
     return $error if $error;
   } else {
-    $self->freq =~ /^(\d+[dw]?)$/
+    $self->freq =~ /^(\d+[hdw]?)$/
       or return "Illegal or empty freq: ". $self->freq;
     $self->freq($1);
   }
       or return "Illegal or empty freq: ". $self->freq;
     $self->freq($1);
   }
@@ -405,9 +438,20 @@ sub check {
   ;
   return $error if $error;
 
   ;
   return $error if $error;
 
+  if ( $self->classnum !~ /^$/ ) {
+    my $error = $self->ut_foreign_key('classnum', 'pkg_class', 'classnum');
+    return $error if $error;
+  } else {
+    $self->classnum('');
+  }
+
   return 'Unknown plan '. $self->plan
     unless exists($plans{$self->plan});
 
   return 'Unknown plan '. $self->plan
     unless exists($plans{$self->plan});
 
+  my $conf = new FS::Conf;
+  return 'Taxclass is required'
+    if ! $self->taxclass && $conf->exists('require_taxclasses');
+
   '';
 }
 
   '';
 }
 
@@ -502,6 +546,7 @@ Returns an english representation of the I<freq> field, such as "monthly",
 
 tie %freq, 'Tie::IxHash', 
   '0'  => '(no recurring fee)',
 
 tie %freq, 'Tie::IxHash', 
   '0'  => '(no recurring fee)',
+  '1h' => 'hourly',
   '1d' => 'daily',
   '1w' => 'weekly',
   '2w' => 'biweekly (every 2 weeks)',
   '1d' => 'daily',
   '1w' => 'weekly',
   '2w' => 'biweekly (every 2 weeks)',
@@ -511,6 +556,10 @@ tie %freq, 'Tie::IxHash',
   '6'  => 'semiannually (every 6 months)',
   '12' => 'annually',
   '24' => 'biannually (every 2 years)',
   '6'  => 'semiannually (every 6 months)',
   '12' => 'annually',
   '24' => 'biannually (every 2 years)',
+  '36' => 'triannually (every 3 years)',
+  '48' => '(every 4 years)',
+  '60' => '(every 5 years)',
+  '120' => '(every 10 years)',
 ;
 
 sub freq_pretty {
 ;
 
 sub freq_pretty {
@@ -520,8 +569,8 @@ sub freq_pretty {
     $freq{$freq};
   } else {
     my $interval = 'month';
     $freq{$freq};
   } else {
     my $interval = 'month';
-    if ( $freq =~ /^(\d+)([dw])$/ ) {
-      my %interval = ( 'd'=>'day', 'w'=>'week' );
+    if ( $freq =~ /^(\d+)([hdw])$/ ) {
+      my %interval = ( 'h' => 'hour', 'd'=>'day', 'w'=>'week' );
       $interval = $interval{$2};
     }
     if ( $1 == 1 ) {
       $interval = $interval{$2};
     }
     if ( $1 == 1 ) {
@@ -582,7 +631,7 @@ Returns the option value for the given name, or the empty string.
 =cut
 
 sub option {
 =cut
 
 sub option {
-  my( $self, $opt ) = @_;
+  my( $self, $opt, $ornull ) = @_;
   my $part_pkg_option =
     qsearchs('part_pkg_option', {
       pkgpart    => $self->pkgpart,
   my $part_pkg_option =
     qsearchs('part_pkg_option', {
       pkgpart    => $self->pkgpart,
@@ -592,7 +641,8 @@ sub option {
   my %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); }
                      split("\n", $self->get('plandata') );
   return $plandata{$opt} if exists $plandata{$opt};
   my %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); }
                      split("\n", $self->get('plandata') );
   return $plandata{$opt} if exists $plandata{$opt};
-  cluck "Package definition option $opt not found in options or plandata!\n";
+  cluck "Package definition option $opt not found in options or plandata!\n"
+    unless $ornull;
   '';
 }
 
   '';
 }
 
@@ -607,9 +657,16 @@ on how to create new price plans, but until then, see L</NEW PLAN CLASSES>.
 sub _rebless {
   my $self = shift;
   my $plan = $self->plan;
 sub _rebless {
   my $self = shift;
   my $plan = $self->plan;
+  unless ( $plan ) {
+    confess "no price plan found for pkgpart ". $self->pkgpart. "\n"
+      if $DEBUG;
+    return $self;
+  }
+  return $self if ref($self) =~ /::$plan$/; #already blessed into plan subclass
   my $class = ref($self). "::$plan";
   my $class = ref($self). "::$plan";
+  warn "reblessing $self into $class" if $DEBUG;
   eval "use $class;";
   eval "use $class;";
-  #die $@ if $@;
+  die $@ if $@;
   bless($self, $class) unless $@;
   $self;
 }
   bless($self, $class) unless $@;
   $self;
 }
@@ -644,6 +701,11 @@ sub _calc_eval {
   $value;
 }
 
   $value;
 }
 
+#fallback that return 0 for old legacy packages with no plan
+
+sub calc_remain { 0; }
+sub calc_cancel { 0; }
+
 =back
 
 =head1 SUBROUTINES
 =back
 
 =head1 SUBROUTINES
@@ -656,6 +718,7 @@ sub _calc_eval {
 
 my %info;
 foreach my $INC ( @INC ) {
 
 my %info;
 foreach my $INC ( @INC ) {
+  warn "globbing $INC/FS/part_pkg/*.pm\n" if $DEBUG;
   foreach my $file ( glob("$INC/FS/part_pkg/*.pm") ) {
     warn "attempting to load plan info from $file\n" if $DEBUG;
     $file =~ /\/(\w+)\.pm$/ or do {
   foreach my $file ( glob("$INC/FS/part_pkg/*.pm") ) {
     warn "attempting to load plan info from $file\n" if $DEBUG;
     $file =~ /\/(\w+)\.pm$/ or do {
@@ -706,6 +769,8 @@ The delete method is unimplemented.
 setup and recur semantics are not yet defined (and are implemented in
 FS::cust_bill.  hmm.).
 
 setup and recur semantics are not yet defined (and are implemented in
 FS::cust_bill.  hmm.).
 
+plandata should go
+
 =head1 SEE ALSO
 
 L<FS::Record>, L<FS::cust_pkg>, L<FS::type_pkgs>, L<FS::pkg_svc>, L<Safe>.
 =head1 SEE ALSO
 
 L<FS::Record>, L<FS::cust_pkg>, L<FS::type_pkgs>, L<FS::pkg_svc>, L<Safe>.