Merge branch 'master' of git.freeside.biz:/home/git/freeside
[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 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];
109   }
110 }
111
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, 
120       function(returned) {
121         update_invoices(rownum, JSON.parse(returned));
122         create_application_row(rownum, 0);
123         next.call(this, rownum);
124       }
125     );
126   } else {
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]);
132       } else {
133         break;
134       }
135     }
136     lock_payment_row(rownum, false);
137   }
138 }
139
140 function lock_payment_row(rownum, flag) {
141 % foreach (qw(invnum custnum customer)) {
142   obj = document.getElementById('<% $_ %>'+rownum);
143   obj.readOnly = flag;
144 % }
145   document.getElementById('enable_app'+rownum).disabled = flag;
146 }
147
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;
155   }
156     
157   tr_app.parentNode.removeChild(tr_app);
158   if ( appnum > 0 ) {
159     document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = '';
160   }
161   else {
162     lock_payment_row(rownum, false);
163     document.getElementById('enable_app'+rownum).checked = false;
164   }
165 }
166
167 function amount_unapplied(rownum) {
168   var appnum = 0;
169   var total = 0;
170   var payment_amount = parseFloat(document.getElementById('paid'+rownum).value)
171                        || 0;
172   while (true) {
173     var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
174     if ( input_amount ) {
175       total += parseFloat(input_amount.value || 0);
176       appnum++;
177     }
178     else {
179       return payment_amount - total;
180     }
181   }
182 }
183
184 var change_app_amount;
185
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;
192   }
193
194   if ( this.value ) {
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'] + '&nbsp;';
199     delete invoices_for_row[rownum][this.value];
200
201     var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
202     if ( input_amount.value == '' ) {
203       input_amount.value = 
204         Math.max(
205           0, Math.min( amount_unapplied(rownum), this_invoice['owed'])
206         ).toFixed(2);
207       // trigger onchange
208       change_app_amount.call(input_amount);
209     }
210   }
211 }
212
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');
218     o.text = label;
219     o.value = value;
220     obj.add(o);
221   }
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);
226   } else {
227     add_opt(this, '', '');
228   }
229   for ( var x in invoices_for_row[rownum] ) {
230     add_opt(this,
231             invoices_for_row[rownum][x].invnum,
232             invoices_for_row[rownum][x].label);
233   }
234 }
235
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 ) {
246
247     create_application_row(rownum, parseInt(appnum) + 1);
248   }
249 }
250
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);
257   
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 ') %>')
263   );
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;
272   
273   td_invnum.appendChild(select_invnum);
274   tr_app.appendChild(td_invnum);
275
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);
284
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);
297
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);
313
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);
322
323   if ( appnum > 0 ) {
324     //remove delete button on the previous row
325     document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = 'none';
326   }
327   rownum++;
328   var next_row = document.getElementById('row'+rownum); // always exists
329   payment_row.parentNode.insertBefore(tr_app, next_row);
330
331 }
332
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) %>;
336 function preload() {
337   var rownum;
338   var appnum;
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;
347           //set invnum
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;
353             }
354           }
355           choose_app_invnum.call(select_invnum);
356           //set amount
357           var input_amount = document.getElementById('amount'+x);
358           input_amount.value = this_app.amount;
359
360           //set error
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
364         } //for appnum
365       }; //preload_row function
366       toggle_application_row.call(enable, null, preload_row);
367     } // if (row_obj[rownum].length
368   } //for rownum
369 }
370
371 </SCRIPT>
372
373 <% include('/elements/xmlhttp.html',
374               'url'  => $p. 'misc/xmlhttp-cust_main-discount_terms.cgi',
375               'subs' => [qw( discount_terms )],
376            )
377 %>
378
379 <FORM ACTION="process/batch-cust_pay.cgi" NAME="OneTrueForm" METHOD="POST" onsubmit="document.OneTrueForm.btnsubmit.disabled=true;window.onbeforeunload = null;">
380
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']
385 &>
386
387 <& /elements/customer-table.html,
388     name_singular => 'payment',
389     header  => \@header,
390     fields  => \@fields,
391     type    => \@types,
392     align   => \@align,
393     size    => \@sizes,
394     color   => \@colors,
395     param   => \%param,
396     footer  => \@footer,
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',
403 &>
404
405 <BR>
406 <INPUT TYPE="button" VALUE="Post payment batch" name="btnsubmit" onclick="window.onbeforeunload = null; document.OneTrueForm.submit(); this.disabled = true;">
407
408 </FORM>
409
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 %>, '');
414 % #}
415 % ###not perl </SCRIPT>
416
417 <% include('/elements/footer.html') %>
418
419 <%init>
420
421 die "access denied"
422   unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch');
423
424 my $conf = new FS::Conf;
425 my $money_char = $conf->config('money_char') || '$';
426
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  = ( '', '' );
433 my %param   = ();
434 my @footer  = ( '_TOTAL', '' );
435 my @footer_align = ( 'r', 'r' );
436 my @onchange = ( '', '' );;
437 my $use_discounts = '';
438
439 # Not entirely sure this works anymore...
440 if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) {
441   #push @header, 'Discount';
442   push @header, '';
443   push @fields, 'discount_term';
444   push @types, 'immutable';
445   push @align, 'r';
446   push @sizes, '0';
447   push @colors, '';
448   push @footer, '';
449   push @footer_align, '';
450   push @onchange, '';
451   $use_discounts = 'Y';
452 }
453
454 push @header, 'Allocate';
455 push @fields, 'enable_app';
456 push @types, 'checkbox';
457 push @align, 'c';
458 push @sizes, '0';
459 push @colors, '';
460 push @footer, '';
461 push @footer_align, '';
462 push @onchange, 'toggle_application_row';
463
464 push @header, 'No Auto Allocate';
465 push @fields, 'no_auto_apply';
466 push @types, 'checkbox';
467 push @align, 'c';
468 push @sizes, '0';
469 push @colors, '';
470 push @footer, '';
471 push @footer_align, '';
472 push @onchange, '';
473
474 #push @header, 'Error';
475 push @header, '';
476 push @fields, 'error';
477 push @types, 'immutable';
478 push @align, 'l';
479 push @sizes, '0';
480 push @colors, '#ff0000';
481 push @footer, '';
482 push @footer_align, '';
483 push @onchange, '';
484
485 $m->comp('/elements/handle_uri_query');
486
487 # set up for preloading
488 my %rows;
489 my %row_errors;
490 if ( $cgi->param('error') ) {
491   my $param = $cgi->Vars;
492   my $enum = 0; #errors numbered separately
493   my @invrows = grep /^invnum\d+\.\d+$/, keys %$param; #pare down possibilities
494   foreach my $row ( sort { $a <=> $b } map /^custnum(\d+)$/, keys %$param ) {
495 #  for( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
496     $rows{$row} = [];
497     $row_errors{$row} = $param->{"error$enum"};
498     $enum++;
499     foreach my $app ( map /^invnum$row\.(\d+)$/, @invrows ) {
500       next if !$param->{"invnum$row.$app"};
501       my %this_app = map { $_ => ($param->{$_.$row.'.'.$app} || '') } 
502         qw( invnum amount );
503       $this_app{'error'} = $param->{"error$enum"} || '';
504       $param->{"error$enum"} = ''; # don't pass this error through
505       $rows{$row}[$app] = \%this_app;
506       $enum++;
507     }
508   }
509   foreach my $row (keys %rows) {
510     $param->{"error$row"} = $row_errors{$row};
511   }
512 }
513 #warn Dumper {rows => \%rows, row_errors => \%row_errors };
514
515 </%init>