use strict;
use vars qw(@ISA $disable_agentcheck $DEBUG);
+use Carp qw(cluck);
use Scalar::Util qw( blessed );
use List::Util qw(max);
use Tie::IxHash;
+use MIME::Entity;
use FS::UID qw( getotaker dbh );
use FS::Misc qw( send_email );
use FS::Record qw( qsearch qsearchs );
Optional link to package location (see L<FS::location>)
+=item start_date
+
+date
+
=item setup
date
cust_pkg_option records will be created
+=item ticket_subject
+
+a ticket will be added to this customer with this subject
+
+=item ticket_queue
+
+an optional queue name for ticket additions
+
=back
=cut
my $conf = new FS::Conf;
+ if ( $conf->config('ticket_system') && $options{ticket_subject} ) {
+ eval '
+ use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" );
+ use RT;
+ ';
+ die $@ if $@;
+
+ RT::LoadConfig();
+ RT::Init();
+ my $q = new RT::Queue($RT::SystemUser);
+ $q->Load($options{ticket_queue}) if $options{ticket_queue};
+ my $t = new RT::Ticket($RT::SystemUser);
+ my $mime = new MIME::Entity;
+ $mime->build( Type => 'text/plain', Data => $options{ticket_subject} );
+ $t->Create( $options{ticket_queue} ? (Queue => $q) : (),
+ Subject => $options{ticket_subject},
+ MIMEObj => $mime,
+ );
+ $t->AddLink( Type => 'MemberOf',
+ Target => 'freeside://freeside/cust_main/'. $self->custnum,
+ );
+ }
+
if ($conf->config('welcome_letter') && $self->cust_main->num_pkgs == 1) {
my $queue = new FS::queue {
'job' => 'FS::cust_main::queueable_print',
|| $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
|| $self->ut_numbern('pkgpart')
|| $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
+ || $self->ut_numbern('start_date')
|| $self->ut_numbern('setup')
|| $self->ut_numbern('bill')
|| $self->ut_numbern('susp')
unless ( $disable_agentcheck ) {
my $agent =
qsearchs( 'agent', { 'agentnum' => $self->cust_main->agentnum } );
- my $pkgpart_href = $agent->pkgpart_hashref;
- return "agent ". $agent->agentnum.
+ return "agent ". $agent->agentnum. ':'. $agent->agent.
" can't purchase pkgpart ". $self->pkgpart
- unless $pkgpart_href->{ $self->pkgpart };
+ unless $agent->pkgpart_hashref->{ $self->pkgpart }
+ || $agent->agentnum == $self->part_pkg->agentnum;
}
$error = $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart' );
=item date - can be set to a unix style timestamp to specify when to cancel (expire)
+=item nobill - can be set true to skip billing if it might otherwise be done.
+
=back
If there is an error, returns the error, otherwise returns false.
my( $self, %options ) = @_;
my $error;
+ my $conf = new FS::Conf;
+
warn "cust_pkg::cancel called with options".
join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
if $DEBUG;
my $date = $options{date} if $options{date}; # expire/cancel later
$date = '' if ($date && $date <= time); # complain instead?
+ #race condition: usage could be ongoing until unprovisioned
+ #resolved by performing a change package instead (which unprovisions) and
+ #later cancelling
+ if ( !$options{nobill} && !$date && $conf->exists('bill_usage_on_cancel') ) {
+ my $error =
+ $self->cust_main->bill( pkg_list => [ $self ], cancel => 1 );
+ warn "Error billing during cancel, custnum ".
+ #$self->cust_main->custnum. ": $error"
+ ": $error"
+ if $error;
+ }
+
+
my $cancel_time = $options{'time'} || time;
if ( $options{'reason'} ) {
=cut
-use Carp qw(cluck);
sub part_pkg {
my $self = shift;
- cluck "part_pkg called" if $DEBUG > 1 && ! $self->{'_pkgpart'};
- #exists( $self->{'_pkgpart'} )
- $self->{'_pkgpart'}
- ? $self->{'_pkgpart'}
- : qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
+ return $self->{'_pkgpart'} if $self->{'_pkgpart'};
+ cluck "cust_pkg->part_pkg called" if $DEBUG > 1;
+ qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
}
=item old_cust_pkg
sub cust_svc {
my $self = shift;
+ return () unless $self->num_cust_svc(@_);
+
if ( @_ ) {
return qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum,
'svcpart' => shift, } );
}
+ cluck "cust_pkg->cust_svc called" if $DEBUG > 2;
+
#if ( $self->{'_svcnum'} ) {
# values %{ $self->{'_svcnum'}->cache };
#} else {
sub overlimit {
my $self = shift;
- grep { $_->overlimit } $self->cust_svc;
+ return () unless $self->num_cust_svc(@_);
+ grep { $_->overlimit } $self->cust_svc(@_);
}
=item h_cust_svc END_TIMESTAMP [ START_TIMESTAMP ]
sub num_cust_svc {
my $self = shift;
+
+ return $self->{'_num_cust_svc'}
+ if !scalar(@_)
+ && exists($self->{'_num_cust_svc'})
+ && $self->{'_num_cust_svc'} =~ /\d/;
+
+ cluck "cust_pkg->num_cust_svc called, _num_cust_svc:".$self->{'_num_cust_svc'}
+ if $DEBUG > 2;
+
my $sql = 'SELECT COUNT(*) FROM cust_svc WHERE pkgnum = ?';
$sql .= ' AND svcpart = ?' if @_;
- my $sth = dbh->prepare($sql) or die dbh->errstr;
+
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
$sth->execute($self->pkgnum, @_) or die $sth->errstr;
$sth->fetchrow_arrayref->[0];
}
$part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #more evil
$part_svc->{'Hash'}{'num_avail'} =
max( 0, $pkg_svc->quantity - $num_cust_svc );
- $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
+ $part_svc->{'Hash'}{'cust_pkg_svc'} =
+ $num_cust_svc ? [ $self->cust_svc($part_svc->svcpart) ] : [];
$part_svc;
} $self->part_pkg->pkg_svc;
my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
$part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #speak no evail
$part_svc->{'Hash'}{'num_avail'} = 0; #0-$num_cust_svc ?
- $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
+ $part_svc->{'Hash'}{'cust_pkg_svc'} =
+ $num_cust_svc ? [ $self->cust_svc($part_svc->svcpart) ] : [];
$part_svc;
} $self->extra_part_svc;
sub statuses {
my $self = shift; #could be class...
- grep { $_ !~ /^(not yet billed)$/ } #this is a dumb status anyway
- # mayble split btw one-time vs. recur
+ #grep { $_ !~ /^(not yet billed)$/ } #this is a dumb status anyway
+ # # mayble split btw one-time vs. recur
keys %statuscolor;
}
AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
"; }
+=item not_yet_billed_sql
+
+Returns an SQL expression identifying packages which have not yet been billed.
+
+=cut
+
+sub not_yet_billed_sql { "
+ ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 )
+ AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
+ AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
+"; }
+
=item inactive_sql
Returns an SQL expression identifying inactive packages (one-time packages
sub inactive_sql { "
". $_[0]->onetime_sql(). "
+ AND cust_pkg.setup IS NOT NULL AND cust_pkg.setup != 0
AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
"; }
active, inactive, suspended, one-time charge, inactive, cancel (or cancelled)
+=item custom
+
+ boolean selects custom packages
+
=item classnum
=item pkgpart
push @where, FS::cust_pkg->active_sql();
- } elsif ( $params->{'magic'} eq 'inactive'
- || $params->{'status'} eq 'inactive' ) {
+ } elsif ( $params->{'magic'} eq 'not yet billed'
+ || $params->{'status'} eq 'not yet billed' ) {
+
+ push @where, FS::cust_pkg->not_yet_billed_sql();
+
+ } elsif ( $params->{'magic'} =~ /^(one-time charge|inactive)/
+ || $params->{'status'} =~ /^(one-time charge|inactive)/ ) {
push @where, FS::cust_pkg->inactive_sql();
push @where, FS::cust_pkg->cancelled_sql();
- } elsif ( $params->{'status'} =~ /^(one-time charge|inactive)$/ ) {
-
- push @where, FS::cust_pkg->inactive_sql();
-
}
###
}
#eslaf
+ ###
+ # parse package report options
+ ###
+
+ my @report_option = ();
+ if ( exists($params->{'report_option'})
+ && $params->{'report_option'} =~ /^([,\d]*)$/
+ )
+ {
+ @report_option = split(',', $1);
+ }
+
+ if (@report_option) {
+ # this will result in the empty set for the dangling comma case as it should
+ push @where,
+ map{ "0 < ( SELECT count(*) FROM part_pkg_option
+ WHERE part_pkg_option.pkgpart = part_pkg.pkgpart
+ AND optionname = 'report_option_$_'
+ AND optionvalue = '1' )"
+ } @report_option;
+ }
+
+ #eslaf
+
+ ###
+ # parse custom
+ ###
+
+ push @where, "part_pkg.custom = 'Y'" if $params->{custom};
+
+ ###
+ # parse censustract
+ ###
+
+ if ( $params->{'censustract'} =~ /^([.\d]+)$/ and $1 ) {
+ push @where, "cust_main.censustract = '". $params->{censustract}. "'";
+ }
+
###
# parse part_pkg
###