package FS::cust_pkg;
use strict;
-use vars qw(@ISA $disable_agentcheck $DEBUG);
+use base qw( FS::cust_main_Mixin FS::location_Mixin
+ FS::m2m_Common FS::option_Common FS::Record
+ );
+use vars qw(@ISA $disable_agentcheck $DEBUG $me);
use Carp qw(cluck);
use Scalar::Util qw( blessed );
use List::Util qw(max);
use Tie::IxHash;
+use Time::Local qw( timelocal_nocheck );
use MIME::Entity;
use FS::UID qw( getotaker dbh );
use FS::Misc qw( send_email );
use FS::Record qw( qsearch qsearchs );
-use FS::m2m_Common;
-use FS::cust_main_Mixin;
use FS::cust_svc;
use FS::part_pkg;
use FS::cust_main;
# for sending cancel emails in sub cancel
use FS::Conf;
-@ISA = qw( FS::m2m_Common FS::cust_main_Mixin FS::option_Common FS::Record );
-
$DEBUG = 0;
+$me = '[FS::cust_pkg]';
$disable_agentcheck = 0;
sub insert {
my( $self, %options ) = @_;
+ if ( $self->part_pkg->option('start_1st', 1) && !$self->start_date ) {
+ my ($sec,$min,$hour,$mday,$mon,$year) = (localtime(time) )[0,1,2,3,4,5];
+ $mon += 1 unless $mday == 1;
+ until ( $mon < 12 ) { $mon -= 12; $year++; }
+ $self->start_date( timelocal_nocheck(0,0,0,1,$mon,$year) );
+ }
+
+ my $expire_months = $self->part_pkg->option('expire_months', 1);
+ if ( $expire_months && !$self->expire ) {
+ my $start = $self->start_date || $self->setup || time;
+
+ #false laziness w/part_pkg::add_freq
+ my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($start) )[0,1,2,3,4,5];
+ $mon += $expire_months;
+ until ( $mon < 12 ) { $mon -= 12; $year++; }
+
+ #$self->expire( timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year) );
+ $self->expire( timelocal_nocheck(0,0,0,$mday,$mon,$year) );
+ }
+
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
#resolved by performing a change package instead (which unprovisions) and
#later cancelling
if ( !$options{nobill} && !$date && $conf->exists('bill_usage_on_cancel') ) {
+ my $copy = $self->new({$self->hash});
my $error =
- $self->cust_main->bill( pkg_list => [ $self ], cancel => 1 );
+ $copy->cust_main->bill( pkg_list => [ $copy ], cancel => 1 );
warn "Error billing during cancel, custnum ".
#$self->cust_main->custnum. ": $error"
": $error"
}
#reset usage if changing pkgpart
+ # AND usage rollover is off (otherwise adds twice, now and at package bill)
if ($self->pkgpart != $cust_pkg->pkgpart) {
my $part_pkg = $cust_pkg->part_pkg;
$error = $part_pkg->reset_usage($cust_pkg, $part_pkg->is_prepaid
? ()
: ( 'null' => 1 )
)
- if $part_pkg->can('reset_usage');
+ if $part_pkg->can('reset_usage') && ! $part_pkg->option('usage_rollover');
if ($error) {
$dbh->rollback if $oldAutoCommit;
#Good to go, cancel old package.
$error = $self->cancel( quiet=>1 );
if ($error) {
- $dbh->rollback;
+ $dbh->rollback if $oldAutoCommit;
return $error;
}
+ if ( $conf->exists('cust_pkg-change_pkgpart-bill_now') ) {
+ #$self->cust_main
+ my $error = $cust_pkg->cust_main->bill( 'pkg_list' => [ $cust_pkg ] );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
$cust_pkg;
}
sub _sort_cust_svc {
my( $self, $arrayref ) = @_;
+ my $sort =
+ sub ($$) { my ($a, $b) = @_; $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] };
+
map { $_->[0] }
- sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
+ sort $sort
map {
my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $self->pkgpart,
'svcpart' => $_->svcpart } );
#seems to benchmark slightly faster...
qsearch( {
- 'select' => 'DISTINCT ON (svcpart) part_svc.*',
+ #'select' => 'DISTINCT ON (svcpart) part_svc.*',
+ #MySQL doesn't grok DISINCT ON
+ 'select' => 'DISTINCT part_svc.*',
'table' => 'part_svc',
'addl_from' =>
'LEFT JOIN pkg_svc ON ( pkg_svc.svcpart = part_svc.svcpart
$statuscolor{$self->status};
}
+=item pkg_label
+
+Returns a label for this package. (Currently "pkgnum: pkg - comment" or
+"pkg-comment" depending on user preference).
+
+=cut
+
+sub pkg_label {
+ my $self = shift;
+ my $label = $self->part_pkg->pkg_comment( 'nopkgpart' => 1 );
+ $label = $self->pkgnum. ": $label"
+ if $FS::CurrentUser::CurrentUser->option('show_pkgnum');
+ $label;
+}
+
+=item pkg_label_long
+
+Returns a long label for this package, adding the primary service's label to
+pkg_label.
+
+=cut
+
+sub pkg_label_long {
+ my $self = shift;
+ my $label = $self->pkg_label;
+ my $cust_svc = $self->primary_cust_svc;
+ $label .= ' ('. ($cust_svc->label)[1]. ')' if $cust_svc;
+ $label;
+}
+
+=item primary_cust_svc
+
+Returns a primary service (as FS::cust_svc object) if one can be identified.
+
+=cut
+
+#for labeling purposes - might not 100% match up with part_pkg->svcpart's idea
+
+sub primary_cust_svc {
+ my $self = shift;
+
+ my @cust_svc = $self->cust_svc;
+
+ return '' unless @cust_svc; #no serivces - irrelevant then
+
+ return $cust_svc[0] if scalar(@cust_svc) == 1; #always return a single service
+
+ # primary service as specified in the package definition
+ # or exactly one service definition with quantity one
+ my $svcpart = $self->part_pkg->svcpart;
+ @cust_svc = grep { $_->svcpart == $svcpart } @cust_svc;
+ return $cust_svc[0] if scalar(@cust_svc) == 1;
+
+ #couldn't identify one thing..
+ return '';
+}
+
=item labels
Returns a list of lists, calling the label method for all services
map { [ $_->label(@_) ] } $self->h_cust_svc(@_);
}
+=item labels_short
+
+Like labels, except returns a simple flat list, and shortens long
+(currently >5 or the cust_bill-max_same_services configuration value) lists of
+identical services to one line that lists the service label and the number of
+individual services rather than individual items.
+
+=cut
+
+sub labels_short {
+ shift->_labels_short( 'labels', @_ );
+}
+
=item h_labels_short END_TIMESTAMP [ START_TIMESTAMP ]
Like h_labels, except returns a simple flat list, and shortens long
=cut
sub h_labels_short {
- my $self = shift;
+ shift->_labels_short( 'h_labels', @_ );
+}
+
+sub _labels_short {
+ my( $self, $method ) = ( shift, shift );
my $conf = new FS::Conf;
my $max_same_services = $conf->config('cust_bill-max_same_services') || 5;
if ( $num > $max_same_services ) {
push @labels, "$label ($num)";
} else {
- push @labels, map { "$label: $_" } @values;
+ if ( $conf->exists('cust_bill-consolidate_services') ) {
+ # push @labels, "$label: ". join(', ', @values);
+ while ( @values ) {
+ my $detail = "$label: ";
+ $detail .= shift(@values). ', '
+ while @values && length($detail.$values[0]) < 78;
+ $detail =~ s/, $//;
+ push @labels, $detail;
+ }
+ } else {
+ push @labels, map { "$label: $_" } @values;
+ }
}
}
qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
}
+#these subs are in location_Mixin.pm now... unfortunately the POD doesn't mixin
+
=item cust_location
Returns the location object, if any (see L<FS::cust_location>).
-=cut
-
-sub cust_location {
- my $self = shift;
- return '' unless $self->locationnum;
- qsearchs( 'cust_location', { 'locationnum' => $self->locationnum } );
-}
-
=item cust_location_or_main
If this package is associated with a location, returns the locaiton (see
L<FS::cust_location>), otherwise returns the customer (see L<FS::cust_main>).
+=item location_label [ OPTION => VALUE ... ]
+
+Returns the label of the location object (see L<FS::cust_location>).
+
=cut
-sub cust_location_or_main {
- my $self = shift;
- $self->cust_location || $self->cust_main;
-}
+#end of subs in location_Mixin.pm now... unfortunately the POD doesn't mixin
=item seconds_since TIMESTAMP
sub active_sql { "
". $_[0]->recurring_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 )
"; }
"cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0";
}
-=item search_sql HASHREF
+=item search HASHREF
(Class method)
=item pkgpart
-list specified how?
+pkgpart or arrayref or hashref of pkgparts
=item setup
specifies the user for agent virtualization
+=item fcc_line
+
+ boolean selects packages containing fcc form 477 telco lines
+
=back
=cut
-sub search_sql {
+sub search {
my ($class, $params) = @_;
my @where = ();
"cust_main.agentnum = $1";
}
+ ##
+ # parse custnum
+ ##
+
+ if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
+ push @where,
+ "cust_pkg.custnum = $1";
+ }
+
##
# parse status
##
push @where, FS::cust_pkg->active_sql();
- } elsif ( $params->{'magic'} eq 'not yet billed'
- || $params->{'status'} eq 'not yet billed' ) {
+ } elsif ( $params->{'magic'} =~ /^not[ _]yet[ _]billed$/
+ || $params->{'status'} =~ /^not[ _]yet[ _]billed$/ ) {
push @where, FS::cust_pkg->not_yet_billed_sql();
{
$classnum = $1;
if ( $classnum ) { #a specific class
- push @where, "classnum = $classnum";
+ push @where, "part_pkg.classnum = $classnum";
#@pkg_class = ( qsearchs('pkg_class', { 'classnum' => $classnum } ) );
#die "classnum $classnum not found!" unless $pkg_class[0];
} elsif ( $classnum eq '' ) { #the empty class
- push @where, "classnum IS NULL";
+ push @where, "part_pkg.classnum IS NULL";
#$title .= 'Empty class ';
#@pkg_class = ( '(empty class)' );
} elsif ( $classnum eq '0' ) {
push @where, "part_pkg.custom = 'Y'" if $params->{custom};
+ ###
+ # parse fcc_line
+ ###
+
+ push @where, "part_pkg.fcc_ds0s > 0" if $params->{fcc_line};
+
###
# parse censustract
###
# parse part_pkg
###
- my $pkgpart = join (' OR pkgpart=',
- grep {$_} map { /^(\d+)$/; } ($params->{'pkgpart'}));
- push @where, '(pkgpart=' . $pkgpart . ')' if $pkgpart;
+ if ( ref($params->{'pkgpart'}) ) {
+
+ my @pkgpart = ();
+ if ( ref($params->{'pkgpart'}) eq 'HASH' ) {
+ @pkgpart = grep $params->{'pkgpart'}{$_}, keys %{ $params->{'pkgpart'} };
+ } elsif ( ref($params->{'pkgpart'}) eq 'ARRAY' ) {
+ @pkgpart = @{ $params->{'pkgpart'} };
+ } else {
+ die 'unhandled pkgpart ref '. $params->{'pkgpart'};
+ }
+
+ @pkgpart = grep /^(\d+)$/, @pkgpart;
+
+ push @where, 'pkgpart IN ('. join(',', @pkgpart). ')' if scalar(@pkgpart);
+
+ } elsif ( $params->{'pkgpart'} =~ /^(\d+)$/ ) {
+ push @where, "pkgpart = $1";
+ }
###
# parse dates
if ($access_user) {
push @where, $access_user->agentnums_sql('table'=>'cust_main');
- }else{
+ } else {
push @where, "1=0";
}
- }else{
+ } else {
push @where, $FS::CurrentUser::CurrentUser->agentnums_sql('table'=>'cust_main');
}
my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
- my $addl_from = 'LEFT JOIN cust_main USING ( custnum ) '.
- 'LEFT JOIN part_pkg USING ( pkgpart ) '.
- 'LEFT JOIN pkg_class USING ( classnum ) ';
+ my $addl_from = 'LEFT JOIN part_pkg USING ( pkgpart ) '.
+ 'LEFT JOIN pkg_class USING ( classnum ) '.
+ 'LEFT JOIN cust_main USING ( custnum ) ';
my $count_query = "SELECT COUNT(*) FROM cust_pkg $addl_from $extra_sql";
}
+=item fcc_477_count
+
+Returns a list of two package counts. The first is a count of packages
+based on the supplied criteria and the second is the count of residential
+packages with those same criteria. Criteria are specified as in the search
+method.
+
+=cut
+
+sub fcc_477_count {
+ my ($class, $params) = @_;
+
+ my $sql_query = $class->search( $params );
+
+ my $count_sql = delete($sql_query->{'count_query'});
+ $count_sql =~ s/ FROM/,count(CASE WHEN cust_main.company IS NULL OR cust_main.company = '' THEN 1 END) FROM/
+ or die "couldn't parse count_sql";
+
+ my $count_sth = dbh->prepare($count_sql)
+ or die "Error preparing $count_sql: ". dbh->errstr;
+ $count_sth->execute
+ or die "Error executing $count_sql: ". $count_sth->errstr;
+ my $count_arrayref = $count_sth->fetchrow_arrayref;
+
+ return ( @$count_arrayref );
+
+}
+
+
=item location_sql
Returns a list: the first item is an SQL fragment identifying matching
# my $cust_main = qsearchs('cust_main', { custnum => $custnum });
# return "Customer not found: $custnum" unless $cust_main;
+ warn "$me order: pkgnums to remove: ". join(',', @$remove_pkgnum). "\n"
+ if $DEBUG;
+
my @old_cust_pkg = map { qsearchs('cust_pkg', { pkgnum => $_ }) }
@$remove_pkgnum;
my %hash = ();
if ( scalar(@old_cust_pkg) == 1 && scalar(@$pkgparts) == 1 ) {
+ warn "$me order: changing pkgnum ". $old_cust_pkg[0]->pkgnum.
+ " to pkgpart ". $pkgparts->[0]. "\n"
+ if $DEBUG;
+
my $err_or_cust_pkg =
$old_cust_pkg[0]->change( 'pkgpart' => $pkgparts->[0],
'refnum' => $refnum,
}
push @$return_cust_pkg, $err_or_cust_pkg;
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
return '';
}
# Create the new packages.
foreach my $pkgpart (@$pkgparts) {
+
+ warn "$me order: inserting pkgpart $pkgpart\n" if $DEBUG;
+
my $cust_pkg = new FS::cust_pkg { custnum => $custnum,
pkgpart => $pkgpart,
refnum => $refnum,
# Transfer services and cancel old packages.
foreach my $old_pkg (@old_cust_pkg) {
+ warn "$me order: transferring services from pkgnum ". $old_pkg->pkgnum. "\n"
+ if $DEBUG;
+
foreach my $new_pkg (@$return_cust_pkg) {
$error = $old_pkg->transfer($new_pkg);
if ($error and $error == 0) {