$unearned ? ( $money_char. '%.2f unearned revenue' ) : (),
],
'header' => [
+ @pkgnum_header,
+ emt('Pkg Def'),
emt('Description'),
+ @post_desc_header,
( $unearned
- ? ( emt('Unearned'), emt('Owed'), emt('Payment date') )
+ ? ( emt('Unearned'),
+ emt('Owed'), # useful in 'paid' mode?
+ emt('Payment date') )
: ( emt('Setup charge') )
),
( $use_usage eq 'usage'
),
emt('Invoice'),
emt('Date'),
+ emt('Paid'),
+ emt('Credited'),
FS::UI::Web::cust_header(),
],
'fields' => [
+ @pkgnum,
sub { $_[0]->pkgnum > 0
- ? $_[0]->get('pkg') # possibly use override.pkg
- : $_[0]->get('itemdesc') # but i think this correct
+ # possibly use override.pkg but i think this correct
+ ? $_[0]->get('pkgpart')
+ : ''
},
+ sub { $_[0]->pkgnum > 0
+ # possibly use override.pkg but i think this correct
+ ? $_[0]->get('pkg')
+ : $_[0]->get('itemdesc')
+ },
+ @post_desc,
#strikethrough or "N/A ($amount)" or something these when
# they're not applicable to pkg_tax search
sub { my $cust_bill_pkg = shift;
if ( $unearned ) {
- my $period =
- $cust_bill_pkg->edate - $cust_bill_pkg->sdate;
- my $elapsed = $unearned - $cust_bill_pkg->sdate;
- $elapsed = 0 if $elapsed < 0;
- my $remaining = 1 - $elapsed/$period;
-
- sprintf($money_char. '%.2f',
- $remaining * $cust_bill_pkg->recur );
+ sprintf($money_char.'%.2f',
+ $cust_bill_pkg->unearned_revenue)
} else {
sprintf($money_char.'%.2f', $cust_bill_pkg->setup );
),
sub { my $row = shift;
my $value = 0;
- if ( $use_usage eq 'recurring' ) {
+ if ( $use_usage eq 'recurring' or $unearned ) {
$value = $row->recur - $row->usage;
} elsif ( $use_usage eq 'usage' ) {
$value = $row->usage;
},
( $unearned
? ( sub { time2str('%b %d %Y', shift->sdate ) },
- sub { time2str('%b %d %Y', shift->edate ) },
+ # shift edate back a day
+ # 82799 = 3600*23 - 1
+ # (to avoid skipping a day during DST)
+ sub { time2str('%b %d %Y', shift->edate - 82799 ) },
)
: ()
),
'invnum',
sub { time2str('%b %d %Y', shift->_date ) },
+ sub { sprintf($money_char.'%.2f', shift->get('pay_amount')) },
+ sub { sprintf($money_char.'%.2f', shift->get('credit_amount')) },
\&FS::UI::Web::cust_fields,
],
'sort_fields' => [
+ @pkgnum_null,
+ '',
'',
+ @post_desc_null,
'setup', #broken in $unearned case i guess
( $unearned ? ('', '') : () ),
- ( $use_usage eq 'recurring' ? 'recur - usage' :
- $use_usage eq 'usage' ? 'usage'
- : 'recur'
+ ( $use_usage eq 'recurring' or $unearned
+ ? 'recur - usage' :
+ $use_usage eq 'usage'
+ ? 'usage'
+ : 'recur'
),
( $unearned ? ('sdate', 'edate') : () ),
'invnum',
'_date',
+ #'pay_amount',
+ #'credit_amount',
],
'links' => [
- #'',
+ @pkgnum_null,
'',
'',
+ @post_desc_null,
+ '',
( $unearned ? ( '', '' ) : () ),
'',
( $unearned ? ( '', '' ) : () ),
$ilink,
$ilink,
+ $pay_link,
+ $credit_link,
( map { $_ ne 'Cust. Status' ? $clink : '' }
FS::UI::Web::cust_header()
),
],
#'align' => 'rlrrrc'.FS::UI::Web::cust_aligns(),
- 'align' => 'lr'.
+ 'align' => $pkgnum_align.
+ 'rl'.
+ $post_desc_align.
+ 'r'.
( $unearned ? 'rc' : '' ).
'r'.
( $unearned ? 'cc' : '' ).
- 'rc'.
+ 'rcrr'.
FS::UI::Web::cust_aligns(),
'color' => [
- #'',
+ @pkgnum_null,
+ '',
'',
+ @post_desc_null,
'',
( $unearned ? ( '', '' ) : () ),
'',
( $unearned ? ( '', '' ) : () ),
'',
'',
+ '',
+ '',
FS::UI::Web::cust_colors(),
],
'style' => [
- #'',
+ @pkgnum_null,
'',
'',
+ @post_desc_null,
+ '',
( $unearned ? ( '', '' ) : () ),
'',
( $unearned ? ( '', '' ) : () ),
'',
'',
+ '',
+ '',
FS::UI::Web::cust_styles(),
],
&>
#LOTS of false laziness below w/cust_credit_bill_pkg.cgi
-die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied" unless $curuser->access_right('Financial reports');
my $conf = new FS::Conf;
my $unearned = '';
+my $unearned_mode = '';
+my $unearned_base = '';
+my $unearned_sql = '';
+
+my @select = ( 'cust_bill_pkg.*', 'cust_bill._date' );
+
+my @pkgnum_header = ();
+my @pkgnum = ();
+my @pkgnum_null;
+my $pkgnum_align = '';
+if ( $curuser->option('show_pkgnum') ) {
+ push @select, 'cust_bill_pkg.pkgnum';
+ push @pkgnum_header, 'Pkg Num';
+ push @pkgnum, sub { $_[0]->pkgnum > 0 ? $_[0]->pkgnum : '' };
+ push @pkgnum_null, '';
+ $pkgnum_align .= 'r';
+}
+
+my @post_desc_header = ();
+my @post_desc = ();
+my @post_desc_null = ();
+my $post_desc_align = '';
+if ( $conf->exists('enable_taxclasses') ) {
+ push @post_desc_header, 'Tax class';
+ push @post_desc, 'taxclass';
+ push @post_desc_null, '';
+ $post_desc_align .= 'l';
+ push @select, 'part_pkg.taxclass'; # or should this use override?
+}
#here is the agent virtualization
my $agentnums_sql =
my @where = ( $agentnums_sql );
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
-push @where, "_date >= $beginning",
- "_date <= $ending";
+
+if ( $cgi->param('status') =~ /^([a-z]+)$/ ) {
+ push @where, FS::cust_main->cust_status_sql . " = '$1'";
+}
+
+if ( $cgi->param('distribute') == 1 ) {
+ push @where, "sdate <= $ending",
+ "edate > $beginning",
+ ;
+}
+else {
+ push @where, "cust_bill._date >= $beginning",
+ "cust_bill._date <= $ending";
+}
if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
push @where, "cust_main.agentnum = $1";
}
+if ( $cgi->param('refnum') =~ /^(\d+)$/ ) {
+ push @where, "cust_main.refnum = $1";
+}
+
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
+ if @classnums;
+}
+
+
+# custnum
+if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+ push @where, "cust_main.custnum = $1";
+}
+
#classnum
# not specified: all classes
# 0: empty class
}
-my @loc_param = qw( city county state country );
+my @loc_param = qw( district city county state country );
if ( $cgi->param('out') ) {
my %ph = ( 'county' => dbh->quote($_),
map { $_ => dbh->quote( $cgi->param($_) ) }
- qw( city state country )
+ qw( district city state country )
);
my ( $loc_sql, @param ) = FS::cust_pkg->location_sql;
#warn "neither nottax nor istax parameters specified";
}
- if ( $cgi->param('taxclassNULL') ) {
-
+ if ( $cgi->param('taxclassNULL')
+ && ! $cgi->param('istax') #no part_pkg.taxclass in this case
+ #(see comment above?)
+ )
+ {
my %hash = ( 'country' => scalar($cgi->param('country')) );
foreach (qw( state county )) {
$hash{$_} = scalar($cgi->param($_)) if $cgi->param($_);
push @where, FS::tax_rate_location->location_sql(
map { $_ => (scalar($cgi->param($_)) || '') }
- qw( city county state locationtaxid )
+ qw( district city county state locationtaxid )
);
-} elsif ( $cgi->param('unearned_now') =~ /^(\d+)$/ ) {
+}
+
+# unearned revenue mode
+if ( $cgi->param('unearned_now') =~ /^(\d+)$/ ) {
$unearned = $1;
+ $unearned_mode = $cgi->param('mode');
push @where, "cust_bill_pkg.sdate < $unearned",
"cust_bill_pkg.edate > $unearned",
"cust_bill_pkg.recur != 0",
- "part_pkg.freq != '0'",
+ "part_pkg.freq != '0'";
+
+ if ( !$cgi->param('include_monthly') ) {
+ push @where,
"part_pkg.freq != '1'",
"part_pkg.freq NOT LIKE '%h'",
"part_pkg.freq NOT LIKE '%d'",
"part_pkg.freq NOT LIKE '%w'";
+ }
+
+ my $usage_sql = FS::cust_bill_pkg->usage_sql;
+ push @select, "($usage_sql) AS usage"; # we need this
+ my $paid_sql = 'GREATEST(' .
+ FS::cust_bill_pkg->paid_sql($unearned, '', setuprecur => 'recur') .
+ " - $usage_sql, 0)";
+
+ push @select, "$paid_sql AS paid_no_usage"; # need this either way
+
+ if ( $unearned_mode eq 'paid' ) {
+ # then use the amount paid, minus usage charges
+ $unearned_base = $paid_sql;
+ }
+ else {
+ # use the amount billed, minus usage charges and credits
+ $unearned_base = "GREATEST( cust_bill_pkg.recur - ".
+ FS::cust_bill_pkg->credited_sql($unearned, '', setuprecur => 'recur') .
+ " - $usage_sql, 0)";
+ # include only rows that have some non-usage, non-credited portion
+ }
+ # whatever we're using as the base, only show rows where it's positive
+ push @where, "$unearned_base > 0";
+
+ my $period = "CAST(cust_bill_pkg.edate - cust_bill_pkg.sdate AS REAL)";
+ my $elapsed = "GREATEST( $unearned - cust_bill_pkg.sdate, 0 )";
+ my $remaining = "(1 - $elapsed/$period)";
+
+ $unearned_sql = "CAST( $unearned_base * $remaining AS DECIMAL(10,2) )";
+ push @select, "$unearned_sql AS unearned_revenue";
+
+ # last payment/credit date
+ my %t = (pay => 'cust_bill_pay', credit => 'cust_credit_bill');
+ foreach my $x (qw(pay credit)) {
+ my $table = $t{$x};
+ my $link = $table.'_pkg';
+ my $pkey = dbdef->table($table)->primary_key;
+ my $last_date_sql = "SELECT MAX(_date)
+ FROM $table JOIN $link USING ($pkey)
+ WHERE $link.billpkgnum = cust_bill_pkg.billpkgnum
+ AND $table._date <= $unearned";
+ push @select, "($last_date_sql) AS last_$x";
+ }
}
$count_query = "SELECT COUNT(DISTINCT billpkgnum), ";
}
- if ( $use_usage eq 'recurring' ) {
- $count_query .= "SUM(setup + recur - usage)";
+ if ( $unearned ) {
+ $count_query .= "SUM( $unearned_base ), SUM( $unearned_sql )";
+ } elsif ( $use_usage eq 'recurring' ) {
+ $count_query .= "SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - usage)";
} elsif ( $use_usage eq 'usage' ) {
$count_query .= "SUM(usage)";
- } elsif ( $unearned ) {
- $count_query .= "SUM(cust_bill_pkg.recur)";
} elsif ( scalar( grep( /locationtaxid/, $cgi->param ) ) ) {
$count_query .= "SUM( COALESCE(cust_bill_pkg_tax_rate_location.amount, cust_bill_pkg.setup + cust_bill_pkg.recur))";
} elsif ( $cgi->param('iscredit') eq 'rate') {
$count_query .= "SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)";
}
- if ( $unearned ) {
-
- #false laziness w/report_prepaid_income.cgi
-
- my $float = 'REAL'; #'DOUBLE PRECISION';
-
- my $period = "CAST(cust_bill_pkg.edate - cust_bill_pkg.sdate AS $float)";
- my $elapsed = "(CASE WHEN cust_bill_pkg.sdate > $unearned
- THEN 0
- ELSE ($unearned - cust_bill_pkg.sdate)
- END)";
- #my $elapsed = "CAST($unearned - cust_bill_pkg.sdate AS $float)";
+}
- my $remaining = "(1 - $elapsed/$period)";
+my $join_cust = ' JOIN cust_bill USING ( invnum )
+ LEFT JOIN cust_main USING ( custnum ) ';
- $count_query .= ", SUM($remaining * cust_bill_pkg.recur)";
-
- }
+# we want the package and its definition if available
+my $join_pkg =
+' LEFT JOIN cust_pkg USING (pkgnum)
+ LEFT JOIN part_pkg USING (pkgpart)';
+my $part_pkg = 'part_pkg';
+if ( $cgi->param('use_override') ) { #"Separate sub-packages from parents"
+ # still need the real part_pkg for tax applicability,
+ # so alias this one
+ $join_pkg .= " LEFT JOIN part_pkg AS override ON (
+ COALESCE(cust_bill_pkg.pkgpart_override, cust_pkg.pkgpart, 0) = override.pkgpart
+ )";
+ $part_pkg = 'override';
}
-my $join_cust = ' JOIN cust_bill USING ( invnum )
- LEFT JOIN cust_main USING ( custnum ) ';
-
-
-my $join_pkg;
if ( $cgi->param('nottax') ) {
- $join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum )
- LEFT JOIN part_pkg USING ( pkgpart )
- LEFT JOIN part_pkg AS override
- ON pkgpart_override = override.pkgpart ';
$join_pkg .= ' LEFT JOIN cust_location USING ( locationnum ) '
if $conf->exists('tax-pkg_address');
} elsif ( $conf->exists('tax-pkg_address') ) {
- $join_pkg .= ' LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum )
- LEFT JOIN cust_location USING ( locationnum ) ';
+ $join_pkg .= '
+ LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum )
+ LEFT JOIN cust_location ON cust_bill_pkg_tax_location.locationnum
+ = cust_location.locationnum
+ ';
#quelle kludge, somewhat false laziness w/report_tax.cgi
s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g for @where;
}
}
-} else {
+} # nottax / istax
- #die?
- warn "neiether nottax nor istax parameters specified";
- #same as before?
- $join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum )
- LEFT JOIN part_pkg USING ( pkgpart ) ';
-}
+#total payments
+my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount)
+ FROM cust_bill_pay_pkg
+ WHERE cust_bill_pkg.billpkgnum = cust_bill_pay_pkg.billpkgnum
+ ";
+push @select, "($pay_sub) AS pay_amount";
+
+#total credits
+my $credit_sub = "
+ SELECT SUM(cust_credit_bill_pkg.amount)
+ FROM cust_credit_bill_pkg
+ WHERE cust_bill_pkg.billpkgnum = cust_credit_bill_pkg.billpkgnum
+";
+push @select, "($credit_sub) AS credit_amount";
my $where = ' WHERE '. join(' AND ', @where);
$count_query .= " FROM cust_bill_pkg $join_cust $join_pkg $where";
}
-my @select = ( 'cust_bill_pkg.*',
- 'cust_bill._date', );
-
-push @select, 'part_pkg.pkg',
- 'part_pkg.freq',
- unless $cgi->param('istax');
+push @select, 'part_pkg.pkgpart',
+ 'part_pkg.pkg',
+ 'part_pkg.freq';
push @select, 'cust_main.custnum',
FS::UI::Web::cust_sql_fields();
'table' => 'cust_bill_pkg',
'addl_from' => "$join_cust $join_pkg",
'hashref' => {},
- 'select' => join(', ', @select ),
+ 'select' => join(",\n", @select ),
'extra_sql' => $where,
- 'order_by' => 'ORDER BY _date, billpkgnum',
+ 'order_by' => 'ORDER BY cust_bill._date, billpkgnum',
};
my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ];
my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
+my $pay_link = ''; #[, 'billpkgnum', ];
+my $credit_link = [ "${p}search/cust_credit_bill_pkg.html?billpkgnum=", 'billpkgnum', ];
my $conf = new FS::Conf;
my $money_char = $conf->config('money_char') || '$';
my $owed_sub = sub {
- $money_char. shift->owed_recur; #_recur :/
+ $money_char . shift->get('owed') # owed_recur is not correct here
};
-
my $payment_date_sub = sub {
#my $cust_bill_pkg = shift;
my @cust_pay = sort { $a->_date <=> $b->_date }
time2str('%b %d %Y', $cust_pay[-1]->_date );
};
+warn "\n\nQUERY:\n".Dumper($query)."\n\nCOUNT_QUERY:\n$count_query\n\n"
+ if $cgi->param('debug');
+
</%init>