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