package FS::cust_main::Packages;
use strict;
-use vars qw( $DEBUG $me );
+use vars qw( $DEBUG $me $skip_label_sort );
use List::Util qw( min );
use FS::UID qw( dbh );
use FS::Record qw( qsearch qsearchs );
$DEBUG = 0;
$me = '[FS::cust_main::Packages]';
+$skip_label_sort = 0;
=head1 NAME
specific job completes). This can be used to defer provisioning until some
action completes (such as running the customer's credit card successfully).
+=item noexport
+
+This option is option is deprecated but still works for now (use
+I<depend_jobnum> instead for new code). If I<noexport> is set true, no
+provisioning jobs (exports) are scheduled. (You can schedule them later with
+the B<reexport> method for each cust_pkg object. Using the B<reexport> method
+on the cust_main object is not recommended, as existing services will also be
+reexported.)
+
=item ticket_subject
Optional subject for a ticket created and attached to this customer
join(', ', map { "$_: $opt->{$_}" } keys %$opt ). "\n"
if $DEBUG;
+ local $FS::svc_Common::noexport_hack = 1 if $opt->{'noexport'};
+
my $cust_pkg = $opt->{'cust_pkg'};
my $svcs = $opt->{'svcs'} || [];
}
$cust_pkg->locationnum($opt->{'cust_location'}->locationnum);
- } else {
+ } elsif ( ! $cust_pkg->locationnum ) {
$cust_pkg->locationnum($self->ship_locationnum);
'custnum' => $self->custnum,
'main_pkgnum' => $cust_pkg->pkgnum,
# try to prevent as many surprises as possible
- 'pkgbatch' => $cust_pkg->pkgbatch,
- 'start_date' => $cust_pkg->start_date,
- 'order_date' => $cust_pkg->order_date,
- 'expire' => $cust_pkg->expire,
- 'adjourn' => $cust_pkg->adjourn,
- 'contract_end' => $cust_pkg->contract_end,
- 'refnum' => $cust_pkg->refnum,
- 'discountnum' => $cust_pkg->discountnum,
- 'waive_setup' => $cust_pkg->waive_setup,
'allow_pkgpart' => $opt->{'allow_pkgpart'},
+ map { $_ => $cust_pkg->$_() }
+ qw( pkgbatch
+ start_date order_date expire adjourn contract_end
+ refnum discountnum waive_setup
+ )
});
- $error = $self->order_pkg('cust_pkg' => $pkg,
+ $error = $self->order_pkg('cust_pkg' => $pkg,
'locationnum' => $cust_pkg->locationnum);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
This can be used to defer provisioning until some action completes (such
as running the customer's credit card successfully).
-The I<noexport> option is deprecated. If I<noexport> is set true, no
+The I<noexport> option is deprecated but still works for now (use
+I<depend_jobnum> instead for new code). If I<noexport> is set true, no
provisioning jobs (exports) are scheduled. (You can schedule them later with
the B<reexport> method for each cust_pkg object. Using the B<reexport> method
on the cust_main object is not recommended, as existing services will also be
@cust_pkg = $self->_cust_pkg($extra_qsearch);
}
+ local($skip_label_sort) = 1 if $extra_qsearch->{skip_label_sort};
map { $_ } sort sort_packages @cust_pkg;
+
}
=item cust_pkg
}
+ local($skip_label_sort) = 1 if $extra_qsearch->{skip_label_sort};
sort sort_packages @cust_pkg;
}
+=item cancelled_pkgs [ EXTRA_QSEARCH_PARAMS_HASHREF ]
+
+Returns all cancelled packages (see L<FS::cust_pkg>) for this customer.
+
+=cut
+
+sub cancelled_pkgs {
+ my $self = shift;
+ my $extra_qsearch = ref($_[0]) ? shift : { @_ };
+
+ return $self->num_cancelled_pkgs($extra_qsearch) unless wantarray;
+
+ $extra_qsearch->{'extra_sql'} .=
+ ' AND cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel > 0 ';
+
+ local($skip_label_sort) = 1 if $extra_qsearch->{skip_label_sort};
+
+ sort sort_packages $self->_cust_pkg($extra_qsearch);
+}
+
sub _cust_pkg {
my $self = shift;
my $extra_qsearch = ref($_[0]) ? shift : {};
return 0 if !$a_num_cust_svc && !$b_num_cust_svc;
return -1 if $a_num_cust_svc && !$b_num_cust_svc;
return 1 if !$a_num_cust_svc && $b_num_cust_svc;
- return 0 if $a_num_cust_svc + $b_num_cust_svc > 20; #for perf, just give up
+ return 0 if $skip_label_sort
+ || $a_num_cust_svc + $b_num_cust_svc > 20; #for perf, just give up
my @a_cust_svc = $a->cust_svc_unsorted;
my @b_cust_svc = $b->cust_svc_unsorted;
return 0 if !scalar(@a_cust_svc) && !scalar(@b_cust_svc);
grep { $_->susp } $self->ncancelled_pkgs;
}
-=item unflagged_suspended_pkgs
-
-Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
-customer (thouse packages without the `manual_flag' set).
-
-=cut
+### This appears to be unused, will be going away
+#
+#=item unflagged_suspended_pkgs
+#
+#Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
+#customer (thouse packages without the `manual_flag' set).
+#
+#=cut
sub unflagged_suspended_pkgs {
my $self = shift;
=cut
+#recurring_pkgs? different from cust_pkg idea of "active" which has
+# a setup vs not_yet_billed which doesn't
sub active_pkgs {
my $self = shift;
grep { my $part_pkg = $_->part_pkg;
$self->unsuspended_pkgs;
}
+=item ncancelled_active_pkgs
+
+Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer that
+are active (recurring).
+
+=cut
+
+#ncancelled_recurring_pkgs? different from cust_pkg idea of "active" which has
+# a setup vs not_yet_billed which doesn't
+sub ncancelled_active_pkgs {
+ my $self = shift;
+ grep { my $part_pkg = $_->part_pkg;
+ $part_pkg->freq ne '' && $part_pkg->freq ne '0';
+ }
+ $self->ncancelled_pkgs;
+}
+
=item billing_pkgs
Returns active packages, and also any suspended packages which are set to
shift->num_pkgs("cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0");
}
+=item num_ncancelled_pkgs
+
+Returns the number of packages that have not been cancelled (see L<FS::cust_pkg>) for this
+customer.
+
+=cut
+
sub num_ncancelled_pkgs {
shift->num_pkgs("( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )");
}
+=item num_billing_pkgs
+
+Returns the number of packages that have not been cancelled
+and have a non-zero billing frequency (see L<FS::cust_pkg>)
+for this customer.
+
+=cut
+
+sub num_billing_pkgs {
+ my $self = shift;
+ my $opt = shift || {};
+ $opt->{addl_from} .= ' LEFT JOIN part_pkg USING (pkgpart)';
+ $opt->{extra_sql} .= ' AND ' if $opt->{extra_sql};
+ $opt->{extra_sql} .= "freq IS NOT NULL AND freq != '0'";
+ $self->num_ncancelled_pkgs($opt);
+}
+
sub num_suspended_pkgs {
shift->num_pkgs(" ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
AND cust_pkg.susp IS NOT NULL AND cust_pkg.susp != 0 ");
$sth->fetchrow_arrayref->[0];
}
+=item num_usage_pkgs
+
+Returns the number of packages for this customer that have services that
+can have RADIUS usage statistics.
+
+=cut
+
+sub num_usage_pkgs {
+ my $self = shift;
+ # have to enumerate exportnums but it's not bad
+ my @exportnums = map { $_->exportnum }
+ grep { $_->can('usage_sessions') }
+ qsearch('part_export');
+ return 0 if !@exportnums;
+ my $in_exportnums = join(',', @exportnums);
+ my $sql = "SELECT COUNT(DISTINCT pkgnum) FROM cust_pkg
+ JOIN cust_svc USING (pkgnum)
+ JOIN export_svc USING (svcpart)
+ WHERE exportnum IN( $in_exportnums ) AND custnum = ?";
+ FS::Record->scalar_sql($sql, $self->custnum);
+}
+
+=item display_recurring
+
+Returns an array of hash references, one for each recurring freq
+on billable customer packages, with keys of freq, freq_pretty and amount
+(the amount that this customer will next be charged at the given frequency.)
+
+Results will be numerically sorted by freq.
+
+Only intended for display purposes, not used for actual billing.
+
+=cut
+
+sub display_recurring {
+ my $cust_main = shift;
+
+ my $sth = dbh->prepare("
+ SELECT DISTINCT freq FROM cust_pkg LEFT JOIN part_pkg USING (pkgpart)
+ WHERE freq IS NOT NULL AND freq != '0'
+ AND ( cancel IS NULL OR cancel = 0 )
+ AND custnum = ?
+ ") or die $DBI::errstr;
+
+ $sth->execute($cust_main->custnum) or die $sth->errstr;
+
+ #not really a numeric sort because freqs can actually be all sorts of things
+ # but good enough for the 99% cases of ordering monthly quarterly annually
+ my @freqs = sort { $a <=> $b } map { $_->[0] } @{ $sth->fetchall_arrayref };
+
+ $sth->finish;
+
+ my @out;
+
+ foreach my $freq (@freqs) {
+
+ my @cust_pkg = qsearch({
+ 'table' => 'cust_pkg',
+ 'addl_from' => 'LEFT JOIN part_pkg USING (pkgpart)',
+ 'hashref' => { 'custnum' => $cust_main->custnum, },
+ 'extra_sql' => 'AND ( cancel IS NULL OR cancel = 0 )
+ AND freq = '. dbh->quote($freq),
+ 'order_by' => 'ORDER BY COALESCE(start_date,0), pkgnum', # to ensure old pkgs come before change_to_pkg
+ }) or next;
+
+ my $freq_pretty = $cust_pkg[0]->part_pkg->freq_pretty;
+
+ my $amount = 0;
+ my $skip_pkg = {};
+ foreach my $cust_pkg (@cust_pkg) {
+ my $part_pkg = $cust_pkg->part_pkg;
+ next if $cust_pkg->susp
+ && ! $cust_pkg->option('suspend_bill')
+ && ( ! $part_pkg->option('suspend_bill')
+ || $cust_pkg->option('no_suspend_bill')
+ );
+
+ #pkg change handling
+ next if $skip_pkg->{$cust_pkg->pkgnum};
+ if ($cust_pkg->change_to_pkgnum) {
+ #if change is on or before next bill date, use new pkg
+ next if $cust_pkg->expire <= $cust_pkg->bill;
+ #if change is after next bill date, use old (this) pkg
+ $skip_pkg->{$cust_pkg->change_to_pkgnum} = 1;
+ }
+
+ my $pkg_amount = 0;
+
+ #add recurring amounts for this package and its billing add-ons
+ foreach my $l_part_pkg ( $part_pkg->self_and_bill_linked ) {
+ $pkg_amount += $l_part_pkg->base_recur($cust_pkg);
+ }
+
+ #subtract amounts for any active discounts
+ #(there should only be one at the moment, otherwise this makes no sense)
+ foreach my $cust_pkg_discount ( $cust_pkg->cust_pkg_discount_active ) {
+ my $discount = $cust_pkg_discount->discount;
+ #and only one of these for each
+ $pkg_amount -= $discount->amount;
+ $pkg_amount -= $pkg_amount * $discount->percent/100;
+ }
+
+ $pkg_amount *= ( $cust_pkg->quantity || 1 );
+
+ $amount += $pkg_amount;
+
+ } #foreach $cust_pkg
+
+ next unless $amount;
+ push @out, {
+ 'freq' => $freq,
+ 'freq_pretty' => $freq_pretty,
+ 'amount' => $amount,
+ };
+
+ } #foreach $freq
+
+ return @out;
+}
+
=back
=head1 BUGS