X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=httemplate%2Fmisc%2Fbatch-cust_pay.html;h=003dc86ac91b3fe933e4ce1734f79d01cbb170e5;hp=505f2d0ff915856b7d0337150c1f312a1d0ba63f;hb=79c8be3bd0c5db42794b36bf5b5dd25addba67cb;hpb=fed83c5c6c86a9ab00bb30481f5bf3937910da54 diff --git a/httemplate/misc/batch-cust_pay.html b/httemplate/misc/batch-cust_pay.html index 505f2d0ff..003dc86ac 100644 --- a/httemplate/misc/batch-cust_pay.html +++ b/httemplate/misc/batch-cust_pay.html @@ -1,11 +1,23 @@ -<% include('/elements/header.html', 'Quick payment entry') %> +<& /elements/header.html, { + title => 'Quick payment entry', + etc => 'onload="preload()"' +} &> -<% include('/elements/error.html') %> +<& /elements/error.html &> + -
+function add_row_callback(rownum, values) { + if (values) { + custnum_update_callback(rownum); + } else { + document.getElementById('enable_app'+rownum).disabled = true; + } +} - +function delete_row_callback(rownum) { + var i = 0; + var delbutton = document.getElementById('delete'+rownum+'.'+i); + var delrows = []; + while (delbutton) { + delrows[i] = delbutton; + i++; + delbutton = document.getElementById('delete'+rownum+'.'+i); + } + delrows = delrows.reverse(); + for (i = 0; i < delrows.length; i++) { + delrows[i].onclick(); + } +} + +function custnum_update_callback(rownum) { + var custnum = document.getElementById('custnum'+rownum).value; + // if there is a custnum and more than one open invoice, enable + // (and check) the box + var show_applications = !(custnum > 0 && num_open_invoices[rownum] > 1); + var enable_app_checkbox = document.getElementById('enable_app'+rownum); + enable_app_checkbox.disabled = show_applications; + +% if ( $use_discounts ) { + select_discount_term(rownum); +% } +} + +function invnum_update_callback(rownum) { + custnum_update_callback(rownum); +} + +function select_discount_term(row) { + var custnum_obj = document.getElementById('custnum'+row); + var select_obj = document.getElementById('discount_term'+row); + + var value = ''; + if (select_obj.type == 'hidden') { + value = select_obj.value; + } + + var term_select = document.createElement('SELECT'); + term_select.setAttribute('name', 'discount_term'+row); + term_select.setAttribute('id', 'discount_term'+row); + term_select.setAttribute('rownum', row); + term_select.style.display = ''; + select_obj.parentNode.replaceChild(term_select, select_obj); + opt(term_select, '', '1 month'); + + function select_discount_term_update(discount_terms) { + + var termArray = eval('(' + discount_terms + ')'); + for ( var t = 0; t < termArray.length; t++ ) { + opt(term_select, termArray[t][0], termArray[t][1]); + if (termArray[t][0] == value) { + term_select.selectedIndex = t+1; + } + } + + } + + discount_terms(custnum_obj.value, select_discount_term_update); + +} + +var invoices_for_row = new Object; -<% include( "/elements/customer-table.html", - name_singular => 'payment', - header => [ '', 'Amount', 'Check #', '' ], - fields => [ sub {'$'}, 'paid', 'payinfo', 'error', ], - types => [ 'immutable', '', '', 'immutable', ], - align => [ 'c', 'r', 'r', 'l' ], - sizes => [ 0, 8, 10, 0, ], - colors => [ '', '', '', '#ff0000' ], - param => { () }, - footer => [ '$', '_TOTAL', '', '' ], - footer_align => [ 'c', 'r', 'r', '' ], - ) +var preloading = 0; // the number of preloading threads currently running + +// callback from toggle_application_row: we've received a list of +// the customer's open invoices. store them. +function update_invoices(rownum, invoices) { + invoices_for_row[rownum] = new Object; + // only called before create_application_row + for ( var i=0; i -1 ) { + table_rows.removeChild(table_rows[i]); + } else { + break; + } + } + lock_payment_row(rownum, false); + } +} + +function lock_payment_row(rownum, flag) { +% foreach (qw(invnum custnum customer)) { + obj = document.getElementById('<% $_ %>'+rownum); + obj.readOnly = flag; +% } + document.getElementById('enable_app'+rownum).disabled = flag; +} + +function delete_application_row() { + var rownum = this.getAttribute('rownum'); + var appnum = this.getAttribute('appnum'); + var tr_app = document.getElementById('row'+rownum+'.'+appnum); + var select_invnum = document.getElementById('invnum'+rownum+'.'+appnum); + if ( select_invnum.value ) { + invoices_for_row[rownum][ select_invnum.value ] = select_invnum.curr_invoice; + } + + tr_app.parentNode.removeChild(tr_app); + if ( appnum > 0 ) { + document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = ''; + } + else { + lock_payment_row(rownum, false); + document.getElementById('enable_app'+rownum).checked = false; + } +} + +function amount_unapplied(rownum) { + var appnum = 0; + var total = 0; + var payment_amount = parseFloat(document.getElementById('paid'+rownum).value) + || 0; + while (true) { + var input_amount = document.getElementById('amount'+rownum+'.'+appnum); + if ( input_amount ) { + total += parseFloat(input_amount.value || 0); + appnum++; + } + else { + return payment_amount - total; + } + } +} + +var change_app_amount; + +// the user has chosen an invoice. the previously chosen invoice is still +// in curr_invoice +// - if there is a value there, put it back on the invoices_for_row list for +// this customer. +// - then _remove_ the newly chosen invoice from that list. +// - find the "owed" element for this application row and set its value to the +// amount owed on that invoice. +// - find the "amount" element for this application row and set its value to +// either "owed" or the remaining payment amount, whichever is less. +// - call change_app_amount() on that element. +function choose_app_invnum() { + var rownum = this.getAttribute('rownum'); + var appnum = this.getAttribute('appnum'); + var last_invoice = this.curr_invoice; + if ( last_invoice ) { + invoices_for_row[rownum][ last_invoice['invnum'] ] = last_invoice; + } + + if ( this.value ) { + var this_invoice = invoices_for_row[rownum][this.value]; + this.curr_invoice = invoices_for_row[rownum][this.value]; + var span_owed = document.getElementById('owed'+rownum+'.'+appnum); + span_owed.innerHTML = this_invoice['owed'] + ' '; + delete invoices_for_row[rownum][this.value]; + + var input_amount = document.getElementById('amount'+rownum+'.'+appnum); + if ( input_amount.value == '' ) { + input_amount.value = + Math.max( + 0, Math.min( amount_unapplied(rownum), this_invoice['owed']) + ).toFixed(2); + // trigger onchange + change_app_amount.call(input_amount); + } + } +} + +// the invoice selector has gained focus. clear its list of options, and +// replace them with the list of open invoices (from invoices_for_row). +// if there's already a selected invoice, prepend that to the list. +function focus_app_invnum() { + var rownum = this.getAttribute('rownum'); + var add_opt = function(obj, value, label) { + var o = document.createElement('OPTION'); + o.text = label; + o.value = value; + obj.add(o); + } + this.options.length = 0; + var this_invoice = this.curr_invoice; + if ( this_invoice ) { + add_opt(this, this_invoice.invnum, this_invoice.label); + } else { + add_opt(this, '', ''); + } + for ( var x in invoices_for_row[rownum] ) { + add_opt(this, + invoices_for_row[rownum][x].invnum, + invoices_for_row[rownum][x].label); + } +} + +// an application amount has been changed. if there's any unapplied payment +// amount, and any remaining invoices_for_row, add a blank application row. +// (but don't do this while preloading; it will unconditionally add enough +// rows to show all the attempted applications) +function change_app_amount() { + var rownum = this.getAttribute('rownum'); + var appnum = this.getAttribute('appnum'); + if ( preloading == 0 + && Object.keys(invoices_for_row[rownum]).length > 0 + && !document.getElementById( 'row'+rownum+'.'+(parseInt(appnum) + 1) ) + && amount_unapplied(rownum) > 0 ) { + + create_application_row(rownum, parseInt(appnum) + 1); + } +} + +// we're creating a payment application row. +// create the following elements: , s, "Apply to invoice" caption, +// invnum selector, "owed" display, amount input box, delete button. +function create_application_row(rownum, appnum) { + var payment_row = document.getElementById('row'+rownum); + var tr_app = document.createElement('TR'); + tr_app.setAttribute('rownum', rownum); + tr_app.setAttribute('appnum', appnum); + tr_app.setAttribute('id', 'row'+rownum+'.'+appnum); + + var td_invnum = document.createElement('TD'); + td_invnum.setAttribute('colspan', 4); + td_invnum.style.textAlign = 'right'; + td_invnum.appendChild( + document.createTextNode(<% mt('Apply to Invoice ') |js_string %>) + ); + var select_invnum = document.createElement('SELECT'); + select_invnum.setAttribute('rownum', rownum); + select_invnum.setAttribute('appnum', appnum); + select_invnum.setAttribute('id', 'invnum'+rownum+'.'+appnum); + select_invnum.setAttribute('name', 'invnum'+rownum+'.'+appnum); + select_invnum.className = 'select_invnum'; + select_invnum.onchange = choose_app_invnum; + select_invnum.onfocus = focus_app_invnum; + + td_invnum.appendChild(select_invnum); + tr_app.appendChild(td_invnum); + + var td_owed = document.createElement('TD'); + td_owed.style.textAlign= 'right'; + var span_owed = document.createElement('SPAN'); + span_owed.setAttribute('rownum', rownum); + span_owed.setAttribute('appnum', appnum); + span_owed.setAttribute('id', 'owed'+rownum+'.'+appnum); + td_owed.appendChild(span_owed); + tr_app.appendChild(td_owed); + + var td_amount = document.createElement('TD'); + td_amount.style.textAlign = 'right'; + var input_amount = document.createElement('INPUT'); + input_amount.size = 6; + input_amount.setAttribute('rownum', rownum); + input_amount.setAttribute('appnum', appnum); + input_amount.setAttribute('name', 'amount'+rownum+'.'+appnum); + input_amount.setAttribute('id', 'amount'+rownum+'.'+appnum); + input_amount.style.textAlign = 'right'; + input_amount.onchange = change_app_amount; + td_amount.appendChild(input_amount); + tr_app.appendChild(td_amount); + + var td_delete = document.createElement('TD'); + td_delete.setAttribute('colspan', <% scalar(@fields)-2 %>); + var button_delete = document.createElement('INPUT'); + button_delete.setAttribute('rownum', rownum); + button_delete.setAttribute('appnum', appnum); + button_delete.setAttribute('id', 'delete'+rownum+'.'+appnum); + button_delete.setAttribute('type', 'button'); + button_delete.setAttribute('value', 'X'); + button_delete.onclick = delete_application_row; + button_delete.style.color = '#ff0000'; + button_delete.style.fontWeight = 'bold'; + button_delete.style.paddingLeft = '2px'; + button_delete.style.paddingRight = '2px'; + td_delete.appendChild(button_delete); + tr_app.appendChild(td_delete); + + var td_error = document.createElement('TD'); + var span_error = document.createElement('SPAN'); + span_error.setAttribute('rownum', rownum); + span_error.setAttribute('appnum', appnum); + span_error.setAttribute('id', 'error'+rownum+'.'+appnum); + span_error.style.color = '#ff0000'; + td_error.appendChild(span_error); + tr_app.appendChild(td_error); + + if ( appnum > 0 ) { + //remove delete button on the previous row + document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = 'none'; + } + rownum++; + var next_row = document.getElementById('row'+rownum); // always exists + payment_row.parentNode.insertBefore(tr_app, next_row); + +} + +%# for error handling--ugly, but the alternative is translating the whole +%# process of creating rows into Mason +var row_obj = <% encode_json(\%rows) %>; +function preload() { + var rownum; + var appnum; + for (rownum in row_obj) { + if ( row_obj[rownum].length ) { + var enable = document.getElementById('enable_app'+rownum); + enable.checked = true; + var preload_row = function(r) {//continuation from toggle_application_row + + preloading++; + + try { + for (appnum=0; appnum < row_obj[r].length; appnum++) { + this_app = row_obj[r][appnum]; + var x = r + '.' + appnum; + //set invnum + var select_invnum = document.getElementById('invnum'+x); + focus_app_invnum.call(select_invnum); + for (i=0; i + +<% include('/elements/xmlhttp.html', + 'url' => $p. 'misc/xmlhttp-cust_main-discount_terms.cgi', + 'subs' => [qw( discount_terms )], + ) %> - + + + +<& /elements/xmlhttp.html, + url => $p.'misc/xmlhttp-cust_bill-search.html', + subs => ['custnum_search_open'] +&> + +<& /elements/customer-table.html, + name_singular => 'payment', + header => \@header, + fields => \@fields, + type => \@types, + align => \@align, + size => \@sizes, + color => \@colors, + param => \%param, + footer => \@footer, + footer_align => \@footer_align, + onchange => \@onchange, + custnum_update_callback => 'custnum_update_callback', + invnum_update_callback => 'invnum_update_callback', + add_row_callback => 'add_row_callback', + delete_row_callback => 'delete_row_callback', +&>
- + +% #XXX I think this can go away completely, but need to test with $use_discount +% ###not perl + <% include('/elements/footer.html') %> <%init> @@ -48,4 +466,95 @@ window.onbeforeunload = warnUnload; die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch'); +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my @header = ( 'Amount', 'Check #', 'Date override' ); +my @fields = ( 'paid', 'payinfo', '_date' ); +my @types = ( '', '', 'date', ); +my @align = ( 'r', 'r', 'r' ); +my @sizes = ( 8, 10, 8 ); +my @colors = ( '', '', '' ); +my %param = (); +my @footer = ( '_TOTAL', '', '' ); +my @footer_align = ( 'r', 'r', '' ); +my @onchange = ( '', '', '' ); +my $use_discounts = ''; + +# Not entirely sure this works anymore... +if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) { + #push @header, 'Discount'; + push @header, ''; + push @fields, 'discount_term'; + push @types, 'immutable'; + push @align, 'r'; + push @sizes, '0'; + push @colors, ''; + push @footer, ''; + push @footer_align, ''; + push @onchange, ''; + $use_discounts = 'Y'; +} + +push @header, 'Allocate'; +push @fields, 'enable_app'; +push @types, 'checkbox'; +push @align, 'c'; +push @sizes, '0'; +push @colors, ''; +push @footer, ''; +push @footer_align, ''; +push @onchange, 'toggle_application_row'; + +push @header, 'No Auto Allocate'; +push @fields, 'no_auto_apply'; +push @types, 'checkbox'; +push @align, 'c'; +push @sizes, '0'; +push @colors, ''; +push @footer, ''; +push @footer_align, ''; +push @onchange, ''; + +#push @header, 'Error'; +push @header, ''; +push @fields, 'error'; +push @types, 'immutable'; +push @align, 'l'; +push @sizes, '0'; +push @colors, '#ff0000'; +push @footer, ''; +push @footer_align, ''; +push @onchange, ''; + +$m->comp('/elements/handle_uri_query'); + +# set up for preloading +my %rows; +my %row_errors; +if ( $cgi->param('error') ) { + my $param = $cgi->Vars; + my $enum = 0; #errors numbered separately + my @invrows = grep /^invnum\d+\.\d+$/, keys %$param; #pare down possibilities + foreach my $row ( sort { $a <=> $b } map /^custnum(\d+)$/, keys %$param ) { +# for( my $row = 0; exists($param->{"custnum$row"}); $row++ ) { + $rows{$row} = []; + $row_errors{$row} = $param->{"error$enum"}; + $enum++; + foreach my $app ( map /^invnum$row\.(\d+)$/, @invrows ) { + next if !$param->{"invnum$row.$app"}; + my %this_app = map { $_ => ($param->{$_.$row.'.'.$app} || '') } + qw( invnum amount ); + $this_app{'error'} = $param->{"error$enum"} || ''; + $param->{"error$enum"} = ''; # don't pass this error through + $rows{$row}[$app] = \%this_app; + $enum++; + } + } + foreach my $row (keys %rows) { + $param->{"error$row"} = $row_errors{$row}; + } +} +#warn Dumper {rows => \%rows, row_errors => \%row_errors }; +