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