RT# 73964 - Added biling event action to send an email to phone nunber, and updated...
[freeside.git] / httemplate / misc / batch-cust_pay.html
1 <& /elements/header.html, {
2   title => 'Quick payment entry',
3   etc   => 'onload="preload()"'
4 } &>
5
6 <& /elements/error.html &>
7
8 <STYLE TYPE="text/css">
9 .select_invnum {
10   text-align: right;
11   width: 220px;
12 }
13 .select_invnum * {
14   font-family: monospace;
15 }
16 </STYLE>
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.";
22   }
23   else {
24     return null;
25   }
26 }
27 window.onbeforeunload = warnUnload;
28
29 function add_row_callback(rownum, values) {
30   if (values) {
31     custnum_update_callback(rownum);
32   } else {
33     document.getElementById('enable_app'+rownum).disabled = true;
34   }
35 }
36
37 function delete_row_callback(rownum) {
38   var i = 0;
39   var delbutton = document.getElementById('delete'+rownum+'.'+i);
40   var delrows = [];
41   while (delbutton) {
42     delrows[i] = delbutton;
43     i++;
44     delbutton = document.getElementById('delete'+rownum+'.'+i);
45   }
46   delrows = delrows.reverse();
47   for (i = 0; i < delrows.length; i++) {
48     delrows[i].onclick();
49   }
50 }
51
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;
59
60 % if ( $use_discounts ) {
61   select_discount_term(rownum);
62 % }
63 }
64
65 function invnum_update_callback(rownum) {
66   custnum_update_callback(rownum);
67 }
68
69 function select_discount_term(row) {
70   var custnum_obj = document.getElementById('custnum'+row);
71   var select_obj = document.getElementById('discount_term'+row);
72
73   var value = '';
74   if (select_obj.type == 'hidden') {
75     value = select_obj.value;
76   }
77
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');
85   
86   function select_discount_term_update(discount_terms) {
87
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;
93       }
94     }
95
96   }
97
98   discount_terms(custnum_obj.value, select_discount_term_update);
99
100 }
101
102 var invoices_for_row = new Object;
103
104 var preloading = 0; // the number of preloading threads currently running
105
106 // callback from toggle_application_row: we've received a list of
107 // the customer's open invoices. store them.
108 function update_invoices(rownum, invoices) {
109   invoices_for_row[rownum] = new Object;
110   // only called before create_application_row
111   for ( var i=0; i<invoices.length; i++ ) {
112     invoices_for_row[rownum][ invoices[i].invnum ] = invoices[i];
113   }
114 }
115
116 function toggle_application_row(ev, next) {
117   if (!next) next = function(){}; //optional continuation
118   var rownum = this.getAttribute('rownum');
119   if ( this.checked ) {
120     // the user has opted to apply the payment to specific invoices.
121     // - lock the customer
122     // - fetch the list of open invoices
123     // - create a row to select an invoice
124     // - then optionally call "next", with this as the invocant
125     //   and the rownum as argument; we use this to preload rows.
126     var custnum = document.getElementById('custnum'+rownum).value;
127     if (!custnum) return;
128     lock_payment_row(rownum, true);
129     custnum_search_open( custnum, 
130       function(returned) {
131         update_invoices(rownum, JSON.parse(returned));
132         create_application_row(rownum, 0);
133         next.call(this, rownum);
134       }
135     );
136   } else {
137     // the user has opted not to do that.
138     // - remove all application rows
139     // - unlock the customer
140     var row = document.getElementById('row'+rownum);
141     var table_rows = row.parentNode.rows;
142     for (i = row.sectionRowIndex; i < table_rows.count; i++) {
143       if ( table_rows[i].id.indexof('row'+rownum+'.') > -1 ) {
144         table_rows.removeChild(table_rows[i]);
145       } else {
146         break;
147       }
148     }
149     lock_payment_row(rownum, false);
150   }
151 }
152
153 function lock_payment_row(rownum, flag) {
154 % foreach (qw(invnum custnum customer)) {
155   obj = document.getElementById('<% $_ %>'+rownum);
156   obj.readOnly = flag;
157 % }
158   document.getElementById('enable_app'+rownum).disabled = flag;
159 }
160
161 function delete_application_row() {
162   var rownum = this.getAttribute('rownum');
163   var appnum = this.getAttribute('appnum');
164   var tr_app = document.getElementById('row'+rownum+'.'+appnum);
165   var select_invnum = document.getElementById('invnum'+rownum+'.'+appnum);
166   if ( select_invnum.value ) {
167     invoices_for_row[rownum][ select_invnum.value ] = select_invnum.curr_invoice;
168   }
169     
170   tr_app.parentNode.removeChild(tr_app);
171   if ( appnum > 0 ) {
172     document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = '';
173   }
174   else {
175     lock_payment_row(rownum, false);
176     document.getElementById('enable_app'+rownum).checked = false;
177   }
178 }
179
180 function amount_unapplied(rownum) {
181   var appnum = 0;
182   var total = 0;
183   var payment_amount = parseFloat(document.getElementById('paid'+rownum).value)
184                        || 0;
185   while (true) {
186     var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
187     if ( input_amount ) {
188       total += parseFloat(input_amount.value || 0);
189       appnum++;
190     }
191     else {
192       return payment_amount - total;
193     }
194   }
195 }
196
197 var change_app_amount;
198
199 // the user has chosen an invoice. the previously chosen invoice is still
200 // in curr_invoice
201 // - if there is a value there, put it back on the invoices_for_row list for
202 // this customer.
203 // - then _remove_ the newly chosen invoice from that list.
204 // - find the "owed" element for this application row and set its value to the
205 // amount owed on that invoice.
206 // - find the "amount" element for this application row and set its value to
207 // either "owed" or the remaining payment amount, whichever is less.
208 // - call change_app_amount() on that element.
209 function choose_app_invnum() {
210   var rownum = this.getAttribute('rownum');
211   var appnum = this.getAttribute('appnum');
212   var last_invoice = this.curr_invoice;
213   if ( last_invoice ) {
214     invoices_for_row[rownum][ last_invoice['invnum'] ] = last_invoice;
215   }
216
217   if ( this.value ) {
218     var this_invoice = invoices_for_row[rownum][this.value];
219     this.curr_invoice = invoices_for_row[rownum][this.value];
220     var span_owed = document.getElementById('owed'+rownum+'.'+appnum);
221     span_owed.innerHTML = this_invoice['owed'] + '&nbsp;';
222     delete invoices_for_row[rownum][this.value];
223
224     var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
225     if ( input_amount.value == '' ) {
226       input_amount.value = 
227         Math.max(
228           0, Math.min( amount_unapplied(rownum), this_invoice['owed'])
229         ).toFixed(2);
230       // trigger onchange
231       change_app_amount.call(input_amount);
232     }
233   }
234 }
235
236 // the invoice selector has gained focus. clear its list of options, and
237 // replace them with the list of open invoices (from invoices_for_row).
238 // if there's already a selected invoice, prepend that to the list.
239 function focus_app_invnum() {
240   var rownum = this.getAttribute('rownum');
241   var add_opt = function(obj, value, label) {
242     var o = document.createElement('OPTION');
243     o.text = label;
244     o.value = value;
245     obj.add(o);
246   }
247   this.options.length = 0;
248   var this_invoice = this.curr_invoice;
249   if ( this_invoice ) {
250     add_opt(this, this_invoice.invnum, this_invoice.label);
251   } else {
252     add_opt(this, '', '');
253   }
254   for ( var x in invoices_for_row[rownum] ) {
255     add_opt(this,
256             invoices_for_row[rownum][x].invnum,
257             invoices_for_row[rownum][x].label);
258   }
259 }
260
261 // an application amount has been changed. if there's any unapplied payment
262 // amount, and any remaining invoices_for_row, add a blank application row.
263 // (but don't do this while preloading; it will unconditionally add enough
264 // rows to show all the attempted applications)
265 function change_app_amount() {
266   var rownum = this.getAttribute('rownum');
267   var appnum = this.getAttribute('appnum');
268   if ( preloading == 0
269        && Object.keys(invoices_for_row[rownum]).length > 0
270        && !document.getElementById( 'row'+rownum+'.'+(parseInt(appnum) + 1) )
271        && amount_unapplied(rownum) > 0 ) {
272
273     create_application_row(rownum, parseInt(appnum) + 1);
274   }
275 }
276
277 // we're creating a payment application row.
278 // create the following elements: <TR>, <TD>s, "Apply to invoice" caption,
279 // invnum selector, "owed" display, amount input box, delete button.
280 function create_application_row(rownum, appnum) {
281   var payment_row = document.getElementById('row'+rownum);
282   var tr_app = document.createElement('TR');
283   tr_app.setAttribute('rownum', rownum);
284   tr_app.setAttribute('appnum', appnum);
285   tr_app.setAttribute('id', 'row'+rownum+'.'+appnum);
286   
287   var td_invnum = document.createElement('TD');
288   td_invnum.setAttribute('colspan', 4);
289   td_invnum.style.textAlign = 'right';
290   td_invnum.appendChild(
291     document.createTextNode(<% mt('Apply to Invoice ') |js_string %>)
292   );
293   var select_invnum = document.createElement('SELECT');
294   select_invnum.setAttribute('rownum', rownum);
295   select_invnum.setAttribute('appnum', appnum);
296   select_invnum.setAttribute('id', 'invnum'+rownum+'.'+appnum);
297   select_invnum.setAttribute('name', 'invnum'+rownum+'.'+appnum);
298   select_invnum.className = 'select_invnum';
299   select_invnum.onchange = choose_app_invnum;
300   select_invnum.onfocus  = focus_app_invnum;
301   
302   td_invnum.appendChild(select_invnum);
303   tr_app.appendChild(td_invnum);
304
305   var td_owed = document.createElement('TD');
306   td_owed.style.textAlign= 'right';
307   var span_owed = document.createElement('SPAN');
308   span_owed.setAttribute('rownum', rownum);
309   span_owed.setAttribute('appnum', appnum);
310   span_owed.setAttribute('id', 'owed'+rownum+'.'+appnum);
311   td_owed.appendChild(span_owed);
312   tr_app.appendChild(td_owed);
313
314   var td_amount = document.createElement('TD');
315   td_amount.style.textAlign = 'right';
316   var input_amount = document.createElement('INPUT');
317   input_amount.size = 6;
318   input_amount.setAttribute('rownum', rownum);
319   input_amount.setAttribute('appnum', appnum);
320   input_amount.setAttribute('name', 'amount'+rownum+'.'+appnum);
321   input_amount.setAttribute('id', 'amount'+rownum+'.'+appnum);
322   input_amount.style.textAlign = 'right';
323   input_amount.onchange = change_app_amount;
324   td_amount.appendChild(input_amount);
325   tr_app.appendChild(td_amount);
326
327   var td_delete = document.createElement('TD');
328   td_delete.setAttribute('colspan', <% scalar(@fields)-2 %>);
329   var button_delete = document.createElement('INPUT');
330   button_delete.setAttribute('rownum', rownum);
331   button_delete.setAttribute('appnum', appnum);
332   button_delete.setAttribute('id', 'delete'+rownum+'.'+appnum);
333   button_delete.setAttribute('type', 'button');
334   button_delete.setAttribute('value', 'X');
335   button_delete.onclick = delete_application_row;
336   button_delete.style.color = '#ff0000';
337   button_delete.style.fontWeight = 'bold';
338   button_delete.style.paddingLeft = '2px';
339   button_delete.style.paddingRight = '2px';
340   td_delete.appendChild(button_delete);
341   tr_app.appendChild(td_delete);
342
343   var td_error = document.createElement('TD');
344   var span_error = document.createElement('SPAN');
345   span_error.setAttribute('rownum', rownum);
346   span_error.setAttribute('appnum', appnum);
347   span_error.setAttribute('id', 'error'+rownum+'.'+appnum);
348   span_error.style.color = '#ff0000';
349   td_error.appendChild(span_error);
350   tr_app.appendChild(td_error);
351
352   if ( appnum > 0 ) {
353     //remove delete button on the previous row
354     document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = 'none';
355   }
356   rownum++;
357   var next_row = document.getElementById('row'+rownum); // always exists
358   payment_row.parentNode.insertBefore(tr_app, next_row);
359
360 }
361
362 %# for error handling--ugly, but the alternative is translating the whole 
363 %# process of creating rows into Mason
364 var row_obj = <% encode_json(\%rows) %>;
365 function preload() {
366   var rownum;
367   var appnum;
368   for (rownum in row_obj) {
369     if ( row_obj[rownum].length ) {
370       var enable = document.getElementById('enable_app'+rownum);
371       enable.checked = true;
372       var preload_row = function(r) {//continuation from toggle_application_row
373
374         preloading++;
375
376         try {
377           for (appnum=0; appnum < row_obj[r].length; appnum++) {
378             this_app = row_obj[r][appnum];
379             var x = r + '.' + appnum;
380             //set invnum
381             var select_invnum = document.getElementById('invnum'+x);
382             focus_app_invnum.call(select_invnum);
383             for (i=0; i<select_invnum.options.length; i++) {
384               if (select_invnum.options[i].value == this_app.invnum) {
385                 select_invnum.selectedIndex = i;
386               }
387             }
388             choose_app_invnum.call(select_invnum);
389             //set amount
390             var input_amount = document.getElementById('amount'+x);
391             input_amount.value = this_app.amount;
392
393             //set error
394             var span_error = document.getElementById('error'+x);
395             span_error.innerHTML = this_app.error;
396
397             // create another row (unconditionally)
398             create_application_row(r, appnum + 1);
399
400           } //for appnum
401
402         } finally {
403           preloading--;
404         }
405
406       }; //preload_row function
407
408       // enable application rows on the selected customer. this creates
409       // the first row, then kicks off preloading.      
410       toggle_application_row.call(enable, null, preload_row);
411
412     } // if (row_obj[rownum].length
413   } //for rownum
414 }
415
416 </SCRIPT>
417
418 <% include('/elements/xmlhttp.html',
419               'url'  => $p. 'misc/xmlhttp-cust_main-discount_terms.cgi',
420               'subs' => [qw( discount_terms )],
421            )
422 %>
423
424 <FORM ACTION="process/batch-cust_pay.cgi" NAME="OneTrueForm" METHOD="POST" onsubmit="document.OneTrueForm.btnsubmit.disabled=true;window.onbeforeunload = null;">
425
426 <!-- <B>Batch</B> <INPUT TYPE="text" NAME="paybatch"><BR><BR> -->
427 <& /elements/xmlhttp.html,
428     url => $p.'misc/xmlhttp-cust_bill-search.html',
429     subs => ['custnum_search_open']
430 &>
431
432 <& /elements/customer-table.html,
433     name_singular => 'payment',
434     header  => \@header,
435     fields  => \@fields,
436     type    => \@types,
437     align   => \@align,
438     size    => \@sizes,
439     color   => \@colors,
440     param   => \%param,
441     footer  => \@footer,
442     footer_align => \@footer_align,
443     onchange => \@onchange,
444     custnum_update_callback => 'custnum_update_callback',
445     invnum_update_callback => 'invnum_update_callback',
446     add_row_callback => 'add_row_callback',
447     delete_row_callback => 'delete_row_callback',
448 &>
449
450 <BR>
451 <INPUT TYPE="button" VALUE="Post payment batch" name="btnsubmit" id="btnsubmit" onclick="window.onbeforeunload = null; document.OneTrueForm.submit(); this.disabled = true;">
452
453 </FORM>
454
455 % #XXX I think this can go away completely, but need to test with $use_discount
456 % ###not perl <SCRIPT TYPE="text/javascript">
457 % #foreach my $row ( keys %rows ) {
458 % ###not perl   select_discount_term(<% $row %>, '');
459 % #}
460 % ###not perl </SCRIPT>
461
462 <% include('/elements/footer.html') %>
463
464 <%init>
465
466 die "access denied"
467   unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch');
468
469 my $conf = new FS::Conf;
470 my $money_char = $conf->config('money_char') || '$';
471
472 my @header  = ( 'Amount', 'Check #', 'Date override' );
473 my @fields  = ( 'paid', 'payinfo', '_date' );
474 my @types   = ( '', '', 'date', );
475 my @align   = ( 'r', 'r', 'r' );
476 my @sizes   = ( 8, 10, 8 );
477 my @colors  = ( '', '', '' );
478 my %param   = ();
479 my @footer  = ( '_TOTAL', '', '' );
480 my @footer_align = ( 'r', 'r', '' );
481 my @onchange = ( '', '', '' );
482 my $use_discounts = '';
483
484 # Not entirely sure this works anymore...
485 if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) {
486   #push @header, 'Discount';
487   push @header, '';
488   push @fields, 'discount_term';
489   push @types, 'immutable';
490   push @align, 'r';
491   push @sizes, '0';
492   push @colors, '';
493   push @footer, '';
494   push @footer_align, '';
495   push @onchange, '';
496   $use_discounts = 'Y';
497 }
498
499 push @header, 'Allocate';
500 push @fields, 'enable_app';
501 push @types, 'checkbox';
502 push @align, 'c';
503 push @sizes, '0';
504 push @colors, '';
505 push @footer, '';
506 push @footer_align, '';
507 push @onchange, 'toggle_application_row';
508
509 push @header, 'No Auto Allocate';
510 push @fields, 'no_auto_apply';
511 push @types, 'checkbox';
512 push @align, 'c';
513 push @sizes, '0';
514 push @colors, '';
515 push @footer, '';
516 push @footer_align, '';
517 push @onchange, '';
518
519 #push @header, 'Error';
520 push @header, '';
521 push @fields, 'error';
522 push @types, 'immutable';
523 push @align, 'l';
524 push @sizes, '0';
525 push @colors, '#ff0000';
526 push @footer, '';
527 push @footer_align, '';
528 push @onchange, '';
529
530 $m->comp('/elements/handle_uri_query');
531
532 # set up for preloading
533 my %rows;
534 my %row_errors;
535 if ( $cgi->param('error') ) {
536   my $param = $cgi->Vars;
537   my $enum = 0; #errors numbered separately
538   my @invrows = grep /^invnum\d+\.\d+$/, keys %$param; #pare down possibilities
539   foreach my $row ( sort { $a <=> $b } map /^custnum(\d+)$/, keys %$param ) {
540 #  for( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
541     $rows{$row} = [];
542     $row_errors{$row} = $param->{"error$enum"};
543     $enum++;
544     foreach my $app ( map /^invnum$row\.(\d+)$/, @invrows ) {
545       next if !$param->{"invnum$row.$app"};
546       my %this_app = map { $_ => ($param->{$_.$row.'.'.$app} || '') } 
547         qw( invnum amount );
548       $this_app{'error'} = $param->{"error$enum"} || '';
549       $param->{"error$enum"} = ''; # don't pass this error through
550       $rows{$row}[$app] = \%this_app;
551       $enum++;
552     }
553   }
554   foreach my $row (keys %rows) {
555     $param->{"error$row"} = $row_errors{$row};
556   }
557 }
558 #warn Dumper {rows => \%rows, row_errors => \%row_errors };
559
560 </%init>