=item change
If set true, supresses actions that should only be taken for new package
-orders. (Currently this includes: intro periods when delay_setup is on.)
+orders. (Currently this includes: intro periods when delay_setup is on,
+auto-adding a 1st start date, auto-adding expiration/adjourn/contract_end dates)
=item options
my $part_pkg = $self->part_pkg;
- # if the package def says to start only on the first of the month:
- if ( $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) );
- }
-
- # set up any automatic expire/adjourn/contract_end timers
- # based on the start date
- foreach my $action ( qw(expire adjourn contract_end) ) {
- my $months = $part_pkg->option("${action}_months",1);
- if($months and !$self->$action) {
- my $start = $self->start_date || $self->setup || time;
- $self->$action( $part_pkg->add_freq($start, $months) );
+ if ( ! $options{'change'} ) {
+
+ # if the package def says to start only on the first of the month:
+ if ( $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) );
+ }
+
+ # set up any automatic expire/adjourn/contract_end timers
+ # based on the start date
+ foreach my $action ( qw(expire adjourn contract_end) ) {
+ my $months = $part_pkg->option("${action}_months",1);
+ if($months and !$self->$action) {
+ my $start = $self->start_date || $self->setup || time;
+ $self->$action( $part_pkg->add_freq($start, $months) );
+ }
+ }
+
+ # if this package has "free days" and delayed setup fee, then
+ # set start date that many days in the future.
+ # (this should have been set in the UI, but enforce it here)
+ if ( ! $options{'change'}
+ && $part_pkg->option('free_days',1)
+ && $part_pkg->option('delay_setup',1)
+ #&& ! $self->start_date
+ )
+ {
+ $self->start_date( $part_pkg->default_start_date );
}
- }
- # if this package has "free days" and delayed setup fee, tehn
- # set start date that many days in the future.
- # (this should have been set in the UI, but enforce it here)
- if ( ! $options{'change'}
- && ( my $free_days = $part_pkg->option('free_days',1) )
- && $part_pkg->option('delay_setup',1)
- #&& ! $self->start_date
- )
- {
- $self->start_date( $part_pkg->default_start_date );
}
- $self->order_date(time);
+ # set order date unless this was previously a different package
+ $self->order_date(time) unless $self->change_pkgnum;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{TSTP} = 'IGNORE';
local $SIG{PIPE} = 'IGNORE';
+ $self->susp( $self->order_date ) if $self->susp eq 'now';
+
my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
|| $self->ut_numbern('dundate')
|| $self->ut_enum('no_auto', [ '', 'Y' ])
|| $self->ut_enum('waive_setup', [ '', 'Y' ])
- || $self->ut_numbern('agent_pkgid')
+ || $self->ut_textn('agent_pkgid')
|| $self->ut_enum('recur_show_zero', [ '', 'Y', 'N', ])
|| $self->ut_enum('setup_show_zero', [ '', 'Y', 'N', ])
|| $self->ut_foreign_keyn('main_pkgnum', 'cust_pkg', 'pkgnum')
'to' => \@invoicing_list,
'subject' => ( $conf->config('cancelsubject') || 'Cancellation Notice' ),
'body' => [ map "$_\n", $conf->config('cancelmessage') ],
+ 'custnum' => $self->custnum,
+ 'msgtype' => '', #admin?
);
}
#should this do something on errors?
'Package : #'. $self->pkgnum. " (". $self->part_pkg->pkg_comment. ")\n",
( map { "Service : $_\n" } @labels ),
],
+ 'custnum' => $self->custnum,
+ 'msgtype' => 'admin'
);
if ( $error ) {
my $conf = new FS::Conf;
- if ( $inactive > 0 &&
- ( $hash{'bill'} || $hash{'setup'} ) &&
- ( $opt{'adjust_next_bill'} ||
- $conf->exists('unsuspend-always_adjust_next_bill_date') ||
- $self->part_pkg->option('unsuspend_adjust_bill', 1) )
- ) {
-
- $hash{'bill'} = ( $hash{'bill'} || $hash{'setup'} ) + $inactive;
-
- }
+ $hash{'bill'} = ( $hash{'bill'} || $hash{'setup'} ) + $inactive
+ if $inactive > 0
+ && ( $hash{'bill'} || $hash{'setup'} )
+ && ( $opt{'adjust_next_bill'}
+ || $conf->exists('unsuspend-always_adjust_next_bill_date')
+ || $self->part_pkg->option('unsuspend_adjust_bill', 1)
+ )
+ && ! $self->option('suspend_bill',1)
+ && ( ! $self->part_pkg->option('suspend_bill',1)
+ || $self->option('no_suspend_bill',1)
+ )
+ && $hash{'order_date'} != $hash{'susp'}
+ ;
$hash{'susp'} = '';
$hash{'adjourn'} = '' if $hash{'adjourn'} and $hash{'adjourn'} < time;
: ''
),
],
+ 'custnum' => $self->custnum,
+ 'msgtype' => 'admin',
);
if ( $error ) {
$error = $opt->{'cust_location'}->find_or_insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "inserting cust_location (transaction rolled back): $error";
+ return "creating location record: $error";
}
$opt->{'locationnum'} = $opt->{'cust_location'}->locationnum;
}
$hash{$date} = $self->getfield($date);
}
}
+ # always keep this date, regardless of anything
+ # (the date of the package change is in a different field)
+ $hash{'order_date'} = $self->getfield('order_date');
# allow $opt->{'locationnum'} = '' to specifically set it to null
# (i.e. customer default location)
if ( $opt->{cust_main} ) {
my $cust_main = $opt->{cust_main};
unless ( $cust_main->custnum ) {
- my $error = $cust_main->insert;
+ my $error = $cust_main->insert( @{ $opt->{cust_main_insert_args}||[] } );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "inserting cust_main (transaction rolled back): $error";
+ return "inserting customer record: $error";
}
}
$custnum = $cust_main->custnum;
}
if ($error) {
$dbh->rollback if $oldAutoCommit;
- return $error;
+ return "inserting new package: $error";
}
# Transfer services and cancel old package.
if ($error and $error == 0) {
# $old_pkg->transfer failed.
$dbh->rollback if $oldAutoCommit;
- return $error;
+ return "transferring $error";
}
if ( $error > 0 && $conf->exists('cust_pkg-change_svcpart') ) {
if ($error and $error == 0) {
# $old_pkg->transfer failed.
$dbh->rollback if $oldAutoCommit;
- return $error;
+ return "converting $error";
}
}
# Transfers were successful, but we still had services left on the old
# package. We can't change the package under this circumstances, so abort.
$dbh->rollback if $oldAutoCommit;
- return "Unable to transfer all services from package ". $self->pkgnum;
+ return "unable to transfer all services";
}
#reset usage if changing pkgpart
if ($error) {
$dbh->rollback if $oldAutoCommit;
- return "Error setting usage values: $error";
+ return "setting usage values: $error";
}
} else {
# if NOT changing pkgpart, transfer any usage pools over
$error = $usage->replace;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "Error transferring usage pools: $error";
+ return "transferring usage pools: $error";
}
}
}
$error = $new_discount->insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "Error transferring discounts: $error";
+ return "transferring discounts: $error";
}
}
}
$error = $new_detail->insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "Error transferring package notes: $error";
+ return "transferring package notes: $error";
}
}
);
if ($error) {
$dbh->rollback if $oldAutoCommit;
- return $error;
+ return "canceling old package: $error";
}
if ( $conf->exists('cust_pkg-change_pkgpart-bill_now') ) {
);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return $error;
+ return "billing new package: $error";
}
}
=item modify_charge OPTIONS
-Change the properties of a one-time charge. Currently the only properties
-that can be changed this way are those that have no impact on billing
-calculations:
+Change the properties of a one-time charge. The following properties can
+be changed this way:
- pkg: the package description
- classnum: the package class
- additional: arrayref of additional invoice details to add to this package
+and, I<if the charge has not yet been billed>:
+- start_date: the date when it will be billed
+- amount: the setup fee to be charged
+- quantity: the multiplier for the setup fee
+
If you pass 'adjust_commission' => 1, and the classnum changes, and there are
commission credits linked to this charge, they will be recalculated.
}
my %pkg_opt = $part_pkg->options;
- if ( ref($opt{'additional'}) ) {
- delete $pkg_opt{$_} foreach grep /^additional/, keys %pkg_opt;
- my $i;
- for ( $i = 0; exists($opt{'additional'}->[$i]); $i++ ) {
- $pkg_opt{ "additional_info$i" } = $opt{'additional'}->[$i];
+ my $pkg_opt_modified = 0;
+
+ $opt{'additional'} ||= [];
+ my $i;
+ my @old_additional;
+ foreach (grep /^additional/, keys %pkg_opt) {
+ ($i) = ($_ =~ /^additional_info(\d+)$/);
+ $old_additional[$i] = $pkg_opt{$_} if $i;
+ delete $pkg_opt{$_};
+ }
+
+ for ( $i = 0; exists($opt{'additional'}->[$i]); $i++ ) {
+ $pkg_opt{ "additional_info$i" } = $opt{'additional'}->[$i];
+ if (!exists($old_additional[$i])
+ or $old_additional[$i] ne $opt{'additional'}->[$i])
+ {
+ $pkg_opt_modified = 1;
}
- $pkg_opt{'additional_count'} = $i if $i > 0;
}
+ $pkg_opt_modified = 1 if (scalar(@old_additional) - 1) != $i;
+ $pkg_opt{'additional_count'} = $i if $i > 0;
my $old_classnum;
- if ( exists($opt{'classnum'}) and $part_pkg->classnum ne $opt{'classnum'} ) {
+ if ( exists($opt{'classnum'}) and $part_pkg->classnum ne $opt{'classnum'} )
+ {
# remember it
$old_classnum = $part_pkg->classnum;
$part_pkg->set('classnum', $opt{'classnum'});
}
- my $error = $part_pkg->replace( options => \%pkg_opt );
- return $error if $error;
+ if ( !$self->get('setup') ) {
+ # not yet billed, so allow amount and quantity
+ if ( exists($opt{'quantity'})
+ and $opt{'quantity'} != $self->quantity
+ and $opt{'quantity'} > 0 ) {
+
+ $self->set('quantity', $opt{'quantity'});
+ }
+ if ( exists($opt{'start_date'})
+ and $opt{'start_date'} != $self->start_date ) {
+
+ $self->set('start_date', $opt{'start_date'});
+ }
+
+ if ( exists($opt{'amount'})
+ and $part_pkg->option('setup_fee') != $opt{'amount'}
+ and $opt{'amount'} > 0 ) {
+
+ $pkg_opt{'setup_fee'} = $opt{'amount'};
+ $pkg_opt_modified = 1;
+
+ }
+ } # else simply ignore them; the UI shouldn't allow editing the fields
+
+ my $error;
+ if ( $part_pkg->modified or $pkg_opt_modified ) {
+ # can we safely modify the package def?
+ # Yes, if it's not available for purchase, and this is the only instance
+ # of it.
+ if ( $part_pkg->disabled
+ and FS::cust_pkg->count('pkgpart = '.$part_pkg->pkgpart) == 1
+ and FS::quotation_pkg->count('pkgpart = '.$part_pkg->pkgpart) == 0
+ ) {
+ $error = $part_pkg->replace( options => \%pkg_opt );
+ } else {
+ # clone it
+ $part_pkg = $part_pkg->clone;
+ $part_pkg->set('disabled' => 'Y');
+ $error = $part_pkg->insert( options => \%pkg_opt );
+ # and associate this as yet-unbilled package to the new package def
+ $self->set('pkgpart' => $part_pkg->pkgpart);
+ }
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+ if ($self->modified) { # for quantity or start_date change, or if we had
+ # to clone the existing package def
+ my $error = $self->replace;
+ return $error if $error;
+ }
if (defined $old_classnum) {
# fix invoice grouping records
my $old_catname = $old_classnum
=item cust_svc [ OPTION => VALUE ... ] (current usage)
+=item cust_svc_unsorted [ OPTION => VALUE ... ]
+
Returns the services for this package, as FS::cust_svc objects (see
L<FS::cust_svc>). Available options are svcpart and svcdb. If either is
spcififed, returns only the matching services.
+As an optimization, use the cust_svc_unsorted version if you are not displaying
+the results.
+
=cut
sub cust_svc {
my $self = shift;
+ cluck "cust_pkg->cust_svc called" if $DEBUG > 2;
+ $self->_sort_cust_svc( $self->cust_svc_unsorted_arrayref(@_) );
+}
- return () unless $self->num_cust_svc(@_);
+sub cust_svc_unsorted {
+ my $self = shift;
+ @{ $self->cust_svc_unsorted_arrayref(@_) };
+}
+
+sub cust_svc_unsorted_arrayref {
+ my $self = shift;
+
+ return [] unless $self->num_cust_svc(@_);
my %opt = ();
if ( @_ && $_[0] =~ /^\d+/ ) {
$search{extra_sql} = ' AND svcdb = '. dbh->quote( $opt{'svcdb'} );
}
- cluck "cust_pkg->cust_svc called" if $DEBUG > 2;
-
- #if ( $self->{'_svcnum'} ) {
- # values %{ $self->{'_svcnum'}->cache };
- #} else {
- $self->_sort_cust_svc( [ qsearch(\%search) ] );
- #}
+ [ qsearch(\%search) ];
}
=over 4
-=item num_cust_svc (count)
+=item num_cust_svc
+
+(count)
-=item num_avail (quantity - count)
+=item num_avail
-=item cust_pkg_svc (services) - array reference containing the provisioned services, as cust_svc objects
+(quantity - count)
+
+=item cust_pkg_svc
+
+(services) - array reference containing the provisioned services, as cust_svc objects
=back
-Accepts one option: summarize_size. If specified and non-zero, will omit the
-extra cust_pkg_svc option for objects where num_cust_svc is this size or
-greater.
+Accepts two options:
+
+=over 4
+
+=item summarize_size
+
+If true, will omit the extra cust_pkg_svc option for objects where num_cust_svc
+is this size or greater.
+
+=item hide_discontinued
+
+If true, will omit looking for services that are no longer avaialble in the
+package definition.
+
+=back
=cut
$part_svc;
} $self->part_pkg->pkg_svc;
- #extras
- push @part_svc, map {
- my $part_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'} =
- $num_cust_svc ? [ $self->cust_svc($part_svc->svcpart) ] : [];
- $part_svc;
- } $self->extra_part_svc;
+ unless ( $opt{hide_discontinued} ) {
+ #extras
+ push @part_svc, map {
+ my $part_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'} =
+ $num_cust_svc ? [ $self->cust_svc($part_svc->svcpart) ] : [];
+ $part_svc;
+ } $self->extra_part_svc;
+ }
@part_svc;
=over 4
+=item on hold
+
=item not yet billed
=item one-time charge
my $freq = length($self->freq) ? $self->freq : $self->part_pkg->freq;
return 'cancelled' if $self->get('cancel');
+ return 'on hold' if $self->susp && ! $self->setup;
return 'suspended' if $self->susp;
return 'not yet billed' unless $self->setup;
return 'one-time charge' if $freq =~ /^(0|$)/;
=cut
tie my %statuscolor, 'Tie::IxHash',
+ 'on hold' => '7E0079', #purple!
'not yet billed' => '009999', #teal? cyan?
- 'one-time charge' => '000000',
+ 'one-time charge' => '0000CC', #blue #'000000',
'active' => '00CC00',
'suspended' => 'FF9900',
'cancelled' => 'FF0000',
keys %statuscolor;
}
+sub statuscolors {
+ #my $self = shift;
+ \%statuscolor;
+}
+
=item statuscolor
Returns a hex triplet color string for this package's status.
sub tax_location {
my $self = shift;
- FS::cust_location->by_key( $self->tax_locationnum )
+ my $conf = FS::Conf->new;
+ if ( $conf->exists('tax-pkg_address') and $self->locationnum ) {
+ return FS::cust_location->by_key($self->locationnum);
+ }
+ elsif ( $conf->exists('tax-ship_address') ) {
+ return $self->cust_main->ship_location;
+ }
+ else {
+ return $self->cust_main->bill_location;
+ }
}
=item seconds_since TIMESTAMP
}
}
+ my $error;
foreach my $cust_svc ($self->cust_svc) {
+ my $svcnum = $cust_svc->svcnum;
if($target{$cust_svc->svcpart} > 0
or $FS::cust_svc::ignore_quantity) { # maybe should be a 'force' option
$target{$cust_svc->svcpart}--;
my $new = new FS::cust_svc { $cust_svc->hash };
$new->pkgnum($dest_pkgnum);
- my $error = $new->replace($cust_svc);
- return $error if $error;
+ $error = $new->replace($cust_svc);
} elsif ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
if ( $DEBUG ) {
warn "looking for alternates for svcpart ". $cust_svc->svcpart. "\n";
my $new = new FS::cust_svc { $cust_svc->hash };
$new->svcpart($change_svcpart);
$new->pkgnum($dest_pkgnum);
- my $error = $new->replace($cust_svc);
- return $error if $error;
+ $error = $new->replace($cust_svc);
} else {
$remaining++;
}
} else {
$remaining++
}
+ if ( $error ) {
+ my @label = $cust_svc->label;
+ return "service $label[1]: $error";
+ }
}
return $remaining;
}
minutes => min($cust_pkg_usage->minutes, $minutes),
});
$cust_pkg_usage->set('minutes',
- sprintf('%.0f', $cust_pkg_usage->minutes - $cdr_cust_pkg_usage->minutes)
+ $cust_pkg_usage->minutes - $cdr_cust_pkg_usage->minutes
);
$error = $cust_pkg_usage->replace || $cdr_cust_pkg_usage->insert;
$minutes -= $cdr_cust_pkg_usage->minutes;
AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
"; }
+=item on_hold_sql
+
+Returns an SQL expression identifying on-hold packages.
+
+=cut
+
+sub on_hold_sql {
+ #$_[0]->recurring_sql(). ' AND '.
+ "
+ ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
+ AND cust_pkg.susp IS NOT NULL AND cust_pkg.susp != 0
+ AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 )
+ ";
+}
+
=item susp_sql
=item suspended_sql
"
( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
AND cust_pkg.susp IS NOT NULL AND cust_pkg.susp != 0
+ AND cust_pkg.setup IS NOT NULL AND cust_pkg.setup != 0
";
}
sub status_sql {
"CASE
WHEN cust_pkg.cancel IS NOT NULL THEN 'cancelled'
+ WHEN ( cust_pkg.susp IS NOT NULL AND cust_pkg.setup IS NULL ) THEN 'on hold'
WHEN cust_pkg.susp IS NOT NULL THEN 'suspended'
WHEN cust_pkg.setup IS NULL THEN 'not yet billed'
WHEN ".onetime_sql()." THEN 'one-time charge'
=item magic
-active, inactive, suspended, cancel (or cancelled)
+on hold, active, inactive (or one-time charge), suspended, cancel (or cancelled)
=item status
-active, inactive, suspended, one-time charge, inactive, cancel (or cancelled)
+on hold, active, inactive (or one-time charge), suspended, cancel (or cancelled)
=item custom
push @where, FS::cust_pkg->inactive_sql();
+ } elsif ( $params->{'magic'} =~ /^on[ _]hold$/
+ || $params->{'status'} =~ /^on[ _]hold$/ ) {
+
+ push @where, FS::cust_pkg->on_hold_sql();
+
+
} elsif ( $params->{'magic'} eq 'suspended'
|| $params->{'status'} eq 'suspended' ) {
);
if( exists($params->{'active'} ) ) {
- # This overrides all the other date-related fields
+ # This overrides all the other date-related fields, and includes packages
+ # that were active at some time during the interval. It excludes:
+ # - packages that were set up after the end of the interval
+ # - packages that were canceled before the start of the interval
+ # - packages that were suspended before the start of the interval
+ # and are still suspended now
my($beginning, $ending) = @{$params->{'active'}};
push @where,
"cust_pkg.setup IS NOT NULL",
"cust_pkg.setup <= $ending",
"(cust_pkg.cancel IS NULL OR cust_pkg.cancel >= $beginning )",
+ "(cust_pkg.susp IS NULL OR cust_pkg.susp >= $beginning )",
"NOT (".FS::cust_pkg->onetime_sql . ")";
}
else {