1 <& /elements/header.html, {
2 title => 'Quick payment entry',
3 etc => 'onload="preload()"'
6 <& /elements/error.html &>
8 <STYLE TYPE="text/css">
14 font-family: monospace;
17 <SCRIPT TYPE="text/javascript">
18 function warnUnload() {
19 if(document.getElementById("OneTrueTable").rows.length > 3 &&
20 !document.OneTrueForm.btnsubmit.disabled) {
21 return "The current batch will be lost.";
27 window.onbeforeunload = warnUnload;
29 function add_row_callback(rownum, values) {
31 custnum_update_callback(rownum);
33 document.getElementById('enable_app'+rownum).disabled = true;
37 function delete_row_callback(rownum) {
39 var delbutton = document.getElementById('delete'+rownum+'.'+i);
42 delrows[i] = delbutton;
44 delbutton = document.getElementById('delete'+rownum+'.'+i);
46 delrows = delrows.reverse();
47 for (i = 0; i < delrows.length; i++) {
52 function custnum_update_callback(rownum) {
53 var custnum = document.getElementById('custnum'+rownum).value;
54 // if there is a custnum and more than one open invoice, enable
55 // (and check) the box
56 var show_applications = !(custnum > 0 && num_open_invoices[rownum] > 1);
57 var enable_app_checkbox = document.getElementById('enable_app'+rownum);
58 enable_app_checkbox.disabled = show_applications;
60 % if ( $use_discounts ) {
61 select_discount_term(rownum);
65 function invnum_update_callback(rownum) {
66 custnum_update_callback(rownum);
69 function select_discount_term(row) {
70 var custnum_obj = document.getElementById('custnum'+row);
71 var select_obj = document.getElementById('discount_term'+row);
74 if (select_obj.type == 'hidden') {
75 value = select_obj.value;
78 var term_select = document.createElement('SELECT');
79 term_select.setAttribute('name', 'discount_term'+row);
80 term_select.setAttribute('id', 'discount_term'+row);
81 term_select.setAttribute('rownum', row);
82 term_select.style.display = '';
83 select_obj.parentNode.replaceChild(term_select, select_obj);
84 opt(term_select, '', '1 month');
86 function select_discount_term_update(discount_terms) {
88 var termArray = eval('(' + discount_terms + ')');
89 for ( var t = 0; t < termArray.length; t++ ) {
90 opt(term_select, termArray[t][0], termArray[t][1]);
91 if (termArray[t][0] == value) {
92 term_select.selectedIndex = t+1;
98 discount_terms(custnum_obj.value, select_discount_term_update);
102 var invoices_for_row = new Object;
104 function update_invoices(rownum, invoices) {
105 invoices_for_row[rownum] = new Object;
106 // only called before create_application_row
107 for ( var i=0; i<invoices.length; i++ ) {
108 invoices_for_row[rownum][ invoices[i].invnum ] = invoices[i];
112 function toggle_application_row(ev, next) {
113 if (!next) next = function(){}; //optional continuation
114 var rownum = this.getAttribute('rownum');
115 if ( this.checked ) {
116 var custnum = document.getElementById('custnum'+rownum).value;
117 if (!custnum) return;
118 lock_payment_row(rownum, true);
119 custnum_search_open( custnum,
121 update_invoices(rownum, JSON.parse(returned));
122 create_application_row(rownum, 0);
123 next.call(this, rownum);
127 var row = document.getElementById('row'+rownum);
128 var table_rows = row.parentNode.rows;
129 for (i = row.sectionRowIndex; i < table_rows.count; i++) {
130 if ( table_rows[i].id.indexof('row'+rownum+'.') > -1 ) {
131 table_rows.removeChild(table_rows[i]);
136 lock_payment_row(rownum, false);
140 function lock_payment_row(rownum, flag) {
141 % foreach (qw(invnum custnum customer)) {
142 obj = document.getElementById('<% $_ %>'+rownum);
145 document.getElementById('enable_app'+rownum).disabled = flag;
148 function delete_application_row() {
149 var rownum = this.getAttribute('rownum');
150 var appnum = this.getAttribute('appnum');
151 var tr_app = document.getElementById('row'+rownum+'.'+appnum);
152 var select_invnum = document.getElementById('invnum'+rownum+'.'+appnum);
153 if ( select_invnum.value ) {
154 invoices_for_row[rownum][ select_invnum.value ] = select_invnum.curr_invoice;
157 tr_app.parentNode.removeChild(tr_app);
159 document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = '';
162 lock_payment_row(rownum, false);
163 document.getElementById('enable_app'+rownum).checked = false;
167 function amount_unapplied(rownum) {
170 var payment_amount = parseFloat(document.getElementById('paid'+rownum).value)
173 var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
174 if ( input_amount ) {
175 total += parseFloat(input_amount.value || 0);
179 return payment_amount - total;
184 var change_app_amount;
186 function choose_app_invnum() {
187 var rownum = this.getAttribute('rownum');
188 var appnum = this.getAttribute('appnum');
189 var last_invoice = this.curr_invoice;
190 if ( last_invoice ) {
191 invoices_for_row[rownum][ last_invoice['invnum'] ] = last_invoice;
195 var this_invoice = invoices_for_row[rownum][this.value];
196 this.curr_invoice = invoices_for_row[rownum][this.value];
197 var span_owed = document.getElementById('owed'+rownum+'.'+appnum);
198 span_owed.innerHTML = this_invoice['owed'] + ' ';
199 delete invoices_for_row[rownum][this.value];
201 var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
202 if ( input_amount.value == '' ) {
205 0, Math.min( amount_unapplied(rownum), this_invoice['owed'])
208 change_app_amount.call(input_amount);
213 function focus_app_invnum() {
214 % # invoice numbers just display as invoice numbers
215 var rownum = this.getAttribute('rownum');
216 var add_opt = function(obj, value, label) {
217 var o = document.createElement('OPTION');
222 this.options.length = 0;
223 var this_invoice = this.curr_invoice;
224 if ( this_invoice ) {
225 add_opt(this, this_invoice.invnum, this_invoice.label);
227 add_opt(this, '', '');
229 for ( var x in invoices_for_row[rownum] ) {
231 invoices_for_row[rownum][x].invnum,
232 invoices_for_row[rownum][x].label);
236 function change_app_amount() {
237 var rownum = this.getAttribute('rownum');
238 var appnum = this.getAttribute('appnum');
239 %# maybe some kind of warning if amount_unapplied < 0?
240 %# only spawn a new application row if there are open invoices left,
241 %# and this is the highest-numbered application row for the customer,
242 %# and the sum of the applied amounts is < the amount of the payment,
243 if ( Object.keys(invoices_for_row[rownum]).length > 0
244 && !document.getElementById( 'row'+rownum+'.'+(parseInt(appnum) + 1) )
245 && amount_unapplied(rownum) > 0 ) {
247 create_application_row(rownum, parseInt(appnum) + 1);
251 function create_application_row(rownum, appnum) {
252 var payment_row = document.getElementById('row'+rownum);
253 var tr_app = document.createElement('TR');
254 tr_app.setAttribute('rownum', rownum);
255 tr_app.setAttribute('appnum', appnum);
256 tr_app.setAttribute('id', 'row'+rownum+'.'+appnum);
258 var td_invnum = document.createElement('TD');
259 td_invnum.setAttribute('colspan', 4);
260 td_invnum.style.textAlign = 'right';
261 td_invnum.appendChild(
262 document.createTextNode('<% mt('Apply to Invoice ') %>')
264 var select_invnum = document.createElement('SELECT');
265 select_invnum.setAttribute('rownum', rownum);
266 select_invnum.setAttribute('appnum', appnum);
267 select_invnum.setAttribute('id', 'invnum'+rownum+'.'+appnum);
268 select_invnum.setAttribute('name', 'invnum'+rownum+'.'+appnum);
269 select_invnum.className = 'select_invnum';
270 select_invnum.onchange = choose_app_invnum;
271 select_invnum.onfocus = focus_app_invnum;
273 td_invnum.appendChild(select_invnum);
274 tr_app.appendChild(td_invnum);
276 var td_owed = document.createElement('TD');
277 td_owed.style.textAlign= 'right';
278 var span_owed = document.createElement('SPAN');
279 span_owed.setAttribute('rownum', rownum);
280 span_owed.setAttribute('appnum', appnum);
281 span_owed.setAttribute('id', 'owed'+rownum+'.'+appnum);
282 td_owed.appendChild(span_owed);
283 tr_app.appendChild(td_owed);
285 var td_amount = document.createElement('TD');
286 td_amount.style.textAlign = 'right';
287 var input_amount = document.createElement('INPUT');
288 input_amount.size = 6;
289 input_amount.setAttribute('rownum', rownum);
290 input_amount.setAttribute('appnum', appnum);
291 input_amount.setAttribute('name', 'amount'+rownum+'.'+appnum);
292 input_amount.setAttribute('id', 'amount'+rownum+'.'+appnum);
293 input_amount.style.textAlign = 'right';
294 input_amount.onchange = change_app_amount;
295 td_amount.appendChild(input_amount);
296 tr_app.appendChild(td_amount);
298 var td_delete = document.createElement('TD');
299 td_delete.setAttribute('colspan', <% scalar(@fields)-2 %>);
300 var button_delete = document.createElement('INPUT');
301 button_delete.setAttribute('rownum', rownum);
302 button_delete.setAttribute('appnum', appnum);
303 button_delete.setAttribute('id', 'delete'+rownum+'.'+appnum);
304 button_delete.setAttribute('type', 'button');
305 button_delete.setAttribute('value', 'X');
306 button_delete.onclick = delete_application_row;
307 button_delete.style.color = '#ff0000';
308 button_delete.style.fontWeight = 'bold';
309 button_delete.style.paddingLeft = '2px';
310 button_delete.style.paddingRight = '2px';
311 td_delete.appendChild(button_delete);
312 tr_app.appendChild(td_delete);
314 var td_error = document.createElement('TD');
315 var span_error = document.createElement('SPAN');
316 span_error.setAttribute('rownum', rownum);
317 span_error.setAttribute('appnum', appnum);
318 span_error.setAttribute('id', 'error'+rownum+'.'+appnum);
319 span_error.style.color = '#ff0000';
320 td_error.appendChild(span_error);
321 tr_app.appendChild(td_error);
324 //remove delete button on the previous row
325 document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = 'none';
328 var next_row = document.getElementById('row'+rownum); // always exists
329 payment_row.parentNode.insertBefore(tr_app, next_row);
333 %# for error handling--ugly, but the alternative is translating the whole
334 %# process of creating rows into Mason
335 var row_obj = <% encode_json(\%rows) %>;
339 for (rownum in row_obj) {
340 if ( row_obj[rownum].length ) {
341 var enable = document.getElementById('enable_app'+rownum);
342 enable.checked = true;
343 var preload_row = function(r) {//continuation from toggle_application_row
344 for (appnum=0; appnum < row_obj[r].length; appnum++) {
345 this_app = row_obj[r][appnum];
346 var x = r + '.' + appnum;
348 var select_invnum = document.getElementById('invnum'+x);
349 focus_app_invnum.call(select_invnum);
350 for (i=0; i<select_invnum.options.length; i++) {
351 if (select_invnum.options[i].value == this_app.invnum) {
352 select_invnum.selectedIndex = i;
355 choose_app_invnum.call(select_invnum);
357 var input_amount = document.getElementById('amount'+x);
358 input_amount.value = this_app.amount;
361 var span_error = document.getElementById('error'+x);
362 span_error.innerHTML = this_app.error;
363 change_app_amount.call(input_amount); //creates next row
365 }; //preload_row function
366 toggle_application_row.call(enable, null, preload_row);
367 } // if (row_obj[rownum].length
373 <% include('/elements/xmlhttp.html',
374 'url' => $p. 'misc/xmlhttp-cust_main-discount_terms.cgi',
375 'subs' => [qw( discount_terms )],
379 <FORM ACTION="process/batch-cust_pay.cgi" NAME="OneTrueForm" METHOD="POST" onsubmit="document.OneTrueForm.btnsubmit.disabled=true;window.onbeforeunload = null;">
381 <!-- <B>Batch</B> <INPUT TYPE="text" NAME="paybatch"><BR><BR> -->
382 <& /elements/xmlhttp.html,
383 url => $p.'misc/xmlhttp-cust_bill-search.html',
384 subs => ['custnum_search_open']
387 <& /elements/customer-table.html,
388 name_singular => 'payment',
397 footer_align => \@footer_align,
398 onchange => \@onchange,
399 custnum_update_callback => 'custnum_update_callback',
400 invnum_update_callback => 'invnum_update_callback',
401 add_row_callback => 'add_row_callback',
402 delete_row_callback => 'delete_row_callback',
406 <INPUT TYPE="button" VALUE="Post payment batch" name="btnsubmit" onclick="window.onbeforeunload = null; document.OneTrueForm.submit(); this.disabled = true;">
410 % #XXX I think this can go away completely, but need to test with $use_discount
411 % ###not perl <SCRIPT TYPE="text/javascript">
412 % #foreach my $row ( keys %rows ) {
413 % ###not perl select_discount_term(<% $row %>, '');
415 % ###not perl </SCRIPT>
417 <% include('/elements/footer.html') %>
422 unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch');
424 my $conf = new FS::Conf;
425 my $money_char = $conf->config('money_char') || '$';
427 my @header = ( 'Amount', 'Check #' );
428 my @fields = ( 'paid', 'payinfo' );
429 my @types = ( '', '' );
430 my @align = ( 'r', 'r' );
431 my @sizes = ( 8, 10 );
432 my @colors = ( '', '' );
434 my @footer = ( '_TOTAL', '' );
435 my @footer_align = ( 'r', 'r' );
436 my @onchange = ( '', '' );;
437 my $use_discounts = '';
439 # Not entirely sure this works anymore...
440 if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) {
441 #push @header, 'Discount';
443 push @fields, 'discount_term';
444 push @types, 'immutable';
449 push @footer_align, '';
451 $use_discounts = 'Y';
454 push @header, 'Allocate';
455 push @fields, 'enable_app';
456 push @types, 'checkbox';
461 push @footer_align, '';
462 push @onchange, 'toggle_application_row';
464 #push @header, 'Error';
466 push @fields, 'error';
467 push @types, 'immutable';
470 push @colors, '#ff0000';
472 push @footer_align, '';
475 $m->comp('/elements/handle_uri_query');
477 # set up for preloading
480 if ( $cgi->param('error') ) {
481 my $param = $cgi->Vars;
482 my $enum = 0; #errors numbered separately
483 my @invrows = grep /^invnum\d+\.\d+$/, keys %$param; #pare down possibilities
484 foreach my $row ( sort { $a <=> $b } map /^custnum(\d+)$/, keys %$param ) {
485 # for( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
487 $row_errors{$row} = $param->{"error$enum"};
489 foreach my $app ( map /^invnum$row\.(\d+)$/, @invrows ) {
490 next if !$param->{"invnum$row.$app"};
491 my %this_app = map { $_ => ($param->{$_.$row.'.'.$app} || '') }
493 $this_app{'error'} = $param->{"error$enum"} || '';
494 $param->{"error$enum"} = ''; # don't pass this error through
495 $rows{$row}[$app] = \%this_app;
499 foreach my $row (keys %rows) {
500 $param->{"error$row"} = $row_errors{$row};
503 #warn Dumper {rows => \%rows, row_errors => \%row_errors };