5 include( '/elements/customer-table.html',
12 'header' => [ '#', 'Item' ],
15 sub { my ($row,$param) = @_;
16 $param->{"column$row"};
24 'name_singular' => 'customer', #label
25 'custnum_update_callback' => 'name_of_js_callback' #passed a rownum
28 'types' => ['immutable', ''], # immutable or ''/text
29 'align' => [ 'c', 'l', 'r', '' ],
30 'size' => [], # sizes ignored for immutable
32 'footer' => ['string', '_TOTAL'], # strings or the special
34 'footer_align' => [ 'c', 'l', 'r', '' ],
36 'param' => { column0 => 1 }, # preset column of row 0 to 1
40 Some incomplete notes for javascript programmers:
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.
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
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.
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)
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.
70 <SCRIPT TYPE="text/javascript">
72 var num_open_invoices = new Array;
74 function clearhint_invnum() {
76 if ( this.value == 'Not found' || this.value == 'Multiple' ) {
78 this.style.color = '#000000';
83 function clearhint_custnum() {
85 if ( this.value == 'Not found' || this.value == 'Multiple' ) {
87 this.style.color = '#000000';
92 function clearhint_customer() {
94 this.style.color = '#000000';
96 if ( this.value == '(last name or company)' || this.value == 'Not found' )
101 function update_customer(searchrow, customerArray) {
103 var display_custnum_obj = document.getElementById('display_custnum'+searchrow);
104 var custnum_obj = document.getElementById('custnum'+searchrow);
105 var customer = document.getElementById('customer'+searchrow);
106 var customer_select = document.getElementById('cust_select'+searchrow);
108 display_custnum_obj.disabled = false;
109 display_custnum_obj.style.backgroundColor = '#ffffff';
110 customer.disabled = false;
111 customer.style.backgroundColor = '#ffffff';
113 if ( customerArray.length == 0 ) {
115 custnum_obj.value = '';
116 display_custnum_obj.value = 'Not found';
117 customer.value = 'Not found';
118 display_custnum_obj.style.color = '#ff0000';
119 customer.style.color = '#ff0000';
121 customer.style.display = '';
122 customer_select.style.display = 'none';
125 } else if ( customerArray.length >= 6 ) {
127 custnum_obj.value = customerArray[0];
128 display_custnum_obj.value = customerArray[6];
129 display_custnum_obj.style.color = '#000000';
130 customer.value = customerArray[1];
132 update_balance_text(searchrow, customerArray[2]);
133 update_status_text( searchrow, customerArray[3]);
134 update_status_color(searchrow, '#'+customerArray[4]);
135 update_num_open(searchrow, customerArray[5]);
137 customer.style.display = '';
138 customer_select.style.display = 'none';
143 function search_invnum() {
145 this.style.color = '#000000'
147 var invnum_obj = this;
148 var searchrow = this.getAttribute('rownum');
149 var invnum = this.value;
151 if ( invnum == 'searching...' || invnum == 'Not found' || invnum == '' )
154 if ( this.getAttribute('magic') == 'nosearch' ) {
155 this.setAttribute('magic', '');
159 if ( document.getElementById('row'+searchrow).emptyrow ) {
160 newEmptyRow(searchrow);
162 var customer = document.getElementById('customer'+searchrow);
163 customer.value = 'searching...';
164 customer.disabled = true;
165 customer.style.color = '#000000';
166 customer.style.backgroundColor = '#dddddd';
168 var customer_select = document.getElementById('cust_select'+searchrow);
170 customer.style.display = '';
171 customer_select.style.display = 'none';
173 update_balance_text(searchrow, '');
174 update_status_text(searchrow, '');
175 update_status_color(searchrow, '#000000');
176 update_num_open(searchrow, 0);
178 function search_invnum_update(customers) {
180 var customerArray = eval('(' + customers + ')');
181 update_customer(searchrow, customerArray);
183 % if ( $opt{invnum_update_callback} ) {
184 <% $opt{invnum_update_callback} %>(searchrow)
189 invnum_search( invnum, search_invnum_update );
193 function search_custnum() {
195 this.style.color = '#000000'
197 var display_custnum_obj = this;
198 var searchrow = this.getAttribute('rownum');
199 var custnum_obj = document.getElementById('custnum'+searchrow);
200 var display_custnum = this.value;
202 if ( display_custnum == 'searching...' || display_custnum == 'Not found' || display_custnum == '' )
205 if ( this.getAttribute('magic') == 'nosearch' ) {
206 this.setAttribute('magic', '');
210 if ( document.getElementById('row'+searchrow).emptyrow ) {
211 newEmptyRow(searchrow);
214 var customer_obj = document.getElementById('customer'+searchrow);
215 customer_obj.value = 'searching...';
216 customer_obj.disabled = true;
217 customer_obj.style.color = '#000000';
218 customer_obj.style.backgroundColor = '#dddddd';
220 var customer_select = document.getElementById('cust_select'+searchrow);
222 customer_obj.style.display = '';
223 customer_select.style.display = 'none';
225 var invnum = document.getElementById('invnum'+searchrow);
228 update_balance_text(searchrow, '');
229 update_status_text( searchrow, '');
230 update_status_color(searchrow, '#000000');
231 update_num_open(searchrow, 0);
233 function search_custnum_update(customers) {
235 var customerArrayArray = eval('(' + customers + ')') || [];
237 if ( customerArrayArray.length == 0 ) {
239 update_customer(searchrow, []);
241 } else if ( customerArrayArray.length == 1 ) {
243 update_customer(searchrow, customerArrayArray[0]);
244 % if ( $opt{custnum_update_callback} ) {
245 <% $opt{custnum_update_callback} %>(searchrow)
250 custnum_obj.value = 'Multiple'; // or something
251 custnum_obj.style.color = '#ff0000';
253 //blank the current list
254 customer_select.options.length = 0;
256 opt(customer_select, '', 'Multiple customers match "' + custnum + '" - select one', '#ff0000');
257 //add the multiple customers
258 for ( var s = 0; s < customerArrayArray.length; s++ ) {
260 JSON.stringify(customerArrayArray[s]),
261 customerArrayArray[s][1],
265 opt(customer_select, 'cancel', '(Edit search string)', '#000000');
267 customer_obj.style.display = 'none';
269 customer_select.style.display = '';
275 custnum_search(display_custnum, search_custnum_update );
279 function search_customer() {
281 var customer_obj = this;
282 var searchrow = this.getAttribute('rownum');
283 var customer = this.value;
285 if ( customer == 'searching...' || customer == 'Not found' || customer == '' )
288 if ( this.getAttribute('magic') == 'nosearch' ) {
289 this.setAttribute('magic', '');
293 if ( document.getElementById('row'+searchrow).emptyrow ) {
294 newEmptyRow(searchrow);
297 var invnum = document.getElementById('invnum'+searchrow);
300 var custnum_obj = document.getElementById('display_custnum'+searchrow);
301 custnum_obj.value = 'searching...';
302 custnum_obj.disabled = true;
303 custnum_obj.style.color = '#000000';
304 custnum_obj.style.backgroundColor = '#dddddd';
306 var customer_select = document.getElementById('cust_select'+searchrow);
308 function search_customer_update(customers) {
310 var customerArrayArray = eval('(' + customers + ')') || [ [] ];
312 custnum_obj.disabled = false;
313 custnum_obj.style.backgroundColor = '#ffffff';
315 if ( customerArrayArray.length == 0 ) {
317 update_customer(searchrow, []);
319 } else if ( customerArrayArray.length == 1 ) {
321 update_customer(searchrow, customerArrayArray[0]);
322 % if ( $opt{custnum_update_callback} ) {
323 <% $opt{custnum_update_callback} %>(searchrow)
328 custnum_obj.value = 'Multiple'; // or something
329 custnum_obj.style.color = '#ff0000';
331 //blank the current list
332 customer_select.options.length = 0;
334 opt(customer_select, '', 'Multiple customers match "' + customer + '" - select one', '#ff0000');
335 //add the multiple customers
336 for ( var s = 0; s < customerArrayArray.length; s++ ) {
338 JSON.stringify(customerArrayArray[s]),
339 customerArrayArray[s][1],
343 opt(customer_select, 'cancel', '(Edit search string)', '#000000');
345 customer_obj.style.display = 'none';
347 customer_select.style.display = '';
353 smart_search( customer, search_customer_update );
357 function select_customer() {
359 var custnum_balance_status = this.options[this.selectedIndex].value;
360 var customer = this.options[this.selectedIndex].text;
362 var searchrow = this.getAttribute('rownum');
363 var display_custnum_obj = document.getElementById('display_custnum'+searchrow);
364 var custnum_obj = document.getElementById('custnum'+searchrow);
365 var customer_obj = document.getElementById('customer'+searchrow);
366 var balance_obj = document.getElementById('balance'+searchrow);
367 var status_obj = document.getElementById('status'+searchrow);
369 if ( custnum_balance_status == '' ) {
371 } else if ( custnum_balance_status == 'cancel' ) {
373 display_custnum_obj.value = '';
374 custnum_obj.value = '';
375 custnum_obj.style.color = '#000000';
377 this.style.display = 'none';
378 customer_obj.style.display = '';
379 customer_obj.focus();
383 update_customer(searchrow, JSON.parse(custnum_balance_status));
385 % if ( $opt{custnum_update_callback} ) {
386 <% $opt{custnum_update_callback} %>(searchrow)
393 function opt(what,value,text,color) {
394 var optionName = new Option(text, value, false, false);
395 optionName.style.color = color;
396 var length = what.length;
397 what.options[length] = optionName;
400 function update_status_text(rownum, newval) {
401 document.getElementById('status'+rownum).value = newval;
402 document.getElementById('status'+rownum+'_text').innerHTML = newval;
405 function update_status_color(rownum, newval) {
406 document.getElementById('statuscolor'+rownum).value = newval;
407 document.getElementById('status'+rownum+'_text').style.color = newval;
410 function update_balance_text(rownum, newval) {
411 document.getElementById('balance'+rownum).value = newval;
412 document.getElementById('balance'+rownum+'_text').innerHTML = newval;
415 function update_num_open(rownum, newval) {
416 document.getElementById('num_open'+rownum).value = newval;
417 num_open_invoices[rownum] = newval;
420 // updates display of total rows based on value of totalrows
421 function updateTotalRow () {
422 if ( totalrows == 1 ) {
426 + ' <% $opt{name_singular} || 'customer' %>';
431 + ' <% PL($opt{name_singular} || 'customer') %>';
435 var total_el, rownum, totalrows, allrows;
437 function addDeleteButton (searchrow) {
438 var td_delete = document.getElementById('delete'+searchrow);
439 var button_delete = document.createElement('INPUT');
440 button_delete.setAttribute('rownum', searchrow);
441 button_delete.setAttribute('type', 'button');
442 button_delete.setAttribute('value', 'X');
443 button_delete.onclick = deleteRow;
444 button_delete.style.color = '#ff0000';
445 button_delete.style.fontWeight = 'bold';
446 button_delete.style.paddingLeft = '2px';
447 button_delete.style.paddingRight = '2px';
448 td_delete.appendChild(button_delete);
451 function newEmptyRow (searchrow) {
452 // add delete button to current row
453 addDeleteButton(searchrow);
454 // mark current row as non-empty
455 var oldemptyrow = document.getElementById('row'+searchrow);
456 oldemptyrow.emptyrow = false;
460 // add a new empty row
464 function deleteRow() {
465 var thisrownum = this.getAttribute('rownum');
466 % if ( $opt{delete_row_callback} ) {
468 <% $opt{delete_row_callback} %>(thisrownum);
470 // remove the actual row
471 var thisrow = document.getElementById('row'+thisrownum);
472 thisrow.parentNode.removeChild(thisrow);
473 // remove row from tally of all rows
475 for (i = 0; i < allrows.length; i++) {
476 if (allrows[i] == thisrownum) continue;
477 newrows.push(allrows[i]);
480 totalrows--; // should never be deleting empty rows
482 // recalculate column totals, if any
484 % foreach my $footer ( @{$opt{footer}} ) {
485 % if ($footer eq '_TOTAL' ) {
486 calc_total<% $col %>()
492 function addRow(values) {
494 var table = document.getElementById('OneTrueTable');
495 var tablebody = table.getElementsByTagName('tbody').item(0);
497 var row = table.insertRow(table.rows.length - 1);
498 var thisrownum = values ? values.rownum : rownum;
499 row.setAttribute('id', 'row'+thisrownum);
500 row.emptyrow = values ? false : true;
502 var invnum_cell = document.createElement('TD');
504 var invnum_input = document.createElement('INPUT');
505 invnum_input.setAttribute('name', 'invnum'+thisrownum);
506 invnum_input.setAttribute('id', 'invnum'+thisrownum);
507 invnum_input.style.textAlign = 'right';
508 invnum_input.setAttribute('size', 8);
509 invnum_input.setAttribute('maxlength', 12);
510 invnum_input.setAttribute('rownum', thisrownum);
511 invnum_input.value = values ? values.invnum : '';
512 invnum_input.onfocus = clearhint_invnum;
513 invnum_input.onchange = search_invnum;
514 invnum_cell.appendChild(invnum_input);
516 row.appendChild(invnum_cell);
518 var custnum_cell = document.createElement('TD');
520 var display_custnum_input = document.createElement('INPUT');
521 display_custnum_input.setAttribute('name', 'display_custnum'+thisrownum);
522 display_custnum_input.setAttribute('id', 'display_custnum'+thisrownum);
523 display_custnum_input.style.textAlign = 'right';
524 display_custnum_input.setAttribute('size', 8);
525 display_custnum_input.setAttribute('maxlength', 12);
526 display_custnum_input.setAttribute('rownum', thisrownum);
527 display_custnum_input.value = values ? values.custnum : '';
528 display_custnum_input.onfocus = clearhint_custnum;
529 display_custnum_input.onchange = search_custnum;
530 custnum_cell.appendChild(display_custnum_input);
532 var custnum_input = document.createElement('INPUT');
533 custnum_input.type = 'hidden';
534 custnum_input.setAttribute('name', 'custnum'+thisrownum);
535 custnum_input.setAttribute('id', 'custnum'+thisrownum);
536 custnum_input.setAttribute('rownum', thisrownum);
537 custnum_input.value = values ? values.custnum : '';
538 custnum_cell.appendChild(custnum_input);
540 row.appendChild(custnum_cell);
542 var status_cell = document.createElement('TD');
543 status_cell.style.textAlign = 'center';
545 var status_span = document.createElement('SPAN');
546 status_span.setAttribute('id', 'status'+thisrownum+'_text');
547 status_span.style.fontWeight = 'bold';
548 status_span.style.color = values ? values.statuscolor : '';
549 status_span.setAttribute('rownum', thisrownum);
550 status_span.appendChild(
551 document.createTextNode(values ? values.status : '')
553 status_cell.appendChild(status_span);
555 var status_input = document.createElement('INPUT');
556 status_input.setAttribute('type', 'hidden');
557 status_input.setAttribute('name', 'status'+thisrownum);
558 status_input.setAttribute('id', 'status'+thisrownum);
559 status_input.setAttribute('rownum', thisrownum);
560 status_input.value = values ? values.status : '';
561 status_cell.appendChild(status_input);
563 var statuscolor_input = document.createElement('INPUT');
564 statuscolor_input.setAttribute('type', 'hidden');
565 statuscolor_input.setAttribute('name', 'statuscolor'+thisrownum);
566 statuscolor_input.setAttribute('id', 'statuscolor'+thisrownum);
567 statuscolor_input.setAttribute('rownum', thisrownum);
568 statuscolor_input.value = values ? values.statuscolor : '';
569 status_cell.appendChild(statuscolor_input);
571 row.appendChild(status_cell);
573 var customer_cell = document.createElement('TD');
575 var customer_input = document.createElement('INPUT');
576 customer_input.setAttribute('name', 'customer'+thisrownum);
577 customer_input.setAttribute('id', 'customer'+thisrownum);
578 customer_input.setAttribute('size', 64);
579 customer_input.setAttribute('value', '(last name or company)' );
580 customer_input.setAttribute('rownum', thisrownum);
581 customer_input.value = values ? values.customer : '';
582 customer_input.onfocus = clearhint_customer;
583 customer_input.onclick = clearhint_customer;
584 customer_input.onchange = search_customer;
585 customer_cell.appendChild(customer_input);
587 var customer_select = document.createElement('SELECT');
588 customer_select.setAttribute('name', 'cust_select'+thisrownum);
589 customer_select.setAttribute('id', 'cust_select'+thisrownum);
590 customer_select.setAttribute('rownum', thisrownum);
591 customer_select.style.color = '#ff0000';
592 customer_select.style.display = 'none';
593 customer_select.onchange = select_customer;
594 customer_cell.appendChild(customer_select);
596 row.appendChild(customer_cell);
598 var balance_cell = document.createElement('TD');
600 balance_cell.style.textAlign = 'right';
601 balance_cell.appendChild(document.createTextNode('<%$money_char%>'));
603 var balance_span = document.createElement('SPAN');
604 balance_span.setAttribute('id', 'balance'+thisrownum+'_text');
605 balance_span.setAttribute('rownum', thisrownum);
606 balance_cell.appendChild(balance_span);
608 balance_cell.appendChild(
609 document.createTextNode(String.fromCharCode(160) + (values ? values.balance : '')) //
612 var balance_input = document.createElement('INPUT');
613 balance_input.setAttribute('type', 'hidden');
614 balance_input.setAttribute('name', 'balance'+thisrownum);
615 balance_input.setAttribute('id', 'balance'+thisrownum);
616 balance_input.setAttribute('rownum', thisrownum);
617 balance_input.value = values ? values.balance : '';
618 balance_cell.appendChild(balance_input);
620 var num_open_input = document.createElement('INPUT');
621 num_open_input.setAttribute('type', 'hidden');
622 num_open_input.setAttribute('name', 'num_open'+thisrownum);
623 num_open_input.setAttribute('id', 'num_open'+thisrownum);
624 num_open_input.setAttribute('rownum', thisrownum);
625 balance_cell.appendChild(num_open_input);
627 row.appendChild(balance_cell);
630 % foreach my $field ( @{$opt{fields}} ) {
632 var my_cell = document.createElement('TD');
633 my_cell.setAttribute('align', '<% $align{ $opt{align}->[$col] || 'l' } %>');
634 % if ($opt{'color'}->[$col]) {
635 my_cell.style.color = '<% $opt{color}->[$col] %>';
638 % if ($types->[$col] eq 'immutable') {
639 var my_text = document.createTextNode(values ? values.<% $field %> : '');
640 my_cell.appendChild(my_text);
643 % my $name = (ref($field) eq 'CODE') ? "column${col}_" : $field;
644 var my_input = document.createElement('INPUT');
645 my_input.setAttribute('name', '<% $name %>'+thisrownum);
646 my_input.setAttribute('id', '<% $name %>'+thisrownum);
647 my_input.style.textAlign = '<% $align{ $opt{align}->[$col] || 'l' } %>';
648 my_input.setAttribute('size', <% $sizes->[$col] || 10 %>);
649 my_input.setAttribute('rownum', thisrownum);
650 % if ( $types->[$col] eq 'immutable' ) {
651 my_input.setAttribute('type', 'hidden');
652 % } elsif ( $types->[$col] eq 'checkbox' ) {
653 my_input.setAttribute('type', 'checkbox');
654 my_input.checked = (values && values.<% $field %>) ? true : false;
656 my_input.value = (values && values.<% $field %>) || '';
657 % if ( $opt{onchange}->[$col] ) {
658 my_input.onchange = <% $opt{onchange}->[$col] %>;
660 % elsif ( $opt{footer}->[$col] eq '_TOTAL' ) {
661 my_input.onchange = calc_total<%$col%>;
662 my_input.onkeyup = calc_total<%$col%>;
664 my_cell.appendChild(my_input);
666 row.appendChild(my_cell);
671 var td_delete = document.createElement('TD');
672 td_delete.setAttribute('id', 'delete'+thisrownum);
673 row.appendChild(td_delete);
675 addDeleteButton(thisrownum);
678 update_num_open(thisrownum, (values ? values.num_open : '0'));
680 % if ( $opt{add_row_callback} ) {
681 <% $opt{add_row_callback} %>(thisrownum, values);
684 // update the total number of rows display
685 allrows.push(thisrownum);
686 if (values) totalrows++;
689 // update the next available row number
690 if (thisrownum >= rownum) {
691 rownum = thisrownum + 1;
699 <TABLE ID="OneTrueTable" CLASS="fsinnerbox">
707 % foreach my $header ( @{$opt{header}} ) {
708 <TH><% $header %></TH>
712 % my @rownums = sort { $a <=> $b } map /^custnum(\d+)$/, keys %$param;
714 <TH COLSPAN=5 ID="_TOTAL_TOTAL">
715 Total <% @rownums || 0 %>
716 <% PL($opt{name_singular} || 'customer', ( @rownums || 0 ) ) %>
719 % foreach my $footer ( @{$opt{footer}} ) {
720 % my $align = $align{ $opt{'footer_align'}->[$col] || 'c' };
721 % if ($footer eq '_TOTAL' ) {
722 % my $id = $opt{'fields'}->[$col];
723 % $id = ref($id) ? "column${col}_TOTAL" : "${id}_TOTAL";
724 <TH ALIGN="<% $align %>" ID="<% $id %>"> <% sprintf('%.2f', $total[$col] ) %></TH>
726 <TH ALIGN="<% $align %>"><% $footer %></TH>
734 <SCRIPT TYPE="text/javascript">
737 document.getElementById("_TOTAL_TOTAL");
739 rownum = 1; // really more of a "next row", used by addrow
740 totalrows = 0; // will not include empty rows
741 allrows = []; // will include empty rows
743 % foreach my $row ( @rownums ) {
744 % if ( grep($param->{$_.$row},qw(invnum display_custnum custnum status statuscolor customer balance),@{$opt{fields}} ) ) {
748 num_open:<% $param->{"num_open$row"} |js_string %>,
749 invnum:<% $param->{"invnum$row"} |js_string %>,
750 display_custnum:<% $param->{"display_custnum$row"} |js_string %>,
751 custnum:<% $param->{"custnum$row"} |js_string %>,
752 status:<% $param->{"status$row"} |js_string %>,
753 statuscolor:<% $param->{"statuscolor$row"} |js_string %>,
754 customer:<% $param->{"customer$row"} |js_string %>,
755 balance:<% $param->{"balance$row"} |js_string %>,
757 % foreach my $field ( @{$opt{fields}} ) {
759 % if ( ref($field) eq 'CODE' ) {
760 % $value = &{$field}($row,$param) || '';
762 % $value = $param->{"$field$row"} || '';
764 % my $name = (ref($field) eq 'CODE') ? "column${col}" : "$field";
765 <% $name %>:<% $value |js_string %>,
775 % foreach my $footer ( @{$opt{footer}} ) {
776 % if ($footer eq '_TOTAL' ) {
777 % my $name = $opt{fields}->[$col];
778 % $name = ref($name) ? "column$col" : $name;
779 var th_el = document.getElementById("<%$name%>_TOTAL");
780 function calc_total<% $col %>() {
783 for (i = 0; i < allrows.length; i++) {
784 var value = document.getElementById("<%$name%>"+allrows[i]).value;
785 value = parseFloat(value);
786 if ( ! isNaN(value) ) {
787 total = total + value;
790 th_el.innerHTML = ' ' + total.toFixed(2);
792 calc_total<% $col %>()
798 <% include('/elements/xmlhttp.html',
799 'url' => $p. 'misc/xmlhttp-cust_main-search.cgi',
800 'subs' => [qw( custnum_search smart_search invnum_search )],
807 my $conf = new FS::Conf;
809 my $types = $opt{'type'} ? [ @{$opt{'type'}} ] : [];
810 my $sizes = $opt{'size'} ? [ @{$opt{'size'}} ] : [];
812 my $param = $opt{param};
813 $param = $cgi->Vars if $cgi->param('error');
815 $opt{$_} ||= [] foreach qw(align color footer footer_align);
817 my @total = map 0, @{$opt{footer}};
825 my $money_char = $conf->config('money_char') || '$';