=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';
'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 ) {
: ''
),
],
+ '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 $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'});
}
+ 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 ($self->modified) { # for quantity or start_date change
+ my $error = $self->replace;
+ return $error if $error;
+ }
+
+ if ( exists($opt{'amount'})
+ and $part_pkg->option('setup_fee') != $opt{'amount'}
+ and $opt{'amount'} > 0 ) {
+
+ $pkg_opt{'setup_fee'} = $opt{'amount'};
+ # standard for one-time charges is to set comment = (formatted) amount
+ # update it to avoid confusion
+ my $conf = FS::Conf->new;
+ $part_pkg->set('comment',
+ ($conf->config('money_char') || '$') .
+ sprintf('%.2f', $opt{'amount'})
+ );
+ }
+ } # else simply ignore them; the UI shouldn't allow editing the fields
+
my $error = $part_pkg->replace( options => \%pkg_opt );
- return $error if $error;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
if (defined $old_classnum) {
# fix invoice grouping records
=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(@_) );
+}
+
+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(@_);
$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) ];
}
foreach my $cust_svc (
grep {
my $part_svc = $_->part_svc;
- $part_svc->svcdb eq 'svc_acct'
- && scalar($part_svc->part_export_usage);
+ scalar($part_svc->part_export_usage);
} $self->cust_svc
) {
$sum += $cust_svc->attribute_since_sqlradacct($start, $end, $attrib);
}
}
+ 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;
}
=item location_cust
-Limit to packages whose service location is the same as the customer's
+Limit to packages whose service locations are the same as the customer's
default service location.
=item location_nocust
-Limit to packages whose service location is not the customer's default
+Limit to packages whose service locations are not the customer's default
service location.
=item location_census
-Limit to packages whose service location has a census tract.
+Limit to packages whose service locations have census tracts.
=item location_nocensus
-Limit to packages whose service location doesn't have a census tract.
+Limit to packages whose service locations do not have a census tract.
+
+=item location_geocode
+
+Limit to packages whose locations have geocodes.
+
+=item location_geocode
+
+Limit to packages whose locations do not have geocodes.
=back
my $op = $params->{location_census} ? "IS NOT NULL" : "IS NULL";
push @where, "cust_location.censustract $op";
}
+ if ( $params->{location_geocode} xor $params->{location_nogeocode} ) {
+ my $op = $params->{location_geocode} ? "IS NOT NULL" : "IS NULL";
+ push @where, "cust_location.geocode $op";
+ }
###
# parse part_pkg
);
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 {