From: jeff Date: Wed, 28 Oct 2009 19:01:18 +0000 (+0000) Subject: UI changes for credit applications include on the fly tax calculations #4729 X-Git-Tag: root_of_svc_elec_features~731 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=2cb70470a8e5c3287146008e4ce2c4eb9f242373 UI changes for credit applications include on the fly tax calculations #4729 --- diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 4058f1f38..016b8bf66 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -627,7 +627,8 @@ sub disintegrate { } #split usage from recur - my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage ); + my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage ) + if exists($cust_bill_pkg{recur}); warn "usage is $usage\n" if $DEBUG > 1; if ($usage) { my $cust_bill_pkg_usage = diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index 6c3effa13..fda10decf 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -306,6 +306,9 @@ sub check { return "amount must be > 0 " if $self->amount <= 0; + return "amount must be greater or equal to amount applied" + if $self->unapplied < 0; + return "Unknown customer" unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 1c6284976..700e15a79 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -2623,6 +2623,141 @@ sub bill { } + my $listref_or_error = + $self->calculate_taxes( \@cust_bill_pkg, \%taxlisthash, $invoice_time); + + unless ( ref( $listref_or_error ) ) { + $dbh->rollback if $oldAutoCommit; + return $listref_or_error; + } + + foreach my $taxline ( @$listref_or_error ) { + $total_setup = sprintf('%.2f', $total_setup+$taxline->setup ); + push @cust_bill_pkg, $taxline; + } + + #add tax adjustments + warn "adding tax adjustments...\n" if $DEBUG > 2; + foreach my $cust_tax_adjustment ( + qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum, + 'billpkgnum' => '', + } + ) + ) { + + my $tax = sprintf('%.2f', $cust_tax_adjustment->amount ); + + my $itemdesc = $cust_tax_adjustment->taxname; + $itemdesc = '' if $itemdesc eq 'Tax'; + + push @cust_bill_pkg, new FS::cust_bill_pkg { + 'pkgnum' => 0, + 'setup' => $tax, + 'recur' => 0, + 'sdate' => '', + 'edate' => '', + 'itemdesc' => $itemdesc, + 'itemcomment' => $cust_tax_adjustment->comment, + 'cust_tax_adjustment' => $cust_tax_adjustment, + #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location, + }; + + } + + my $charged = sprintf('%.2f', $total_setup + $total_recur ); + + my @cust_bill = $self->cust_bill; + my $balance = $self->balance; + my $previous_balance = scalar(@cust_bill) + ? ( $cust_bill[$#cust_bill]->billing_balance || 0 ) + : 0; + + $previous_balance += $cust_bill[$#cust_bill]->charged + if scalar(@cust_bill); + #my $balance_adjustments = + # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged); + + #create the new invoice + my $cust_bill = new FS::cust_bill ( { + 'custnum' => $self->custnum, + '_date' => ( $invoice_time ), + 'charged' => $charged, + 'billing_balance' => $balance, + 'previous_balance' => $previous_balance, + 'invoice_terms' => $options{'invoice_terms'}, + } ); + $error = $cust_bill->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "can't create invoice for customer #". $self->custnum. ": $error"; + } + + foreach my $cust_bill_pkg ( @cust_bill_pkg ) { + $cust_bill_pkg->invnum($cust_bill->invnum); + my $error = $cust_bill_pkg->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "can't create invoice line item: $error"; + } + } + + + foreach my $hook ( @precommit_hooks ) { + eval { + &{$hook}; #($self) ? + }; + if ( $@ ) { + $dbh->rollback if $oldAutoCommit; + return "$@ running precommit hook $hook\n"; + } + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; #no error +} + +=item calculate_taxes LINEITEMREF TAXHASHREF INVOICE_TIME + +This is a weird one. Perhaps it should not even be exposed. + +Generates tax line items (see L) for this customer. +Usually used internally by bill method B. + +If there is an error, returns the error, otherwise returns reference to a +list of line items suitable for insertion. + +=over 4 + +=item LINEITEMREF + +An array ref of the line items being billed. + +=item TAXHASHREF + +A strange beast. The keys to this hash are internal identifiers consisting +of the name of the tax object type, a space, and its unique identifier ( e.g. + 'cust_main_county 23' ). The values of the hash are listrefs. The first +item in the list is the tax object. The remaining items are either line +items or floating point values (currency amounts). + +The taxes are calculated on this entity. Calculated exemption records are +transferred to the LINEITEMREF items on the assumption that they are related. + +Read the source. + +=item INVOICE_TIME + +This specifies the date appearing on the associated invoice. Some +jurisdictions (i.e. Texas) have tax exemptions which are date sensitive. + +=back + +=cut +sub calculate_taxes { + my ($self, $cust_bill_pkg, $taxlisthash, $invoice_time) = @_; + + my @tax_line_items = (); + warn "having a look at the taxes we found...\n" if $DEBUG > 2; # keys are tax names (as printed on invoices / itemdesc ) @@ -2641,20 +2776,18 @@ sub bill { # values are listrefs of cust_bill_pkg_tax_rate_location hashrefs my %tax_rate_location = (); - foreach my $tax ( keys %taxlisthash ) { - my $tax_object = shift @{ $taxlisthash{$tax} }; + foreach my $tax ( keys %$taxlisthash ) { + my $tax_object = shift @{ $taxlisthash->{$tax} }; warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2; - warn " ". join('/', @{ $taxlisthash{$tax} } ). "\n" if $DEBUG > 2; + warn " ". join('/', @{ $taxlisthash->{$tax} } ). "\n" if $DEBUG > 2; my $hashref_or_error = - $tax_object->taxline( $taxlisthash{$tax}, + $tax_object->taxline( $taxlisthash->{$tax}, 'custnum' => $self->custnum, 'invoice_time' => $invoice_time ); - unless ( ref($hashref_or_error) ) { - $dbh->rollback if $oldAutoCommit; - return $hashref_or_error; - } - unshift @{ $taxlisthash{$tax} }, $tax_object; + return $hashref_or_error unless ref($hashref_or_error); + + unshift @{ $taxlisthash->{$tax} }, $tax_object; my $name = $hashref_or_error->{'name'}; my $amount = $hashref_or_error->{'amount'}; @@ -2694,9 +2827,9 @@ sub bill { } #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit - my %packagemap = map { $_->pkgnum => $_ } @cust_bill_pkg; - foreach my $tax ( keys %taxlisthash ) { - foreach ( @{ $taxlisthash{$tax} }[1 ... scalar(@{ $taxlisthash{$tax} })] ) { + my %packagemap = map { $_->pkgnum => $_ } @$cust_bill_pkg; + foreach my $tax ( keys %$taxlisthash ) { + foreach ( @{ $taxlisthash->{$tax} }[1 ... scalar(@{ $taxlisthash->{$tax} })] ) { next unless ref($_) eq 'FS::cust_bill_pkg'; push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg }, @@ -2726,7 +2859,6 @@ sub bill { next unless $tax; $tax = sprintf('%.2f', $tax ); - $total_setup = sprintf('%.2f', $total_setup+$tax ); my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname, 'disabled' => '', @@ -2745,7 +2877,7 @@ sub bill { } - push @cust_bill_pkg, new FS::cust_bill_pkg { + push @tax_line_items, new FS::cust_bill_pkg { 'pkgnum' => 0, 'setup' => $tax, 'recur' => 0, @@ -2759,88 +2891,9 @@ sub bill { } - #add tax adjustments - warn "adding tax adjustments...\n" if $DEBUG > 2; - foreach my $cust_tax_adjustment ( - qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum, - 'billpkgnum' => '', - } - ) - ) { - - my $tax = sprintf('%.2f', $cust_tax_adjustment->amount ); - $total_setup = sprintf('%.2f', $total_setup+$tax ); - - my $itemdesc = $cust_tax_adjustment->taxname; - $itemdesc = '' if $itemdesc eq 'Tax'; - - push @cust_bill_pkg, new FS::cust_bill_pkg { - 'pkgnum' => 0, - 'setup' => $tax, - 'recur' => 0, - 'sdate' => '', - 'edate' => '', - 'itemdesc' => $itemdesc, - 'itemcomment' => $cust_tax_adjustment->comment, - 'cust_tax_adjustment' => $cust_tax_adjustment, - #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location, - }; - - } - - my $charged = sprintf('%.2f', $total_setup + $total_recur ); - - my @cust_bill = $self->cust_bill; - my $balance = $self->balance; - my $previous_balance = scalar(@cust_bill) - ? ( $cust_bill[$#cust_bill]->billing_balance || 0 ) - : 0; - - $previous_balance += $cust_bill[$#cust_bill]->charged - if scalar(@cust_bill); - #my $balance_adjustments = - # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged); - - #create the new invoice - my $cust_bill = new FS::cust_bill ( { - 'custnum' => $self->custnum, - '_date' => ( $invoice_time ), - 'charged' => $charged, - 'billing_balance' => $balance, - 'previous_balance' => $previous_balance, - 'invoice_terms' => $options{'invoice_terms'}, - } ); - $error = $cust_bill->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "can't create invoice for customer #". $self->custnum. ": $error"; - } - - foreach my $cust_bill_pkg ( @cust_bill_pkg ) { - $cust_bill_pkg->invnum($cust_bill->invnum); - my $error = $cust_bill_pkg->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "can't create invoice line item: $error"; - } - } - - - foreach my $hook ( @precommit_hooks ) { - eval { - &{$hook}; #($self) ? - }; - if ( $@ ) { - $dbh->rollback if $oldAutoCommit; - return "$@ running precommit hook $hook\n"; - } - } - - $dbh->commit or die $dbh->errstr if $oldAutoCommit; - ''; #no error + \@tax_line_items; } - sub _make_lines { my ($self, %params) = @_; diff --git a/httemplate/edit/elements/ApplicationCommon.html b/httemplate/edit/elements/ApplicationCommon.html index b46a3c8fe..181430c5a 100644 --- a/httemplate/edit/elements/ApplicationCommon.html +++ b/httemplate/edit/elements/ApplicationCommon.html @@ -39,10 +39,12 @@ Examples: ) -<% include('/elements/header-popup.html', "Apply $src_thing$to" ) %> + +<% include('/elements/header-popup.html', "Apply $src_thing$to", '', 'onLoad="myOnLoadFunction();"') %> <% include('/elements/error.html') %> +

