From: Ivan Kohler Date: Mon, 11 Jun 2012 22:05:30 +0000 (-0700) Subject: Merge branch 'master' of git.freeside.biz:/home/git/freeside X-Git-Url: http://git.freeside.biz/gitweb/?a=commitdiff_plain;h=f89471ac01284a6d73ebffd6eaebdde79d5f25dc;hp=03ac8bd523fca8f69e6851fdbf2a1de8c2b83043;p=freeside.git Merge branch 'master' of git.freeside.biz:/home/git/freeside --- diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index e866a3f2d..4220d3c06 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -1058,42 +1058,49 @@ sub _X_show_zero { =over 4 -=item owed_sql [ BEFORE, AFTER, OPTIONS ] +=item usage_sql -Returns an SQL expression for the amount owed. BEFORE and AFTER specify -a date window. OPTIONS may include 'no_usage' (excludes usage charges) -and 'setuprecur' (set to "setup" or "recur" to limit to one or the other). +Returns an SQL expression for the total usage charges in details on +an item. =cut -sub owed_sql { +my $usage_sql = + '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0) + FROM cust_bill_pkg_detail + WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)'; + +sub usage_sql { $usage_sql } + +# this makes owed_sql, etc. much more concise +sub charged_sql { my ($class, $start, $end, %opt) = @_; my $charged = $opt{setuprecur} =~ /^s/ ? 'cust_bill_pkg.setup' : $opt{setuprecur} =~ /^r/ ? 'cust_bill_pkg.recur' : 'cust_bill_pkg.setup + cust_bill_pkg.recur'; - if ( $opt{no_usage} ) { - $charged .= ' - ' . $class->usage_sql; + if ($opt{no_usage} and $charged =~ /recur/) { + $charged = "$charged - $usage_sql" } - '(' . $charged . - ' - ' . $class->paid_sql($start, $end, %opt) . - ' - ' . $class->credited_sql($start, $end, %opt) . ')' + $charged; } -=item usage_sql -Returns an SQL expression for the total usage charges in details on -an item. +=item owed_sql [ BEFORE, AFTER, OPTIONS ] + +Returns an SQL expression for the amount owed. BEFORE and AFTER specify +a date window. OPTIONS may include 'no_usage' (excludes usage charges) +and 'setuprecur' (set to "setup" or "recur" to limit to one or the other). =cut -sub usage_sql { +sub owed_sql { my $class = shift; - "(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0) - FROM cust_bill_pkg_detail - WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)" + '(' . $class->charged_sql(@_) . + ' - ' . $class->paid_sql(@_) . + ' - ' . $class->credited_sql(@_) . ')' } =item paid_sql [ BEFORE, AFTER, OPTIONS ] @@ -1104,32 +1111,55 @@ Returns an SQL expression for the sum of payments applied to this item. sub paid_sql { my ($class, $start, $end, %opt) = @_; - $start = $start ? "AND cust_bill_pay._date <= $start" : ''; - $end = $end ? "AND cust_bill_pay._date > $end" : ''; + my $s = $start ? "AND cust_bill_pay._date <= $start" : ''; + my $e = $end ? "AND cust_bill_pay._date > $end" : ''; my $setuprecur = $opt{setuprecur} =~ /^s/ ? 'setup' : $opt{setuprecur} =~ /^r/ ? 'recur' : ''; $setuprecur &&= "AND setuprecur = '$setuprecur'"; - "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0) + + my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0) FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum) WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum - $start $end $setuprecur )"; + $s $e$setuprecur )"; + + if ( $opt{no_usage} ) { + # cap the amount paid at the sum of non-usage charges, + # minus the amount credited against non-usage charges + "LEAST($paid, ". + $class->charged_sql($start, $end, %opt) . ' - ' . + $class->credited_sql($start, $end, %opt).')'; + } + else { + $paid; + } + } sub credited_sql { my ($class, $start, $end, %opt) = @_; - $start = $start ? "AND cust_credit_bill._date <= $start" : ''; - $end = $end ? "AND cust_credit_bill._date > $end" : ''; + my $s = $start ? "AND cust_credit_bill._date <= $start" : ''; + my $e = $end ? "AND cust_credit_bill._date > $end" : ''; my $setuprecur = $opt{setuprecur} =~ /^s/ ? 'setup' : $opt{setuprecur} =~ /^r/ ? 'recur' : ''; $setuprecur &&= "AND setuprecur = '$setuprecur'"; - "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0) + + my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0) FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum) WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum - $start $end $setuprecur )"; + $s $e $setuprecur )"; + + if ( $opt{no_usage} ) { + # cap the amount credited at the sum of non-usage charges + "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')'; + } + else { + $credited; + } + } =back diff --git a/httemplate/elements/customer-table.html b/httemplate/elements/customer-table.html index fc1af69c6..a1e9732fd 100644 --- a/httemplate/elements/customer-table.html +++ b/httemplate/elements/customer-table.html @@ -250,7 +250,7 @@ Example: if ( customerArrayArray.length == 1 ) { - update_customer(customerArrayArray[1]); + update_customer(searchrow, customerArrayArray[0]); % if ( $opt{custnum_update_callback} ) { <% $opt{custnum_update_callback} %>(searchrow, '<% $opt{prefix} %>') % } diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index a84f981e3..f9dd4a20e 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -367,28 +367,44 @@ if ( $cgi->param('unearned_now') =~ /^(\d+)$/ ) { my $usage_sql = FS::cust_bill_pkg->usage_sql; push @select, "($usage_sql) AS usage"; # we need this - my $owed_sql = FS::cust_bill_pkg->owed_sql($unearned, '', - setuprecur => 'recur', no_usage => 1); - push @select, "($owed_sql) AS owed"; # also 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"; - $unearned_base = "(cust_bill_pkg.recur - $usage_sql)"; 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( - GREATEST( ( $unearned_base * $remaining ) - $owed_sql, 0 ) - AS DECIMAL(10,2) - )"; + $unearned_sql = "CAST( $unearned_base * $remaining AS DECIMAL(10,2) )"; push @select, "$unearned_sql AS unearned_revenue"; - if ( $unearned_mode eq 'billed' ) { - # include only rows that have some unearned portion - push @where, "$unearned_base > 0"; - } - elsif ( $unearned_mode eq 'paid' ) { - # include only those that have some _paid_ unearned portion - push @where, "$unearned_sql > 0"; + # 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"; } } diff --git a/httemplate/search/prepaid_income.html b/httemplate/search/prepaid_income.html index deed33a33..d02345edc 100644 --- a/httemplate/search/prepaid_income.html +++ b/httemplate/search/prepaid_income.html @@ -98,7 +98,7 @@ $dt->add(days => 1); # the day after that $dt->subtract(seconds => 1); # the last second of the report day $now = $dt->epoch; -my $link = "cust_bill_pkg.cgi?nottax=1;unearned_now=$now;mode=$mode"; +my $link = "unearned_detail.html?date=$now;mode=$mode"; if ( $cgi->param('include_monthly') ) { $link .= ';include_monthly=1'; @@ -138,17 +138,22 @@ foreach my $agentnum (@agentnums) { my( $total, $total_legacy ) = ( 0, 0 ); - # false laziness - my $usage_sql = FS::cust_bill_pkg->usage_sql; - my $owed_sql = FS::cust_bill_pkg->owed_sql($now, '', - setuprecur => 'recur', no_usage => 1); - my $unearned_base = "(cust_bill_pkg.recur - $usage_sql)"; + my @opt = ($now, '', setuprecur => 'recur', no_usage => 1); + # balance owed, recurring only, not including usage charges + my $unearned_base; + if ( $mode eq 'billed' ) { + $unearned_base = '( ' . + FS::cust_bill_pkg->charged_sql(@opt) . ' - ' . + FS::cust_bill_pkg->credited_sql(@opt) . ' )'; + } elsif ( $mode eq 'paid' ) { + $unearned_base = FS::cust_bill_pkg->paid_sql(@opt); + } my $period = "CAST(cust_bill_pkg.edate - cust_bill_pkg.sdate AS REAL)"; my $elapsed = "GREATEST( $now - cust_bill_pkg.sdate, 0 )"; my $remaining = "(1 - $elapsed/$period)"; my $unearned_sql = "CAST( - GREATEST( $unearned_base * $remaining - $owed_sql, 0 ) + GREATEST( $unearned_base * $remaining, 0 ) AS DECIMAL(10,2) )"; @@ -180,6 +185,8 @@ foreach my $agentnum (@agentnums) { AND part_pkg.freq != '0' $where "; + + warn $sql; my $sth = dbh->prepare($sql) or die dbh->errstr; $sth->execute or die $sth->errstr; my $total = $sth->fetchrow_arrayref->[0]; diff --git a/httemplate/search/unearned_detail.html b/httemplate/search/unearned_detail.html new file mode 100644 index 000000000..831625e7f --- /dev/null +++ b/httemplate/search/unearned_detail.html @@ -0,0 +1,246 @@ +<& elements/search.html, + 'title' => emt("Unearned revenue - ".ucfirst($unearned_mode)), + 'name' => emt('line items'), + 'query' => $query, + 'count_query' => $count_query, + 'count_addl' => [ $money_char. '%.2f total', + $money_char. '%.2f unearned revenue' + ], + 'header' => [ map( {emt $_} + 'Description', + 'Unearned', # depends on mode + 'Recurring charge', #recur - usage + 'Owed', #recur - usage - credits - payments + 'Paid', #payments + 'Payment date', #of last payment + 'Credit date', #of last credit + 'Charge start', + 'Charge end', + 'Invoice', + 'Date' + ), + FS::UI::Web::cust_header(), + ], + 'fields' => [ + #Description + sub { $_[0]->pkgnum > 0 + ? $_[0]->get('pkg') # possibly use override.pkg + : $_[0]->get('itemdesc') # but i think this correct + }, + #Unearned + money_sub('unearned_revenue'), + #Recurring charge + money_sub('recur_no_usage'), + #Owed + money_sub('owed_no_usage'), + #Paid + money_sub('paid_no_usage'), + #Payment date + date_sub('last_pay'), + #Credit date + date_sub('last_credit'), + #Charge start + date_sub('sdate'), + #Charge end + date_sub('edate'), + #Invoice + 'invnum', + #Invoice date + date_sub('_date'), + \&FS::UI::Web::cust_fields, + ], + 'sort_fields' => [ + 'pkg', + # SQL expressions work as sort keys... + 'unearned_revenue', + 'recur_no_usage', + 'owed_no_usage', + 'paid_no_usage', + 'last_pay', + 'last_credit', + 'sdate', + 'edate', + 'invnum', + '_date', + ], + 'links' => [ + ('' x 9), + $ilink, + $ilink, + ( map { $_ ne 'Cust. Status' ? $clink : '' } + FS::UI::Web::cust_header() + ), + ], + 'align' => 'lrrcrccrc'. FS::UI::Web::cust_aligns(), + 'color' => [ + ('' x 11), + FS::UI::Web::cust_colors(), + ], + 'style' => [ + ('' x 11), + FS::UI::Web::cust_styles(), + ], +&> +<%init> + +# Separated from cust_bill_pkg.cgi to simplify things. + +die "access denied" + unless $FS::CurrentUser::CurrentUser->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 ($join_cust, $join_pkg ) = ('', ''); + +#here is the agent virtualization +my $agentnums_sql = + $FS::CurrentUser::CurrentUser->agentnums_sql( 'table' => 'cust_main' ); + +my @where = ( $agentnums_sql ); + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); + +if ( $cgi->param('status') =~ /^([a-z]+)$/ ) { + push @where, FS::cust_main->cust_status_sql . " = '$1'"; +} + +push @where, "cust_bill._date >= $beginning", + "cust_bill._date <= $ending"; + +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + push @where, "cust_main.agentnum = $1"; +} + +# no pkgclass, no taxclass, no tax location... + +# unearned revenue mode +$cgi->param('date') =~ /^(\d+)$/ + or die "date required"; + +$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'"; + +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 @opt = ( + $unearned, #before this date + '', #after this date + setuprecur => 'recur', + no_usage => 1 +); + +my $charged = FS::cust_bill_pkg->charged_sql(@opt); +push @select, "($charged) AS recur_no_usage"; + +my $owed_sql = FS::cust_bill_pkg->owed_sql(@opt); +push @select, "($owed_sql) AS owed_no_usage"; + +my $paid_sql = FS::cust_bill_pkg->paid_sql(@opt); +push @select, "$paid_sql AS paid_no_usage"; + +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 = "( $charged - " . + FS::cust_bill_pkg->credited_sql(@opt) . ' )'; +} +# 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"; +} + +#no itemdesc +#no tax report group kludge +#no tax exemption +#usage always excluded + +# always 'nottax', not 'istax' +$join_cust = ' JOIN cust_bill USING ( invnum ) + LEFT JOIN cust_main USING ( custnum ) '; + +$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 '; + +my $where = ' WHERE '. join(' AND ', @where); + +my $count_query = "SELECT COUNT(DISTINCT billpkgnum), + SUM( $unearned_base ), SUM( $unearned_sql ) + FROM cust_bill_pkg $join_cust $join_pkg $where"; + +push @select, 'part_pkg.pkg', + 'part_pkg.freq', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(); + +my $query = { + 'table' => 'cust_bill_pkg', + 'addl_from' => "$join_cust $join_pkg", + 'hashref' => {}, + 'select' => join(",\n", @select ), + 'extra_sql' => $where, + '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 $money_char; + +sub money_sub { + $conf ||= new FS::Conf; + $money_char ||= $conf->config('money_char') || '$'; + my $field = shift; + sub { + $money_char . sprintf('%.2f', $_[0]->get($field)); + }; +} + +sub date_sub { + my $field = shift; + sub { + my $value = $_[0]->get($field); + $value ? time2str('%b %d %Y', $value) : ''; + }; +}; + +