the taxproductnum is ALWAYS one of the filtering conditions
[freeside.git] / FS / FS / part_pkg.pm
index 536cd89..e53d7b8 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 use vars qw( @ISA %plans $DEBUG );
 use Carp qw(carp cluck confess);
 use Scalar::Util qw( blessed );
+use Time::Local qw( timelocal_nocheck );
 use Tie::IxHash;
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs dbh dbdef );
@@ -144,6 +145,10 @@ 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.
 
+If I<tax_overrides> is set to a hashref with usage classes as keys and comma
+separated tax class numbers as values, appropriate FS::part_pkg_taxoverride
+records will be inserted.
+
 If I<options> is set to a hashref of options, appropriate FS::part_pkg_option
 records will be inserted.
 
@@ -191,6 +196,22 @@ sub insert {
     }
   }
 
+  warn "  inserting part_pkg_taxoverride records" if $DEBUG;
+  my %overrides = %{ $options{'tax_overrides'} || {} };
+  foreach my $usage_class ( keys %overrides ) {
+    my @overrides = (grep "$_", split (',', $overrides{$usage_class}) );
+    my $error = $self->process_m2m (
+                  'link_table'   => 'part_pkg_taxoverride',
+                  'target_table' => 'tax_class',
+                  'hashref'      => { 'usage_class' => $usage_class },
+                  'params'       => \@overrides,
+                );
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
   warn "  inserting pkg_svc records" if $DEBUG;
   my $pkg_svc = $options{'pkg_svc'} || {};
   foreach my $part_svc ( qsearch('part_svc', {} ) ) {
@@ -411,6 +432,12 @@ sub check {
     $self->freq($1);
   }
 
+  my @null_agentnum_right = ( 'Edit global package definitions' );
+  push @null_agentnum_right, 'One-time charge'
+    if $self->freq =~ /^0/;
+  push @null_agentnum_right, 'Customize customer package'
+    if $self->disabled eq 'Y'; #good enough
+
   my $error = $self->ut_numbern('pkgpart')
     || $self->ut_text('pkg')
     || $self->ut_text('comment')
@@ -427,7 +454,7 @@ sub check {
                               'part_pkg_taxproduct',
                               'taxproductnum'
                              )
-    || $self->ut_agentnum_acl('agentnum', 'Edit global package definitions')
+    || $self->ut_agentnum_acl('agentnum', \@null_agentnum_right)
     || $self->SUPER::check
   ;
   return $error if $error;
@@ -579,15 +606,29 @@ sub svcpart {
   my $svcdb = scalar(@_) ? shift : '';
   my @svcdb_pkg_svc =
     grep { ( $svcdb eq $_->part_svc->svcdb || !$svcdb ) } $self->pkg_svc;
-  my @pkg_svc = ();
-  @pkg_svc = grep { $_->primary_svc =~ /^Y/i } @svcdb_pkg_svc
-    if dbdef->table('pkg_svc')->column('primary_svc');
+  my @pkg_svc = grep { $_->primary_svc =~ /^Y/i } @svcdb_pkg_svc;
   @pkg_svc = grep {$_->quantity == 1 } @svcdb_pkg_svc
     unless @pkg_svc;
   return '' if scalar(@pkg_svc) != 1;
   $pkg_svc[0]->svcpart;
 }
 
+=item svcpart_unique_svcdb SVCDB
+
+Returns the svcpart of the a service definition (see L<FS::part_svc>) matching
+SVCDB associated with this package definition (see L<FS::pkg_svc>).  Returns
+false if there not a primary service definition for SVCDB or there are multiple
+service definitions for SVCDB.
+
+=cut
+
+sub svcpart_unique_svcdb {
+  my( $self, $svcdb ) = @_;
+  my @svcdb_pkg_svc = grep { ( $svcdb eq $_->part_svc->svcdb ) } $self->pkg_svc;
+  return '' if scalar(@svcdb_pkg_svc) != 1;
+  $svcdb_pkg_svc[0]->svcpart;
+}
+
 =item payby
 
 Returns a list of the acceptable payment types for this package.  Eventually
@@ -694,6 +735,41 @@ sub freq_pretty {
   }
 }
 
+=item add_freq TIMESTAMP
+
+Adds the frequency of this package to the provided timestamp and returns
+the resulting timestamp, or -1 if the frequency of this package could not be
+parsed (shouldn't happen).
+
+=cut
+
+sub add_freq {
+  my( $self, $date ) = @_;
+  my $freq = $self->freq;
+
+  #change this bit to use Date::Manip? CAREFUL with timezones (see
+  # mailing list archive)
+  my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($date) )[0,1,2,3,4,5];
+
+  if ( $self->freq =~ /^\d+$/ ) {
+    $mon += $self->freq;
+    until ( $mon < 12 ) { $mon -= 12; $year++; }
+  } elsif ( $self->freq =~ /^(\d+)w$/ ) {
+    my $weeks = $1;
+    $mday += $weeks * 7;
+  } elsif ( $self->freq =~ /^(\d+)d$/ ) {
+    my $days = $1;
+    $mday += $days;
+  } elsif ( $self->freq =~ /^(\d+)h$/ ) {
+    my $hours = $1;
+    $hour += $hours;
+  } else {
+    return -1;
+  }
+
+  timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year);
+}
+
 =item plandata
 
 For backwards compatibility, returns the plandata field as well as all options
