more detail when selecting invoices in quick payment entry, #15861
[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, prefix) {
30   document.getElementById('enable_app'+rownum).disabled = true;
31 }
32
33 function custnum_update_callback(rownum, prefix) {
34   var custnum = document.getElementById('custnum'+rownum).value;
35   // if there is a custnum and more than one open invoice, enable
36   // (and check) the box
37   var show_applications = !(custnum > 0 && num_open_invoices[rownum] > 1);
38   var enable_app_checkbox = document.getElementById('enable_app'+rownum);
39   enable_app_checkbox.disabled = show_applications;
40
41 % if ( $use_discounts ) {
42   select_discount_term(rownum, prefix);
43 % }
44 }
45
46 function invnum_update_callback(rownum, prefix) {
47   custnum_update_callback(rownum, prefix);
48 }
49
50 function select_discount_term(row, prefix) {
51   var custnum_obj = document.getElementById('custnum'+prefix+row);
52   var select_obj = document.getElementById('discount_term'+prefix+row);
53
54   var value = '';
55   if (select_obj.type == 'hidden') {
56     value = select_obj.value;
57   }
58
59   var term_select = document.createElement('SELECT');
60   term_select.setAttribute('name', 'discount_term'+row);
61   term_select.setAttribute('id',   'discount_term'+row);
62   term_select.setAttribute('rownum', row);
63   term_select.style.display = '';
64   select_obj.parentNode.replaceChild(term_select, select_obj);
65   opt(term_select, '', '1 month');
66   
67   function select_discount_term_update(discount_terms) {
68
69     var termArray = eval('(' + discount_terms + ')');
70     for ( var t = 0; t < termArray.length; t++ ) {
71       opt(term_select, termArray[t][0], termArray[t][1]);
72       if (termArray[t][0] == value) {
73         term_select.selectedIndex = t+1;
74       }
75     }
76
77   }
78
79   discount_terms(custnum_obj.value, select_discount_term_update);
80
81 }
82
83 var invoices_for_row = new Object;
84
85 function update_invoices(rownum, invoices) {
86   invoices_for_row[rownum] = new Object;
87   // only called before create_application_row
88   for ( var i=0; i<invoices.length; i++ ) {
89     invoices_for_row[rownum][ invoices[i].invnum ] = invoices[i];
90   }
91 }
92
93 function toggle_application_row(ev, next) {
94   if (!next) next = function(){}; //optional continuation
95   var rownum = this.getAttribute('rownum');
96   if ( this.checked ) {
97     var custnum = document.getElementById('custnum'+rownum).value;
98     if (!custnum) return;
99     lock_payment_row(rownum, true);
100     custnum_search_open( custnum, 
101       function(returned) {
102         update_invoices(rownum, JSON.parse(returned));
103         create_application_row(rownum, 0);
104         next.call(this, rownum);
105       }
106     );
107   } else {
108     var row = document.getElementById('row'+rownum);
109     var table_rows = row.parentNode.rows;
110     for (i = row.sectionRowIndex; i < table_rows.count; i++) {
111       if ( table_rows[i].id.indexof('row'+rownum+'.') > -1 ) {
112         table_rows.removeChild(table_rows[i]);
113       } else {
114         break;
115       }
116     }
117     lock_payment_row(rownum, false);
118   }
119 }
120
121 function lock_payment_row(rownum, flag) {
122 % foreach (qw(invnum custnum customer)) {
123   obj = document.getElementById('<% $_ %>'+rownum);
124   obj.readOnly = flag;
125 % }
126   document.getElementById('enable_app'+rownum).disabled = flag;
127 }
128
129 function delete_application_row() {
130   var rownum = this.getAttribute('rownum');
131   var appnum = this.getAttribute('appnum');
132   var tr_app = document.getElementById('row'+rownum+'.'+appnum);
133   var select_invnum = document.getElementById('invnum'+rownum+'.'+appnum);
134   if ( select_invnum.value ) {
135     invoices_for_row[rownum][ select_invnum.value ] = select_invnum.curr_invoice;
136   }
137     
138   tr_app.parentNode.removeChild(tr_app);
139   if ( appnum > 0 ) {
140     document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = '';
141   }
142   else {
143     lock_payment_row(rownum, false);
144     document.getElementById('enable_app'+rownum).checked = false;
145   }
146 }
147
148 function amount_unapplied(rownum) {
149   var appnum = 0;
150   var total = 0;
151   var payment_amount = parseFloat(document.getElementById('paid'+rownum).value)
152                        || 0;
153   while (true) {
154     var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
155     if ( input_amount ) {
156       total += parseFloat(input_amount.value || 0);
157       appnum++;
158     }
159     else {
160       return payment_amount - total;
161     }
162   }
163 }
164
165 var change_app_amount;
166
167 function choose_app_invnum() {
168   var rownum = this.getAttribute('rownum');
169   var appnum = this.getAttribute('appnum');
170   var last_invoice = this.curr_invoice;
171   if ( last_invoice ) {
172     invoices_for_row[rownum][ last_invoice['invnum'] ] = last_invoice;
173   }
174
175   if ( this.value ) {
176     var this_invoice = invoices_for_row[rownum][this.value];
177     this.curr_invoice = invoices_for_row[rownum][this.value];
178     var span_owed = document.getElementById('owed'+rownum+'.'+appnum);
179     span_owed.innerHTML = this_invoice['owed'] + '&nbsp;';
180     delete invoices_for_row[rownum][this.value];
181
182     var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
183     if ( input_amount.value == '' ) {
184       input_amount.value = 
185         Math.max(
186           0, Math.min( amount_unapplied(rownum), this_invoice['owed'])
187         ).toFixed(2);
188       // trigger onchange
189       change_app_amount.call(input_amount);
190     }
191   }
192 }
193
194 function focus_app_invnum() {
195 % # invoice numbers just display as invoice numbers
196   var rownum = this.getAttribute('rownum');
197   var add_opt = function(obj, value, label) {
198     var o = document.createElement('OPTION');
199     o.text = label;
200     o.value = value;
201     obj.add(o);
202   }
203   this.options.length = 0;
204   var this_invoice = this.curr_invoice;
205   if ( this_invoice ) {
206     add_opt(this, this_invoice.invnum, this_invoice.label);
207   } else {
208     add_opt(this, '', '');
209   }
210   for ( var x in invoices_for_row[rownum] ) {
211     add_opt(this,
212             invoices_for_row[rownum][x].invnum,
213             invoices_for_row[rownum][x].label);
214   }
215 }
216
217 function change_app_amount() {
218   var rownum = this.getAttribute('rownum');
219   var appnum = this.getAttribute('appnum');
220 %# maybe some kind of warning if amount_unapplied < 0?
221 %# only spawn a new application row if there are open invoices left,
222 %# and this is the highest-numbered application row for the customer,
223 %# and the sum of the applied amounts is < the amount of the payment,
224   if ( Object.keys(invoices_for_row[rownum]).length > 0
225        && !document.getElementById( 'row'+rownum+'.'+(parseInt(appnum) + 1) )
226        && amount_unapplied(rownum) > 0 ) {
227
228     create_application_row(rownum, parseInt(appnum) + 1);
229   }
230 }
231
232 function create_application_row(rownum, appnum) {
233   var payment_row = document.getElementById('row'+rownum);
234   var tr_app = document.createElement('TR');
235   tr_app.setAttribute('rownum', rownum);
236   tr_app.setAttribute('appnum', appnum);
237   tr_app.setAttribute('id', 'row'+rownum+'.'+appnum);
238   
239   var td_invnum = document.createElement('TD');
240   td_invnum.setAttribute('colspan', 4);
241   td_invnum.style.textAlign = 'right';
242   td_invnum.appendChild(
243     document.createTextNode('<% mt('Apply to Invoice ') %>')
244   );
245   var select_invnum = document.createElement('SELECT');
246   select_invnum.setAttribute('rownum', rownum);
247   select_invnum.setAttribute('appnum', appnum);
248   select_invnum.setAttribute('id', 'invnum'+rownum+'.'+appnum);
249   select_invnum.setAttribute('name', 'invnum'+rownum+'.'+appnum);
250   select_invnum.className = 'select_invnum';
251   select_invnum.onchange = choose_app_invnum;
252   select_invnum.onfocus  = focus_app_invnum;
253   
254   td_invnum.appendChild(select_invnum);
255   tr_app.appendChild(td_invnum);
256
257   var td_owed = document.createElement('TD');
258   td_owed.style.textAlign= 'right';
259   var span_owed = document.createElement('SPAN');
260   span_owed.setAttribute('rownum', rownum);
261   span_owed.setAttribute('appnum', appnum);
262   span_owed.setAttribute('id', 'owed'+rownum+'.'+appnum);
263   td_owed.appendChild(span_owed);
264   tr_app.appendChild(td_owed);
265
266   var td_amount = document.createElement('TD');
267   td_amount.style.textAlign = 'right';
268   var input_amount = document.createElement('INPUT');
269   input_amount.size = 6;
270   input_amount.setAttribute('rownum', rownum);
271   input_amount.setAttribute('appnum', appnum);
272   input_amount.setAttribute('name', 'amount'+rownum+'.'+appnum);
273   input_amount.setAttribute('id', 'amount'+rownum+'.'+appnum);
274   input_amount.style.textAlign = 'right';
275   input_amount.onchange = change_app_amount;
276   td_amount.appendChild(input_amount);
277   tr_app.appendChild(td_amount);
278
279   var td_delete = document.createElement('TD');
280   td_delete.setAttribute('colspan', <% scalar(@fields)-2 %>);
281   var button_delete = document.createElement('INPUT');
282   button_delete.setAttribute('rownum', rownum);
283   button_delete.setAttribute('appnum', appnum);
284   button_delete.setAttribute('id', 'delete'+rownum+'.'+appnum);
285   button_delete.setAttribute('type', 'button');
286   button_delete.setAttribute('value', 'X');
287   button_delete.onclick = delete_application_row;
288   button_delete.style.color = '#ff0000';
289   button_delete.style.fontWeight = 'bold';
290   button_delete.style.paddingLeft = '2px';
291   button_delete.style.paddingRight = '2px';
292   td_delete.appendChild(button_delete);
293   tr_app.appendChild(td_delete);
294
295   var td_error = document.createElement('TD');
296   var span_error = document.createElement('SPAN');
297   span_error.setAttribute('rownum', rownum);
298   span_error.setAttribute('appnum', appnum);
299   span_error.setAttribute('id', 'error'+rownum+'.'+appnum);
300   span_error.style.color = '#ff0000';
301   td_error.appendChild(span_error);
302   tr_app.appendChild(td_error);
303
304   if ( appnum > 0 ) {
305     //remove delete button on the previous row
306     document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = 'none';
307   }
308   rownum++;
309   var next_row = document.getElementById('row'+rownum); // always exists
310   payment_row.parentNode.insertBefore(tr_app, next_row);
311
312 }
313
314 %# for error handling--ugly, but the alternative is translating the whole 
315 %# process of creating rows into Mason
316 var row_array = <% encode_json(\@rows) %>;
317 function preload() {
318   var rownum;
319   var appnum;
320   for (rownum=0; rownum < row_array.length; rownum++) {
321     if ( row_array[rownum].length ) {
322       var enable = document.getElementById('enable_app'+rownum);
323       enable.checked = true;
324       var preload_row = function(r) {//continuation from toggle_application_row
325         for (appnum=0; appnum < row_array[r].length; appnum++) {
326           this_app = row_array[r][appnum];
327           var x = r + '.' + appnum;
328           //set invnum
329           var select_invnum = document.getElementById('invnum'+x);
330           focus_app_invnum.call(select_invnum);
331           for (i=0; i<select_invnum.options.length; i++) {
332             if (select_invnum.options[i].value == this_app.invnum) {
333               select_invnum.selectedIndex = i;
334             }
335           }
336           choose_app_invnum.call(select_invnum);
337           //set amount
338           var input_amount = document.getElementById('amount'+x);
339           input_amount.value = this_app.amount;
340
341           //set error
342           var span_error = document.getElementById('error'+x);
343           span_error.innerHTML = this_app.error;
344           change_app_amount.call(input_amount); //creates next row
345         } //for appnum
346       }; //preload_row function
347       toggle_application_row.call(enable, null, preload_row);
348     } // if row_array[rownum].length
349   } //for rownum
350 }
351
352 </SCRIPT>
353
354 <% include('/elements/xmlhttp.html',
355               'url'  => $p. 'misc/xmlhttp-cust_main-discount_terms.cgi',
356               'subs' => [qw( discount_terms )],
357            )
358 %>
359
360 <FORM ACTION="process/batch-cust_pay.cgi" NAME="OneTrueForm" METHOD="POST" onsubmit="document.OneTrueForm.btnsubmit.disabled=true;window.onbeforeunload = null;">
361
362 <!-- <B>Batch</B> <INPUT TYPE="text" NAME="paybatch"><BR><BR> -->
363 <& /elements/xmlhttp.html,
364     url => $p.'misc/xmlhttp-cust_bill-search.html',
365     subs => ['custnum_search_open']
366 &>
367
368 <& /elements/customer-table.html,
369     name_singular => 'payment',
370     header  => \@header,
371     fields  => \@fields,
372     type    => \@types,
373     align   => \@align,
374     size    => \@sizes,
375     color   => \@colors,
376     param   => \%param,
377     footer  => \@footer,
378     footer_align => \@footer_align,
379     onchange => \@onchange,
380     custnum_update_callback => 'custnum_update_callback',
381     invnum_update_callback => 'invnum_update_callback',
382     add_row_callback => 'add_row_callback',
383 &>
384
385 <BR>
386 <INPUT TYPE="button" VALUE="Post payment batch" name="btnsubmit" onclick="window.onbeforeunload = null; document.OneTrueForm.submit(); this.disabled = true;">
387
388 </FORM>
389
390 %if ( $cgi->param('error') ) {
391 <SCRIPT TYPE="text/javascript">
392 %  for ( my $row = 0; defined($cgi->param("custnum$row")); $row++ ) {
393      select_discount_term(<% $row %>, '');
394 %  }
395 </SCRIPT>
396 %}
397
398 <% include('/elements/footer.html') %>
399
400 <%init>
401
402 die "access denied"
403   unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch');
404
405 my $conf = new FS::Conf;
406 my $money_char = $conf->config('money_char') || '$';
407
408 my @header  = ( 'Amount', 'Check #' );
409 my @fields  = ( 'paid', 'payinfo' );
410 my @types   = ( '', '' );
411 my @align   = ( 'r', 'r' );
412 my @sizes   = ( 8, 10 );
413 my @colors  = ( '', '' );
414 my %param   = ();
415 my @footer  = ( '_TOTAL', '' );
416 my @footer_align = ( 'r', 'r' );
417 my @onchange = ( '', '' );;
418 my $use_discounts = '';
419
420 if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) {
421   #push @header, 'Discount';
422   push @header, '';
423   push @fields, 'discount_term';
424   push @types, 'immutable';
425   push @align, 'r';
426   push @sizes, '0';
427   push @colors, '';
428   push @footer, '';
429   push @footer_align, '';
430   push @onchange, '';
431   $use_discounts = 'Y';
432 }
433
434 push @header, 'Allocate';
435 push @fields, 'enable_app';
436 push @types, 'checkbox';
437 push @align, 'c';
438 push @sizes, '0';
439 push @colors, '';
440 push @footer, '';
441 push @footer_align, '';
442 push @onchange, 'toggle_application_row';
443
444 #push @header, 'Error';
445 push @header, '';
446 push @fields, 'error';
447 push @types, 'immutable';
448 push @align, 'l';
449 push @sizes, '0';
450 push @colors, '#ff0000';
451 push @footer, '';
452 push @footer_align, '';
453 push @onchange, '';
454
455 $m->comp('/elements/handle_uri_query');
456
457 # set up for preloading
458 my @rows;
459 my @row_errors;
460 if ( $cgi->param('error') ) {
461   my $param = $cgi->Vars;
462   my $enum = 0; #errors numbered separately
463   for( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
464     $rows[$row] = [];
465     $row_errors[$row] = $param->{"error$enum"};
466     $enum++;
467     for( my $app = 0; exists($param->{"invnum$row.$app"}); $app++ ) {
468       next if !$param->{"invnum$row.$app"};
469       my %this_app = map { $_ => ($param->{$_.$row.'.'.$app} || '') } 
470         qw( invnum amount );
471       $this_app{'error'} = $param->{"error$enum"} || '';
472       $param->{"error$enum"} = ''; # don't pass this error through
473       $rows[$row][$app] = \%this_app;
474       $enum++;
475     }
476   }
477   for( my $row = 0; $row < @row_errors; $row++ ) {
478     $param->{"error$row"} = $row_errors[$row];
479   }
480 }
481 #warn Dumper {rows => \@rows, row_errors => \@row_errors };
482
483 </%init>