invoice_sections_with_taxes per-agent, RT#79636
[freeside.git] / httemplate / elements / customer-table.html
1 <%doc>
2
3 Example:
4
5   include( '/elements/customer-table.html',
6
7              ###
8              # required
9              ###
10
11              #listrefs...
12              'header'        => [ '#', 'Item' ],
13              'fields'        => [
14                                   'column',
15                                   sub { my ($row,$param) = @_;
16                                         $param->{"column$row"};
17                                       },
18                                 ],
19
20              ###
21              # optional
22              ###
23
24              'name_singular' => 'customer', #label
25              'custnum_update_callback' => 'name_of_js_callback' #passed a rownum
26
27              #listrefs
28              'type'          => ['immutable', ''], # immutable, checkbox, date or ''/text
29              'align'         => [ 'c', 'l', 'r', '' ],
30              'size'          => [],                # sizes ignored for immutable
31              'color'         => [],
32              'footer'        => ['string', '_TOTAL'], # strings or the special
33                                                       #value _TOTAL
34              'footer_align'  => [ 'c', 'l', 'r', '' ],
35
36              'param'         => { column0 => 1 },  # preset column of row 0 to 1
37
38          )
39
40 Some incomplete notes for javascript programmers:
41
42 On page load, existing rows are initialized by passing values to addRow
43 based on existing cgi values.  An empty row (marked with the 'emptyrow' 
44 attribute) is created by invoking addRow without values.  After that, 
45 to keep the non-empty row count (totalrows) accurate, use newEmptyRow to 
46 create the next row.  There should only be one empty row at a time.
47
48 Global vars:
49 total_el  - element for displaying total number of rows
50 totalrows - total number of non-empty rows
51 rownum - really more of a "next row" value, used by addRow
52 allrows - array of tr elements, one for each row
53
54 Don't confuse the global rownum with the element attribute rownum
55 that is set as a reference point on some of the elements generated
56 by this script.  They have different values.
57
58 Some of the functions:
59 updateTotalRow() - updates total_el based on value of totalrows
60 addDeleteButton(searchrow) - adds delete button to searchrow
61 newEmptyRow() - replaces old empty row
62 deleteRow() - removes the row specified by this.rownum
63 addRow(values) - adds a new row (marked as empty if values aren't specified)
64
65 This mason element is currently only used by misc/batch-cust_pay.html, 
66 and probably should be cleaned up more before being used by anything else.
67
68 </%doc>
69 <LINK REL="stylesheet" TYPE="text/css" HREF="<%$fsurl%>elements/calendar-win2k-2.css" TITLE="win2k-2">
70 <SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/calendar_stripped.js"></SCRIPT>
71 <SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/calendar-en.js"></SCRIPT>
72 <SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/calendar-setup.js"></SCRIPT>
73 <SCRIPT TYPE="text/javascript">
74
75   var num_open_invoices = new Array;
76
77   function clearhint_invnum() {
78
79     if ( this.value == 'Not found' || this.value == 'Multiple' ) {
80       this.value = '';
81       this.style.color = '#000000';
82     }
83
84   }
85
86   function clearhint_custnum() {
87
88     if ( this.value == 'Not found' || this.value == 'Multiple' ) {
89       this.value = '';
90       this.style.color = '#000000';
91     }
92
93   }
94
95   function clearhint_customer() {
96
97     this.style.color = '#000000';
98
99     if ( this.value == '(last name or company)' || this.value == 'Not found' )
100       this.value = '';
101
102   }
103
104   function update_customer(searchrow, customerArray) {
105       
106       var display_custnum_obj = document.getElementById('display_custnum'+searchrow);
107       var custnum_obj = document.getElementById('custnum'+searchrow);
108       var customer = document.getElementById('customer'+searchrow);
109       var customer_select = document.getElementById('cust_select'+searchrow);
110
111       var postPaymentBtn = document.getElementById('btnsubmit');
112       postPaymentBtn.disabled = false;
113
114       display_custnum_obj.disabled = false;
115       display_custnum_obj.style.backgroundColor = '#ffffff';
116       customer.disabled = false;
117       customer.style.backgroundColor = '#ffffff';
118
119       if ( customerArray.length == 0 ) {
120
121           custnum_obj.value = '';
122           display_custnum_obj.value = 'Not found';
123           customer.value = 'Not found';
124           display_custnum_obj.style.color = '#ff0000';
125           customer.style.color = '#ff0000';
126
127           customer.style.display = '';
128           customer_select.style.display = 'none';
129           return false;
130
131       } else if ( customerArray.length >= 6 ) {
132
133           custnum_obj.value = customerArray[0];
134           display_custnum_obj.value = customerArray[6];
135           display_custnum_obj.style.color = '#000000';
136           customer.value = customerArray[1];
137
138           update_balance_text(searchrow, customerArray[2]);
139           update_status_text( searchrow, customerArray[3]);
140           update_status_color(searchrow, '#'+customerArray[4]);
141           update_num_open(searchrow, customerArray[5]);
142
143           customer.style.display = '';
144           customer_select.style.display = 'none';
145           return true;
146       }
147   }
148
149   function search_invnum() {
150
151     this.style.color = '#000000'
152
153     var invnum_obj = this;
154     var searchrow = this.getAttribute('rownum');
155     var invnum = this.value;
156
157     if ( invnum == 'searching...' || invnum == 'Not found' || invnum == '' )
158       return;
159
160     if ( this.getAttribute('magic') == 'nosearch' ) {
161       this.setAttribute('magic', '');
162       return;
163     }
164
165     if ( document.getElementById('row'+searchrow).emptyrow ) {
166       newEmptyRow(searchrow);
167     }
168     var customer = document.getElementById('customer'+searchrow);
169     customer.value = 'searching...';
170     customer.disabled = true;
171     customer.style.color = '#000000';
172     customer.style.backgroundColor = '#dddddd';
173
174     var customer_select = document.getElementById('cust_select'+searchrow);
175
176     customer.style.display = '';
177     customer_select.style.display = 'none';
178     
179     update_balance_text(searchrow, '');
180     update_status_text(searchrow, '');
181     update_status_color(searchrow, '#000000');
182     update_num_open(searchrow, 0);
183
184     function search_invnum_update(customers) {
185       
186       var customerArray = eval('(' + customers + ')');
187       update_customer(searchrow, customerArray);
188
189 % if ( $opt{invnum_update_callback} ) {
190         <% $opt{invnum_update_callback} %>(searchrow)
191 % }
192
193     }
194
195     invnum_search( invnum, search_invnum_update );
196
197   }
198
199   function search_custnum() {
200
201     this.style.color = '#000000'
202
203     var display_custnum_obj = this;
204     var searchrow = this.getAttribute('rownum');
205     var custnum_obj = document.getElementById('custnum'+searchrow);
206     var display_custnum = this.value;
207
208     if ( display_custnum == 'searching...' || display_custnum == 'Not found' || display_custnum == '' )
209       return;
210
211     if ( this.getAttribute('magic') == 'nosearch' ) {
212       this.setAttribute('magic', '');
213       return;
214     }
215
216     if ( document.getElementById('row'+searchrow).emptyrow ) {
217       newEmptyRow(searchrow);
218     }
219
220     var customer_obj = document.getElementById('customer'+searchrow);
221     customer_obj.value = 'searching...';
222     customer_obj.disabled = true;
223     customer_obj.style.color = '#000000';
224     customer_obj.style.backgroundColor = '#dddddd';
225
226     var customer_select = document.getElementById('cust_select'+searchrow);
227
228     customer_obj.style.display = '';
229     customer_select.style.display = 'none';
230
231     var invnum = document.getElementById('invnum'+searchrow);
232     invnum.value = '';
233
234     update_balance_text(searchrow, '');
235     update_status_text( searchrow, '');
236     update_status_color(searchrow, '#000000');    
237     update_num_open(searchrow, 0);
238
239     function search_custnum_update(customers) {
240
241       var customerArrayArray = eval('(' + customers + ')') || [];
242
243       if ( customerArrayArray.length == 0 ) {
244
245         update_customer(searchrow, []);
246
247       } else if ( customerArrayArray.length == 1 ) {
248
249         update_customer(searchrow, customerArrayArray[0]);
250 % if ( $opt{custnum_update_callback} ) {
251           <% $opt{custnum_update_callback} %>(searchrow)
252 % }
253
254       } else {
255
256         custnum_obj.value = 'Multiple'; // or something
257         custnum_obj.style.color = '#ff0000';
258
259         //blank the current list
260         customer_select.options.length = 0;
261
262         opt(customer_select, '', 'Multiple customers match "' + custnum + '" - select one', '#ff0000');
263         //add the multiple customers
264         for ( var s = 0; s < customerArrayArray.length; s++ ) {
265           opt(customer_select,
266               JSON.stringify(customerArrayArray[s]),
267               customerArrayArray[s][1],
268               '#000000');
269         }
270
271         opt(customer_select, 'cancel', '(Edit search string)', '#000000');
272
273         customer_obj.style.display = 'none';
274
275         customer_select.style.display = '';
276
277       }
278
279     }
280
281     custnum_search(display_custnum, search_custnum_update );
282
283   }
284
285   function search_customer() {
286
287     var customer_obj = this;
288     var searchrow = this.getAttribute('rownum');
289     var customer = this.value;
290
291     if ( customer == 'searching...' || customer == 'Not found' || customer == '' )
292       return;
293
294     if ( this.getAttribute('magic') == 'nosearch' ) {
295       this.setAttribute('magic', '');
296       return;
297     }
298
299     if ( document.getElementById('row'+searchrow).emptyrow ) {
300       newEmptyRow(searchrow);
301     }
302     
303     var invnum = document.getElementById('invnum'+searchrow);
304     invnum.value = '';
305
306     var custnum_obj = document.getElementById('display_custnum'+searchrow);
307     custnum_obj.value = 'searching...';
308     custnum_obj.disabled = true;
309     custnum_obj.style.color = '#000000';
310     custnum_obj.style.backgroundColor = '#dddddd';
311
312     var customer_select = document.getElementById('cust_select'+searchrow);
313     
314     function search_customer_update(customers) {
315
316       var customerArrayArray = eval('(' + customers + ')') || [ [] ];
317
318       custnum_obj.disabled = false;
319       custnum_obj.style.backgroundColor = '#ffffff';
320
321       if ( customerArrayArray.length == 0 ) {
322
323         update_customer(searchrow, []);
324
325       } else if ( customerArrayArray.length == 1 ) {
326
327         update_customer(searchrow, customerArrayArray[0]);
328 % if ( $opt{custnum_update_callback} ) {
329         <% $opt{custnum_update_callback} %>(searchrow)
330 % }
331
332       } else {
333
334         custnum_obj.value = 'Multiple'; // or something
335         custnum_obj.style.color = '#ff0000';
336
337         var postPaymentBtn = document.getElementById('btnsubmit');
338         postPaymentBtn.disabled = true;
339
340         //blank the current list
341         customer_select.options.length = 0;
342
343         opt(customer_select, '', 'Multiple customers match "' + customer + '" - select one', '#ff0000');
344         //add the multiple customers
345         for ( var s = 0; s < customerArrayArray.length; s++ ) {
346           opt(customer_select,
347               JSON.stringify(customerArrayArray[s]),
348               customerArrayArray[s][1],
349               '#000000');
350         }
351
352         opt(customer_select, 'cancel', '(Edit search string)', '#000000');
353
354         customer_obj.style.display = 'none';
355
356         customer_select.style.display = '';
357
358       }
359
360     }
361
362     smart_search( customer, search_customer_update );
363
364   }
365
366   function select_customer() {
367
368     var custnum_balance_status = this.options[this.selectedIndex].value;
369     var customer = this.options[this.selectedIndex].text;
370
371     var searchrow = this.getAttribute('rownum');
372     var display_custnum_obj = document.getElementById('display_custnum'+searchrow);
373     var custnum_obj = document.getElementById('custnum'+searchrow);
374     var customer_obj = document.getElementById('customer'+searchrow);
375     var balance_obj = document.getElementById('balance'+searchrow);
376     var status_obj = document.getElementById('status'+searchrow);
377
378     if ( custnum_balance_status == '' ) {
379
380     } else if ( custnum_balance_status == 'cancel' ) {
381
382       display_custnum_obj.value = '';
383       custnum_obj.value = '';
384       custnum_obj.style.color = '#000000';
385
386       this.style.display = 'none';
387       customer_obj.style.display = '';
388       customer_obj.focus();
389
390     } else {
391     
392       update_customer(searchrow, JSON.parse(custnum_balance_status));
393
394 % if ( $opt{custnum_update_callback} ) {
395       <% $opt{custnum_update_callback} %>(searchrow)
396 % }
397
398     }
399
400   }
401
402   function opt(what,value,text,color) {
403     var optionName = new Option(text, value, false, false);
404     optionName.style.color = color;
405     var length = what.length;
406     what.options[length] = optionName;
407   }
408
409   function update_status_text(rownum, newval) {
410     document.getElementById('status'+rownum).value = newval;
411     document.getElementById('status'+rownum+'_text').innerHTML = newval;
412   }
413
414   function update_status_color(rownum, newval) {
415     document.getElementById('statuscolor'+rownum).value = newval;
416     document.getElementById('status'+rownum+'_text').style.color = newval;
417   }
418
419   function update_balance_text(rownum, newval) {
420     document.getElementById('balance'+rownum).value = newval;
421     document.getElementById('balance'+rownum+'_text').innerHTML = newval;
422   }
423
424   function update_num_open(rownum, newval) {
425     document.getElementById('num_open'+rownum).value = newval;
426     num_open_invoices[rownum] = newval;
427   }
428
429   // updates display of total rows based on value of totalrows
430   function updateTotalRow () {
431     if ( totalrows == 1 ) {
432       total_el.innerHTML =
433         'Total '
434           + totalrows
435           + ' <% $opt{name_singular} || 'customer' %>';
436     } else {
437       total_el.innerHTML =
438         'Total '
439           + totalrows
440           + ' <% PL($opt{name_singular} || 'customer') %>';
441     }
442   }
443
444   var total_el, rownum, totalrows, allrows;
445
446   function addDeleteButton (searchrow) {
447     var td_delete = document.getElementById('delete'+searchrow);
448     var button_delete = document.createElement('INPUT');
449     button_delete.setAttribute('rownum', searchrow);
450     button_delete.setAttribute('type', 'button');
451     button_delete.setAttribute('value', 'X');
452     button_delete.onclick = deleteRow;
453     button_delete.style.color = '#ff0000';
454     button_delete.style.fontWeight = 'bold';
455     button_delete.style.paddingLeft = '2px';
456     button_delete.style.paddingRight = '2px';
457     td_delete.appendChild(button_delete);
458   }
459
460   function newEmptyRow (searchrow) {
461     // add delete button to current row
462     addDeleteButton(searchrow);
463     // mark current row as non-empty
464     var oldemptyrow = document.getElementById('row'+searchrow);
465     oldemptyrow.emptyrow = false;
466     // update totalrows
467     totalrows++
468     updateTotalRow();
469     // add a new empty row
470     addRow();
471   }
472
473   function deleteRow() {
474     var thisrownum = this.getAttribute('rownum');
475 % if ( $opt{delete_row_callback} ) {
476     // callback
477     <% $opt{delete_row_callback} %>(thisrownum);
478 % }
479     // remove the actual row
480     var thisrow = document.getElementById('row'+thisrownum);
481     thisrow.parentNode.removeChild(thisrow);
482     // remove row from tally of all rows
483     var newrows = [];
484     for (i = 0; i < allrows.length; i++) {
485       if (allrows[i] == thisrownum) continue;
486       newrows.push(allrows[i]);
487     }
488     allrows = newrows;
489     totalrows--; // should never be deleting empty rows
490     updateTotalRow();
491     // recalculate column totals, if any
492 % my $col = 0;
493 % foreach my $footer ( @{$opt{footer}} ) {
494 %   if ($footer eq '_TOTAL' ) {
495     calc_total<% $col %>()
496 %   }
497 %   $col++;
498 % }
499   }
500
501   function addRow(values) {
502
503     var table = document.getElementById('OneTrueTable');
504     var tablebody = table.getElementsByTagName('tbody').item(0);
505
506     var row = table.insertRow(table.rows.length - 1);
507     var thisrownum = values ? values.rownum : rownum;
508     row.setAttribute('id', 'row'+thisrownum);
509     row.emptyrow = values ? false : true;
510     
511     var invnum_cell = document.createElement('TD');
512
513       var invnum_input = document.createElement('INPUT');
514       invnum_input.setAttribute('name', 'invnum'+thisrownum);
515       invnum_input.setAttribute('id',   'invnum'+thisrownum);
516       invnum_input.style.textAlign = 'right';
517       invnum_input.setAttribute('size', 8);
518       invnum_input.setAttribute('maxlength', 12);
519       invnum_input.setAttribute('rownum', thisrownum);
520       invnum_input.value = values ? values.invnum : '';
521       invnum_input.onfocus = clearhint_invnum;
522       invnum_input.onchange = search_invnum;
523       invnum_cell.appendChild(invnum_input);
524
525     row.appendChild(invnum_cell);
526
527     var custnum_cell = document.createElement('TD');
528
529       var display_custnum_input = document.createElement('INPUT');
530       display_custnum_input.setAttribute('name', 'display_custnum'+thisrownum);
531       display_custnum_input.setAttribute('id',   'display_custnum'+thisrownum);
532       display_custnum_input.style.textAlign = 'right';
533       display_custnum_input.setAttribute('size', 8);
534       display_custnum_input.setAttribute('maxlength', 12);
535       display_custnum_input.setAttribute('rownum', thisrownum);
536       display_custnum_input.value = values ? values.custnum : '';
537       display_custnum_input.onfocus = clearhint_custnum;
538       display_custnum_input.onchange = search_custnum;
539       custnum_cell.appendChild(display_custnum_input);
540
541       var custnum_input = document.createElement('INPUT');
542       custnum_input.type = 'hidden';
543       custnum_input.setAttribute('name', 'custnum'+thisrownum);
544       custnum_input.setAttribute('id',   'custnum'+thisrownum);
545       custnum_input.setAttribute('rownum', thisrownum);
546       custnum_input.value = values ? values.custnum : '';
547       custnum_cell.appendChild(custnum_input);
548
549     row.appendChild(custnum_cell);
550     
551     var status_cell = document.createElement('TD');
552       status_cell.style.textAlign = 'center';
553         
554       var status_span = document.createElement('SPAN');
555       status_span.setAttribute('id', 'status'+thisrownum+'_text');
556       status_span.style.fontWeight = 'bold';
557       status_span.style.color = values ? values.statuscolor : '';
558       status_span.setAttribute('rownum', thisrownum);
559       status_span.appendChild(
560         document.createTextNode(values ? values.status : '')
561       );
562       status_cell.appendChild(status_span);
563         
564       var status_input = document.createElement('INPUT');
565       status_input.setAttribute('type', 'hidden');
566       status_input.setAttribute('name', 'status'+thisrownum);
567       status_input.setAttribute('id',   'status'+thisrownum);
568       status_input.setAttribute('rownum', thisrownum);
569       status_input.value = values ? values.status : '';
570       status_cell.appendChild(status_input);
571
572       var statuscolor_input = document.createElement('INPUT');
573       statuscolor_input.setAttribute('type', 'hidden');
574       statuscolor_input.setAttribute('name', 'statuscolor'+thisrownum);
575       statuscolor_input.setAttribute('id',   'statuscolor'+thisrownum);
576       statuscolor_input.setAttribute('rownum', thisrownum);
577       statuscolor_input.value = values ? values.statuscolor : '';
578       status_cell.appendChild(statuscolor_input);
579
580     row.appendChild(status_cell);
581
582     var customer_cell = document.createElement('TD');
583
584       var customer_input = document.createElement('INPUT');
585       customer_input.setAttribute('name', 'customer'+thisrownum);
586       customer_input.setAttribute('id',   'customer'+thisrownum);
587       customer_input.setAttribute('size', 64);
588       customer_input.setAttribute('value', '(last name or company)' );
589       customer_input.setAttribute('rownum', thisrownum);
590       customer_input.value = values ? values.customer : '';
591       customer_input.onfocus = clearhint_customer;
592       customer_input.onclick = clearhint_customer;
593       customer_input.onchange = search_customer;
594       customer_cell.appendChild(customer_input);
595
596       var customer_select = document.createElement('SELECT');
597       customer_select.setAttribute('name', 'cust_select'+thisrownum);
598       customer_select.setAttribute('id',   'cust_select'+thisrownum);
599       customer_select.setAttribute('rownum', thisrownum);
600       customer_select.style.color = '#ff0000';
601       customer_select.style.display = 'none';
602       customer_select.onchange = select_customer;
603       customer_cell.appendChild(customer_select);
604
605     row.appendChild(customer_cell);
606     
607     var balance_cell = document.createElement('TD');
608
609       balance_cell.style.textAlign = 'right';
610       balance_cell.appendChild(document.createTextNode('<%$money_char%>'));
611
612       var balance_span = document.createElement('SPAN');
613       balance_span.setAttribute('id', 'balance'+thisrownum+'_text');
614       balance_span.setAttribute('rownum', thisrownum);
615       balance_cell.appendChild(balance_span);
616
617       balance_cell.appendChild(
618         document.createTextNode(String.fromCharCode(160) + (values ? values.balance : '')) //&nbsp;
619       );
620
621       var balance_input = document.createElement('INPUT');
622       balance_input.setAttribute('type', 'hidden');
623       balance_input.setAttribute('name', 'balance'+thisrownum);
624       balance_input.setAttribute('id',   'balance'+thisrownum);
625       balance_input.setAttribute('rownum', thisrownum);
626       balance_input.value = values ? values.balance : '';
627       balance_cell.appendChild(balance_input);
628
629       var num_open_input = document.createElement('INPUT');
630       num_open_input.setAttribute('type', 'hidden');
631       num_open_input.setAttribute('name', 'num_open'+thisrownum);
632       num_open_input.setAttribute('id',   'num_open'+thisrownum);
633       num_open_input.setAttribute('rownum', thisrownum);
634       balance_cell.appendChild(num_open_input);
635
636     row.appendChild(balance_cell);
637
638 %   my $col = 0;
639 %   foreach my $field ( @{$opt{fields}} ) {
640
641       var my_cell = document.createElement('TD');
642       my_cell.setAttribute('align', '<% $align{ $opt{align}->[$col] || 'l' } %>');
643 %     if ($opt{'color'}->[$col]) {
644       my_cell.style.color = '<% $opt{color}->[$col] %>';
645 %     }
646
647 %     if ($types->[$col] eq 'immutable') {
648         var my_text = document.createTextNode(values ? values.<% $field %> : '');
649         my_cell.appendChild(my_text);
650 %     }
651
652 %     my $name  = (ref($field) eq 'CODE') ? "column${col}_" : $field;
653       var my_input = document.createElement('INPUT');
654       my_input.setAttribute('name', '<% $name %>'+thisrownum);
655       my_input.setAttribute('id',   '<% $name %>'+thisrownum);
656       my_input.style.textAlign = '<% $align{ $opt{align}->[$col] || 'l' } %>';
657       my_input.setAttribute('size', <% $sizes->[$col] || 10 %>);
658       my_input.setAttribute('rownum', thisrownum);
659 %     if ( $types->[$col] eq 'immutable' ) {
660       my_input.setAttribute('type', 'hidden');
661 %     } elsif ( $types->[$col] eq 'checkbox' ) {
662       my_input.setAttribute('type', 'checkbox');
663       my_input.checked = (values && values.<% $field %>) ? true : false;
664 %     } elsif ( $types->[$col] eq 'date' ) {
665       my_input_button = document.createElement('IMG');
666       my_input_button.setAttribute('src', '<% $fsurl %>images/calendar.png');
667       my_input_button.setAttribute('title', <% mt('Select date') |js_string %>);
668       my_input_button.setAttribute('name', '<% $name %>'+thisrownum+'button');
669       my_input_button.setAttribute('id',   '<% $name %>'+thisrownum+'button');
670 %     }
671       my_input.value = (values && values.<% $field %>) || '';
672 %     if ( $opt{onchange}->[$col] ) {
673         my_input.onchange   = <% $opt{onchange}->[$col] %>;
674 %     }
675 %     elsif ( $opt{footer}->[$col] eq '_TOTAL' ) {
676         my_input.onchange   = calc_total<%$col%>;
677         my_input.onkeyup    = calc_total<%$col%>;
678 %     }
679       my_cell.appendChild(my_input);
680 %     if ( $types->[$col] eq 'date' ) {
681       my_cell.appendChild(my_input_button);
682 %     }
683
684     row.appendChild(my_cell);
685
686 %     if ( $types->[$col] eq 'date' ) {
687       Calendar.setup({
688         inputField: '<% $name %>'+thisrownum,
689         ifFormat:   "<% $date_format %>",
690         button:     '<% $name %>'+thisrownum+'button',
691         align:      "BR"
692       });
693 %     }
694
695 %     $col++;
696 %   }
697
698     var td_delete = document.createElement('TD');
699     td_delete.setAttribute('id', 'delete'+thisrownum);
700     row.appendChild(td_delete);
701     if (values) {
702       addDeleteButton(thisrownum);
703     }
704
705     update_num_open(thisrownum, (values ? values.num_open : '0'));
706
707 % if ( $opt{add_row_callback} ) {
708     <% $opt{add_row_callback} %>(thisrownum, values);
709 % }
710
711     // update the total number of rows display
712     allrows.push(thisrownum);
713     if (values) totalrows++;
714     updateTotalRow();
715
716     // update the next available row number
717     if (thisrownum >= rownum) {
718       rownum = thisrownum + 1;
719     }
720
721   } // end of addRow
722
723
724 </SCRIPT>
725
726 <TABLE ID="OneTrueTable" CLASS="fsinnerbox">
727
728 <TR>
729   <TH>Inv #</TH>
730   <TH>Cust #</TH>
731   <TH>Status</TH>
732   <TH>Customer</TH>
733   <TH>Balance</TH>
734 % foreach my $header ( @{$opt{header}} ) {
735     <TH><% $header %></TH>
736 % }
737 </TR>
738
739 % my @rownums = sort { $a <=> $b } map /^custnum(\d+)$/, keys %$param;
740 <TR id="row_total">
741   <TH COLSPAN=5 ID="_TOTAL_TOTAL">
742     Total <% @rownums || 0 %>
743     <% PL($opt{name_singular} || 'customer', ( @rownums || 0 ) ) %>
744   </TH>
745 % my $col = 0;
746 % foreach my $footer ( @{$opt{footer}} ) {
747 %   my $align = $align{ $opt{'footer_align'}->[$col] || 'c' };
748 %   if ($footer eq '_TOTAL' ) {
749 %     my $id = $opt{'fields'}->[$col];
750 %     $id = ref($id) ? "column${col}_TOTAL" : "${id}_TOTAL";
751       <TH ALIGN="<% $align %>" ID="<% $id %>">&nbsp;<% sprintf('%.2f', $total[$col] ) %></TH>
752 %   } else {
753       <TH ALIGN="<% $align %>"><% $footer %></TH>
754 %   }
755 %   $col++;
756 % }
757 </TR>
758
759 </TABLE>
760
761 <SCRIPT TYPE="text/javascript">
762
763 total_el =
764   document.getElementById("_TOTAL_TOTAL");
765
766 rownum = 1; // really more of a "next row", used by addrow
767 totalrows = 0; // will not include empty rows
768 allrows = []; // will include empty rows
769
770 % foreach my $row ( @rownums ) {
771 %   if ( grep($param->{$_.$row},qw(invnum display_custnum custnum status statuscolor customer balance),@{$opt{fields}} ) ) {
772
773 addRow({
774   rownum:<% $row %>,
775   num_open:<% $param->{"num_open$row"} |js_string %>,
776   invnum:<% $param->{"invnum$row"} |js_string %>,
777   display_custnum:<% $param->{"display_custnum$row"} |js_string %>,
778   custnum:<% $param->{"custnum$row"} |js_string %>,
779   status:<% $param->{"status$row"} |js_string %>,
780   statuscolor:<% $param->{"statuscolor$row"} |js_string %>,
781   customer:<% $param->{"customer$row"} |js_string %>,
782   balance:<% $param->{"balance$row"} |js_string %>,
783 %     my $col = 0;
784 %     foreach my $field ( @{$opt{fields}} ) {
785 %       my $value;
786 %       if ( ref($field) eq 'CODE' ) {
787 %         $value = &{$field}($row,$param) || '';
788 %       } else {
789 %         $value = $param->{"$field$row"} || '';
790 %       }
791 %       my $name  = (ref($field) eq 'CODE') ? "column${col}" : "$field";
792   <% $name %>:<% $value |js_string %>,
793 %       $col++;
794 %     }
795 });
796 %   }
797 % }
798
799 addRow();
800
801 % my $col = 0;
802 % foreach my $footer ( @{$opt{footer}} ) {
803 %   if ($footer eq '_TOTAL' ) {
804 %     my $name = $opt{fields}->[$col];
805 %     $name = ref($name) ? "column$col" : $name;
806       var th_el = document.getElementById("<%$name%>_TOTAL");
807       function calc_total<% $col %>() {
808         var row = 0;
809         var total = 0;
810         for (i = 0; i < allrows.length; i++) {
811           var value = document.getElementById("<%$name%>"+allrows[i]).value;
812           value = parseFloat(value);
813           if ( ! isNaN(value) ) {
814             total = total + value;
815           }
816         }
817         th_el.innerHTML = '&nbsp;' + total.toFixed(2);
818       }
819       calc_total<% $col %>()
820 %   }
821 %   $col++;
822 % }
823 </SCRIPT>
824
825 <% include('/elements/xmlhttp.html',
826               'url'  => $p. 'misc/xmlhttp-cust_main-search.cgi',
827               'subs' => [qw( custnum_search smart_search invnum_search )],
828            )
829 %>
830
831 <%init>
832
833 my(%opt) = @_;
834 my $conf = new FS::Conf;
835 my $date_format = $conf->config('date_format') || '%m/%d/%Y';
836
837 my $types = $opt{'type'} ? [ @{$opt{'type'}} ] : [];
838 my $sizes = $opt{'size'} ? [ @{$opt{'size'}} ] : [];
839
840 my $param = $opt{param};
841 $param = $cgi->Vars if $cgi->param('error');
842
843 $opt{$_} ||= [] foreach qw(align color footer footer_align);
844
845 my @total = map 0, @{$opt{footer}};
846
847 my %align = (
848   'l' => 'left',
849   'r' => 'right',
850   'c' => 'center',
851 );
852
853 my $money_char = $conf->config('money_char') || '$';
854 </%init>