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