package FS::part_pkg;
use strict;
-use vars qw( @ISA %plans $DEBUG );
+use vars qw( @ISA %plans $DEBUG $setup_hack );
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 );
@ISA = qw( FS::m2m_Common FS::option_Common );
$DEBUG = 0;
+$setup_hack = 0;
=head1 NAME
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.
}
}
+ 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', {} ) ) {
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 =
+ ( defined($options->{'primary_svc'})
+ && $options->{'primary_svc'} == $part_svc->svcpart
+ )
+ ? 'Y'
+ : '';
+
my $old_pkg_svc = qsearchs('pkg_svc', {
'pkgpart' => $old->pkgpart,
$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')
'part_pkg_taxproduct',
'taxproductnum'
)
- || $self->ut_agentnum_acl('agentnum', 'Edit global package definitions')
+ || ( $setup_hack
+ ? $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum' )
+ : $self->ut_agentnum_acl('agentnum', \@null_agentnum_right)
+ )
|| $self->SUPER::check
;
return $error if $error;
}
}
+=item categoryname
+
+Returns the package category name, or the empty string if there is no package
+category.
+
+=cut
+
+sub categoryname {
+ my $self = shift;
+ my $pkg_class = $self->pkg_class;
+ $pkg_class
+ ? $pkg_class->categoryname
+ : '';
+}
+
=item classname
Returns the package class name, or the empty string if there is no package
=cut
+=item type_pkgs
+
+Returns all FS::type_pkgs objects (see L<FS::type_pkgs>) for this package
+definition.
+
+=cut
+
+sub type_pkgs {
+ my $self = shift;
+ qsearch('type_pkgs', { 'pkgpart' => $self->pkgpart } );
+}
+
sub pkg_svc {
my $self = shift;
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
}
}
+=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
=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
=item svc_part_pkg_link
+Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
+
=cut
sub svc_part_pkg_link {
);
}
-=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_description
+
+=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 [ CLASS ]
Returns the description of the associated tax product for this package
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'
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 = '.
).
')';
# 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';
sub calc_cancel { 0; }
sub calc_units { 0; }
+#fallback for everything except bulk.pm
+sub hide_svc_detail { 0; }
+
+=item format OPTION DATA
+
+Returns data formatted according to the function 'format' described
+in the plan info. Returns DATA if no such function exists.
+
+=cut
+
+sub format {
+ my ($self, $option, $data) = (shift, shift, shift);
+ if (exists($plans{$self->plan}->{fields}->{$option}{format})) {
+ &{$plans{$self->plan}->{fields}->{$option}{format}}($data);
+ }else{
+ $data;
+ }
+}
+
+=item parse OPTION DATA
+
+Returns data parsed according to the function 'parse' described
+in the plan info. Returns DATA if no such function exists.
+
+=cut
+
+sub parse {
+ my ($self, $option, $data) = (shift, shift, shift);
+ if (exists($plans{$self->plan}->{fields}->{$option}{parse})) {
+ &{$plans{$self->plan}->{fields}->{$option}{parse}}($data);
+ }else{
+ $data;
+ }
+}
+
=back
=cut
+=head1 CLASS METHODS
+
+=over 4
+
+=cut
+
# _upgrade_data
#
# Used by FS::Upgrade to migrate to a new database.
}
+=item curuser_pkgs_sql
+
+Returns an SQL fragment for searching for packages the current user can
+use, either via part_pkg.agentnum directly, or via agent type (see
+L<FS::type_pkgs>).
+
+=cut
+
+sub curuser_pkgs_sql {
+ my $class = shift;
+
+ $class->_pkgs_sql( $FS::CurrentUser::CurrentUser->agentnums );
+
+}
+
+=item agent_pkgs_sql AGENT | AGENTNUM, ...
+
+Returns an SQL fragment for searching for packages the provided agent or agents
+can use, either via part_pkg.agentnum directly, or via agent type (see
+L<FS::type_pkgs>).
+
+=cut
+
+sub agent_pkgs_sql {
+ my $class = shift; #i'm a class method, not a sub (the question is... why??)
+ my @agentnums = map { ref($_) ? $_->agentnum : $_ } @_;
+
+ $class->_pkgs_sql(@agentnums); #is this why
+
+}
+
+sub _pkgs_sql {
+ my( $class, @agentnums ) = @_;
+ my $agentnums = join(',', @agentnums);
+
+ "
+ (
+ agentnum IS NOT NULL
+ OR
+ 0 < ( SELECT COUNT(*)
+ FROM type_pkgs
+ LEFT JOIN agent_type USING ( typenum )
+ LEFT JOIN agent AS typeagent USING ( typenum )
+ WHERE type_pkgs.pkgpart = part_pkg.pkgpart
+ AND typeagent.agentnum IN ($agentnums)
+ )
+ )
+ ";
+
+}
+
+=back
+
=head1 SUBROUTINES
=over 4
=cut
+#false laziness w/part_export & cdr
my %info;
foreach my $INC ( @INC ) {
warn "globbing $INC/FS/part_pkg/*.pm\n" if $DEBUG;
next;
}
unless ( keys %$info ) {
- warn "no %info hash found in FS::part_pkg::$mod, skipping\n"
- unless $mod =~ /^(passwdfile|null)$/; #hack but what the heck
+ warn "no %info hash found in FS::part_pkg::$mod, skipping\n";
next;
}
warn "got plan info from FS::part_pkg::$mod: $info\n" if $DEBUG;
}
tie %plans, 'Tie::IxHash',
- map { $_ => $info{$_} }
+ map { $_ => $info{$_} }
sort { $info{$a}->{'weight'} <=> $info{$b}->{'weight'} }
keys %info;
\%plans;
}
-=item format OPTION DATA
-
-Returns data formatted according to the function 'format' described
-in the plan info. Returns DATA if no such function exists.
-
-=cut
-
-sub format {
- my ($self, $option, $data) = (shift, shift, shift);
- if (exists($plans{$self->plan}->{fields}->{$option}{format})) {
- &{$plans{$self->plan}->{fields}->{$option}{format}}($data);
- }else{
- $data;
- }
-}
-
-=item parse OPTION DATA
-
-Returns data parsed according to the function 'parse' described
-in the plan info. Returns DATA if no such function exists.
-
-=cut
-
-sub parse {
- my ($self, $option, $data) = (shift, shift, shift);
- if (exists($plans{$self->plan}->{fields}->{$option}{parse})) {
- &{$plans{$self->plan}->{fields}->{$option}{parse}}($data);
- }else{
- $data;
- }
-}
-
=back