<% $src_thing %> #<% $src_pkeyvalue %>
@@ -57,18 +59,26 @@ Examples: Amount: - <% $money_char %><% $src->amount %> + <% $money_char %><% $src_amount %> + + +% if ($use_sub_dst_thing && $can_change_credit) { + + +% } + + Unapplied amount: - <% $money_char %><% $unapplied %> + <% $money_char %><% $unapplied %> % if ( $src_table eq 'cust_credit' ) { Reason: - <% $src->reason %> + <% $src->reason %> % } @@ -81,10 +91,16 @@ function changed(what) { if ( dst == '' ) { what.form.submit.disabled=true; +%if ($src_pkey eq 'crednum') { + what.form.tax_button.disabled=true; +%} return true; } what.form.submit.disabled=false; +%if ($src_pkey eq 'crednum') { + what.form.tax_button.disabled=false; +%} % foreach my $dst ( @dst ) { @@ -120,14 +136,14 @@ function changed(what) { % my $owed = $taxX->owed; % my $key = &{$key_generator}([ $cbp, 0, { $pkey => $taxX->$pkey } ]); % my $toapp = exists($apphash{ $key }) ? $apphash{ $key }->[1] : 0; - <% &{$row_generator}( $cbp, $taxX->desc, $owed, $toapp, $taxX->$pkey ) %> + <% &{$row_generator}( $key, $cbp, $taxX->desc, $owed, $toapp, $taxX->$pkey ) %> % $total_owed -= $owed; % $amount -= $toapp; % } % $desc .= ' (default)'; % } % if ( $total_owed > 0 ) { - <% &{$row_generator}($cbp, $desc, $total_owed, $amount, '') %> + <% &{$row_generator}($key, $cbp, $desc, $total_owed, $amount, '') %> % } % } % } @@ -143,17 +159,156 @@ function sub_changed(what) { var table = document.getElementById('ApplicationTable'); var i = table.rows.length; while(i-- > 2) { - var amount_input = table.rows[i].getElementsByTagName('input').item(0); - amount += parseFloat( amount_input.value ) || 0; + var inputs = table.rows[i].getElementsByTagName('input'); + if (! inputs.length) { + continue; + } + amount += parseFloat( inputs.item(0).value ) || 0; } what.form.amount.value = parseFloat(amount).toFixed(2); what.form.display_amount.value = parseFloat(amount).toFixed(2); + set_amount_color(what); } + +function set_amount_color(what) { + if (what.form.src_amount.value < what.form.amount.value) { + what.form.display_amount.style.color = '#ff0000'; + } else { + what.form.display_amount.style.color = '#00ff00'; + } +} + Apply to: +% if ($use_sub_dst_thing && $src_pkey eq 'crednum') { +
+<% include( '/elements/xmlhttp.html', + 'url' => $p.'misc/xmlhttp-calculate_taxes.html', + 'subs' => [ 'calculate_taxes' ], + ) + %> + + +%} + @@ -173,7 +328,7 @@ Apply to: % if ($use_sub_dst_thing) { - + % } @@ -184,6 +339,14 @@ Apply to: + + <% include('/elements/footer.html') %> <%init> @@ -209,13 +372,21 @@ my $use_sub_dst_thing = 0; $use_sub_dst_thing = 1 if ( $dst_table eq 'cust_bill' && $conf->exists("${link_table}_pkg-manual") ); +my $can_change_credit = 0; +$can_change_credit = 1 + if ( $src_table eq 'cust_credit' && + $FS::CurrentUser::CurrentUser->access_right('Post credit') && + $FS::CurrentUser::CurrentUser->access_right('Delete credit') + ); + my $to = $dst_table eq 'cust_refund' ? ' to Refund' : ''; -my($src_pkeyvalue, $amount, $dst_pkeyvalue); +my($src_pkeyvalue, $amount, $dst_pkeyvalue, $src_amount); if ( $cgi->param('error') ) { $src_pkeyvalue = $cgi->param($src_pkey); $amount = $cgi->param('amount'); $dst_pkeyvalue = $cgi->param($dst_pkey); + $src_amount = $cgi->param('src_amount'); } else { my($query) = $cgi->keywords; $query =~ /^(\d+)$/; @@ -231,6 +402,8 @@ my $p1 = popurl(1); my $src = qsearchs($src_table, { $src_pkey => $src_pkeyvalue } ); die "$src_thing $src_pkeyvalue not found!" unless $src; +$src_amount = $src->amount unless $cgi->param('error'); + my $unapplied = $src->unapplied; my @dst = sort { $a->_date <=> $b->_date @@ -240,9 +413,11 @@ my @dst = sort { $a->_date <=> $b->_date qsearch($dst_table, { 'custnum' => $src->custnum } ); my $row_generator = sub { - my ($cust_bill_pkg, $desc, $owed, $amount, $taxXnum) = @_; + my ($key, $cust_bill_pkg, $desc, $owed, $amount, $taxXnum) = @_; + my ($num, $s_or_r, $taxlinenum) = split(':', $key); my $id = $cust_bill_pkg->pkgnum || 'Tax'; my $billpkgnum = $cust_bill_pkg->billpkgnum; + my $s_or_r = $cust_bill_pkg->setup > 0 ? 'setup' : 'recur'; $amount = sprintf("%.2f", $amount); qq! @@ -277,6 +452,20 @@ my $row_generator = sub { taxnum_input.setAttribute('rownum', rownum); taxnum_input.setAttribute('value', "$taxXnum"); amount_cell.appendChild(taxnum_input); + var s_or_r_input = document.createElement('INPUT'); + s_or_r_input.setAttribute('name', 's_or_r'+rownum); + s_or_r_input.setAttribute('id', 's_or_r'+rownum); + s_or_r_input.setAttribute('type', 'hidden'); + s_or_r_input.setAttribute('rownum', rownum); + s_or_r_input.setAttribute('value', "$s_or_r"); + amount_cell.appendChild(s_or_r_input); + var itemdesc_input = document.createElement('INPUT'); + itemdesc_input.setAttribute('name', 'itemdesc'+rownum); + itemdesc_input.setAttribute('id', 'itemdesc'+rownum); + itemdesc_input.setAttribute('type', 'hidden'); + itemdesc_input.setAttribute('rownum', rownum); + itemdesc_input.setAttribute('value', "$desc"); + amount_cell.appendChild(itemdesc_input); row.appendChild(pkg_cell); row.appendChild(amount_cell); rownum++; @@ -294,4 +483,24 @@ my $key_generator = sub { join(':', $cust_bill_pkg->billpkgnum, $setup_or_recur, $taxlinenum); }; +my $onload = 'return true;'; + +if ($cgi->param('error')) { + + my $set_sub_amounts = + join(';', map { "myform.subamount$_.value = ". $cgi->param("subamount$_") } + grep { /.+/ } + map { /^subnum(\d+)$/ ? $1 : '' } + $cgi->param + ); + $set_sub_amounts &&= "$set_sub_amounts;sub_changed(myform.subamount0)"; + + $onload = qq! + var myform = document.getElementById('ApplicationForm'); + changed(myform.elements['$dst_pkey']); + $set_sub_amounts; + return true; + !; +} + diff --git a/httemplate/edit/process/cust_credit_bill.cgi b/httemplate/edit/process/cust_credit_bill.cgi index c0f34ae5b..d3847dc40 100755 --- a/httemplate/edit/process/cust_credit_bill.cgi +++ b/httemplate/edit/process/cust_credit_bill.cgi @@ -10,4 +10,10 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Apply credit'); +if ( $cgi->param('src_amount') ) { + die "access denied" + unless ( $FS::CurrentUser::CurrentUser->access_right('Post credit') && + $FS::CurrentUser::CurrentUser->access_right('Delete credit') ); +} + diff --git a/httemplate/edit/process/elements/ApplicationCommon.html b/httemplate/edit/process/elements/ApplicationCommon.html index 3cb7ae6bf..c7bdd3ea2 100644 --- a/httemplate/edit/process/elements/ApplicationCommon.html +++ b/httemplate/edit/process/elements/ApplicationCommon.html @@ -34,6 +34,8 @@ Examples: my %opt = @_; +my $error = ''; + my $src_thing = ucfirst($opt{'src_thing'}); my $src_table = $opt{'src_table'}; my $src_pkey = dbdef->table($src_table)->primary_key; @@ -58,6 +60,10 @@ my @subitems = map { [ $cgi->param("subnum$_"), $cgi->param("subamount$_"), $cgi my %options = (); $options{subitems} = \@subitems if scalar(@subitems); + +my $oldAutoCommit = $FS::UID::AutoCommit; +local $FS::UID::AutoCommit = 0; +my $dbh = dbh; my $new; # $new = new FS::cust_refund ( { @@ -70,6 +76,11 @@ my $new; # } ); #} else { + if ($src->amount != $cgi->param('src_amount')) { + $src->amount($cgi->param('src_amount')); + $error = $src->replace; + } + my $class = 'FS::'. $opt{link_table}; $new = $class->new( { @@ -82,6 +93,11 @@ my $new; $options{manual} = 1; -my $error = $new->insert( %options ); +$error ||= $new->insert( %options ); +if ($error) { + $dbh->rollback if $oldAutoCommit; +} else { + $dbh->commit or die $dbh->errstr if $oldAutoCommit; +}
Amount: <% $money_char %> STYLE="text-align:right;">