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