@@ -762,7 +838,7 @@ sub option {
 
 =item bill_part_pkg_link
 
-Returns the associated part_pkg_link records (see L<FS::part_pkg_link).
+Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
 
 =cut
 
@@ -772,6 +848,8 @@ sub bill_part_pkg_link {
 
 =item svc_part_pkg_link
 
+Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
+
 =cut
 
 sub svc_part_pkg_link {
@@ -799,19 +877,86 @@ sub _self_and_linked {
   );
 }
 
-=item part_pkg_taxoverride
+=item part_pkg_taxoverride [ CLASS ]
 
 Returns all associated FS::part_pkg_taxoverride objects (see
-L<FS::part_pkg_taxoverride>).
+L<FS::part_pkg_taxoverride>).  Limits the returned set to those
+of class CLASS if defined.  Class may be one of 'setup', 'recur',
+the empty string (default), or a usage class number (see L<FS::usage_class>).
+When a class is specified, the empty string class (default) is returned
+if no more specific values exist.
 
 =cut
 
 sub part_pkg_taxoverride {
   my $self = shift;
-  qsearch('part_pkg_taxoverride', { 'pkgpart' => $self->pkgpart } );
+  my $class = shift;
+
+  my $hashref = { 'pkgpart' => $self->pkgpart };
+  $hashref->{'usage_class'} = $class if defined($class);
+  my @overrides = qsearch('part_pkg_taxoverride', $hashref );
+
+  unless ( scalar(@overrides) || !defined($class) || !$class ){
+    $hashref->{'usage_class'} = '';
+    @overrides = qsearch('part_pkg_taxoverride', $hashref );
+  }
+
+  @overrides;
+}
+
+=item has_taxproduct
+
+Returns true if this package has any taxproduct associated with it.  
+
+=cut
+
+sub has_taxproduct {
+  my $self = shift;
+
+  $self->taxproductnum ||
+  scalar( grep { $_ =~/^usage_taxproductnum_/ && $self->option($_) } 
+          keys %{ {$self->options} }
+  )
+
+}
+
+
+=item taxproduct [ CLASS ]
+
+Returns the associated tax product for this package definition (see
+L<FS::part_pkg_taxproduct>).  CLASS may be one of 'setup', 'recur' or
+the usage classnum (see L<FS::usage_class>).  Returns the default
+tax product for this record if the more specific CLASS value does
+not exist.
+
+=cut
+
+sub taxproduct {
+  my $self = shift;
+  my $class = shift;
+
+  my $part_pkg_taxproduct;
+
+  my $taxproductnum = $self->taxproductnum;
+  if ($class) { 
+    my $class_taxproductnum = $self->option("usage_taxproductnum_$class", 1);
+    $taxproductnum = $class_taxproductnum
+      if $class_taxproductnum
+  }
+  
+  $part_pkg_taxproduct =
+    qsearchs( 'part_pkg_taxproduct', { 'taxproductnum' => $taxproductnum } );
+
+  unless ($part_pkg_taxproduct || $taxproductnum eq $self->taxproductnum ) {
+    $taxproductnum = $self->taxproductnum;
+    $part_pkg_taxproduct =
+      qsearchs( 'part_pkg_taxproduct', { 'taxproductnum' => $taxproductnum } );
+  }
+
+  $part_pkg_taxproduct;
 }
 
-=item taxproduct_description
+=item taxproduct_description [ CLASS ]
 
 Returns the description of the associated tax product for this package
 definition (see L<FS::part_pkg_taxproduct>).
@@ -820,30 +965,29 @@ definition (see L<FS::part_pkg_taxproduct>).
 
 sub taxproduct_description {
   my $self = shift;
-  my $part_pkg_taxproduct =
-    qsearchs( 'part_pkg_taxproduct',
-              { 'taxproductnum' => $self->taxproductnum }
-            );
+  my $part_pkg_taxproduct = $self->taxproduct(@_);
   $part_pkg_taxproduct ? $part_pkg_taxproduct->description : '';
 }
 
-=item part_pkg_taxrate DATA_PROVIDER, GEOCODE
+=item part_pkg_taxrate DATA_PROVIDER, GEOCODE, [ CLASS ]
 
 Returns the package to taxrate m2m records for this package in the location
-specified by GEOCODE (see L<FS::part_pkg_taxrate> and ).
+specified by GEOCODE (see L<FS::part_pkg_taxrate>) and usage class CLASS.
+CLASS may be one of 'setup', 'recur', or one of the usage classes numbers
+(see L<FS::usage_class>).
 
 =cut
 
 sub _expand_cch_taxproductnum {
   my $self = shift;
-  my $part_pkg_taxproduct =
-    qsearchs( 'part_pkg_taxproduct',
-              { 'taxproductnum' => $self->taxproductnum }
-            );
+  my $class = shift;
+  my $part_pkg_taxproduct = $self->taxproduct($class);
+
   my ($a,$b,$c,$d) = ( $part_pkg_taxproduct
                          ? ( split ':', $part_pkg_taxproduct->taxproduct )
                          : ()
                      );
+  $a = '' unless $a; $b = '' unless $b; $c = '' unless $c; $d = '' unless $d;
   my $extra_sql = "AND ( taxproduct = '$a:$b:$c:$d'
                       OR taxproduct = '$a:$b:$c:'
                       OR taxproduct = '$a:$b:".":$d'
@@ -857,7 +1001,7 @@ sub _expand_cch_taxproductnum {
 
 sub part_pkg_taxrate {
   my $self = shift;
-  my ($data_vendor, $geocode) = @_;
+  my ($data_vendor, $geocode, $class) = @_;
 
   my $dbh = dbh;
   my $extra_sql = 'WHERE part_pkg_taxproduct.data_vendor = '.
@@ -870,9 +1014,14 @@ sub part_pkg_taxrate {
         ).
     ')';
   # much more CCH oddness in m2m -- this is kludgy
-  $extra_sql .= ' AND ('.
-    join(' OR ', map{ "taxproductnum = $_" } $self->_expand_cch_taxproductnum).
-    ')';
+  my @tpnums = $self->_expand_cch_taxproductnum($class);
+  if (scalar(@tpnums)) {
+    $extra_sql .= ' AND ('.
+                            join(' OR ', map{ "taxproductnum = $_" } @tpnums ).
+                       ')';
+  } else {
+    $extra_sql .= ' AND ( 0 = 1 )';
+  }
 
   my $addl_from = 'LEFT JOIN part_pkg_taxproduct USING ( taxproductnum )';
   my $order_by = 'ORDER BY taxclassnum, length(geocode) desc, length(taxproduct) desc';