summaryrefslogtreecommitdiff
path: root/httemplate/edit/elements
diff options
context:
space:
mode:
Diffstat (limited to 'httemplate/edit/elements')
-rw-r--r--httemplate/edit/elements/ApplicationCommon.html520
-rw-r--r--httemplate/edit/elements/category_Common.html24
-rw-r--r--httemplate/edit/elements/class_Common.html32
-rw-r--r--httemplate/edit/elements/edit.html816
-rw-r--r--httemplate/edit/elements/svc_Common.html144
5 files changed, 1536 insertions, 0 deletions
diff --git a/httemplate/edit/elements/ApplicationCommon.html b/httemplate/edit/elements/ApplicationCommon.html
new file mode 100644
index 000000000..08e1d46e6
--- /dev/null
+++ b/httemplate/edit/elements/ApplicationCommon.html
@@ -0,0 +1,520 @@
+<%doc>
+
+Examples:
+
+ #cust_bill_pay
+ include('elements/ApplicationCommon.html',
+ 'form_action' => 'process/cust_bill_pay.cgi',
+ 'src_table' => 'cust_pay',
+ 'src_thing' => 'payment',
+ 'dst_table' => 'cust_bill',
+ 'dst_thing' => 'invoice',
+ )
+
+ #cust_credit_bill
+ include('elements/ApplicationCommon.html',
+ 'form_action' => 'process/cust_credit_bill.cgi',
+ 'src_table' => 'cust_credit',
+ 'src_thing' => 'credit',
+ 'dst_table' => 'cust_bill',
+ 'dst_thing' => 'invoice',
+ )
+
+ #cust_pay_refund
+ include('elements/ApplicationCommon.html',
+ 'form_action' => 'process/cust_pay_refund.cgi',
+ 'src_table' => 'cust_pay',
+ 'src_thing' => 'payment',
+ 'dst_table' => 'cust_refund',
+ 'dst_thing' => 'refund',
+ )
+
+ #cust_credit_refund
+ include('elements/ApplicationCommon.html',
+ 'form_action' => 'process/cust_credit_refund.cgi',
+ 'src_table' => 'cust_credit',
+ 'src_thing' => 'credit',
+ 'dst_table' => 'cust_refund',
+ 'dst_thing' => 'refund',
+ )
+
+</%doc>
+
+<% include('/elements/header-popup.html', "Apply $src_thing$to", '', 'onLoad="myOnLoadFunction();"') %>
+
+<% include('/elements/error.html') %>
+
+<P ID="ErrorMessage"></P>
+<FORM ACTION="<% $p1. $opt{'form_action'} %>" NAME="ApplicationForm" ID="ApplicationForm" METHOD=POST>
+
+<% $src_thing %> #<B><% $src_pkeyvalue %></B><BR>
+<INPUT TYPE="hidden" NAME="<% $src_pkey %>" VALUE="<% $src_pkeyvalue %>">
+
+<TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
+
+<TR>
+ <TD ALIGN="right">Date: </TD>
+ <TD><B><% time2str("%D", $src->_date) %></B></TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right">Amount: </TD>
+ <TD ID="original_amount"><B><% $money_char %><% $src_amount %></B>
+ </TD>
+ <TD>
+% if ($use_sub_dst_thing && $can_change_credit) {
+ <INPUT TYPE="hidden" NAME="src_amount" VALUE="<% $src_amount %>" >
+ <BUTTON TYPE="button" NAME="expand_button" ID="expand_button" onClick="do_change_amount(this);">Change</BUTTON>
+% }
+ </TD>
+
+</TR>
+
+<TR>
+ <TD ALIGN="right">Unapplied amount: </TD>
+ <TD ID="unapplied_amount"><B><% $money_char %><% $unapplied %></B></TD>
+</TR>
+
+% if ( $src_table eq 'cust_credit' ) {
+ <TR>
+ <TD ALIGN="right">Reason: </TD>
+ <TD COLSPAN=2><B><% $src->reason %></B></TD>
+ </TR>
+% }
+
+</TABLE>
+<BR>
+
+<SCRIPT TYPE="text/javascript">
+function changed(what) {
+ dst = what.options[what.selectedIndex].value;
+
+ if ( dst == '' ) {
+ what.form.submit.disabled=true;
+%if ($src_pkey eq 'crednum') {
+ what.form.tax_button.disabled=true;
+%}
+ return true;
+ }
+
+ what.form.submit.disabled=false;
+%if ($src_pkey eq 'crednum') {
+ what.form.tax_button.disabled=false;
+%}
+
+% foreach my $dst ( @dst ) {
+
+ if ( dst == <% $dst->$dst_pkey %> ) {
+ what.form.amount.value = "<% min($dst->$dst_unapplied, $unapplied) %>";
+% if ($use_sub_dst_thing) {
+ what.form.display_amount.value = "<% min($dst->$dst_unapplied, $unapplied) %>";
+
+ var rownum=0
+ var table = document.getElementById('ApplicationTable');
+ while(table.rows[2]) {
+ table.deleteRow(2);
+ }
+% my $app_class = "FS::$link_table";
+% my $temp_app = $app_class->new(
+% { $src_pkey => $src_pkeyvalue,
+% $dst_pkey => $dst->$dst_pkey,
+% 'amount' => min($dst->$dst_unapplied, $unapplied),
+% }
+% );
+% my %apphash = ();
+% my $listref_or_error = $temp_app->calculate_applications;
+% %apphash = map { &{$key_generator}($_), $_ } @$listref_or_error
+% if ref($listref_or_error);
+% foreach my $cbp ( $dst->open_cust_bill_pkg ) {
+% my $desc = $cbp->desc;
+% my $total_owed = $cbp->owed_setup + $cbp->owed_recur;
+% my $key = &{$key_generator}([ $cbp, 0, {} ]);
+% my $amount = exists($apphash{ $key }) ? $apphash{ $key }->[1] : 0;
+% unless ( $cbp->pkgnum ) {
+% foreach my $taxX ( $cbp->cust_bill_pkg_tax_Xlocation ) {
+% my $pkey = $taxX->primary_key;
+% my $owed = $taxX->owed;
+% my $key = &{$key_generator}([ $cbp, 0, { $pkey => $taxX->$pkey } ]);
+% my $toapp = exists($apphash{ $key }) ? $apphash{ $key }->[1] : 0;
+ <% &{$row_generator}( $key, $cbp, $taxX->desc, $owed, $toapp, $taxX->$pkey ) %>
+% $total_owed -= $owed;
+% $amount -= $toapp;
+% }
+% $desc .= ' (default)';
+% }
+% if ( $total_owed > 0 ) {
+ <% &{$row_generator}($key, $cbp, $desc, $total_owed, $amount, '') %>
+% }
+% }
+% }
+ }
+
+% }
+
+}
+
+function sub_changed(what) {
+
+ var amount = 0;
+ var table = document.getElementById('ApplicationTable');
+ var i = table.rows.length;
+ while(i-- > 2) {
+ var inputs = table.rows[i].getElementsByTagName('input');
+ if (! inputs.length) {
+ continue;
+ }
+ amount += parseFloat( inputs.item(0).value ) || 0;
+ }
+ what.form.amount.value = parseFloat(amount).toFixed(2);
+ what.form.display_amount.value = parseFloat(amount).toFixed(2);
+ set_amount_color(what);
+
+}
+
+function set_amount_color(what) {
+ if (what.form.src_amount.value < what.form.amount.value) {
+ what.form.display_amount.style.color = '#ff0000';
+ } else {
+ what.form.display_amount.style.color = '#00ff00';
+ }
+}
+
+</SCRIPT>
+
+Apply to:
+
+% if ($use_sub_dst_thing && $src_pkey eq 'crednum') {
+<CENTER><BUTTON TYPE="button" NAME="tax_button" ID="tax_button" onClick="do_calculate_tax(this);" DISABLED>Calculate Tax</BUTTON></CENTER>
+<% include( '/elements/xmlhttp.html',
+ 'url' => $p.'misc/xmlhttp-calculate_taxes.html',
+ 'subs' => [ 'calculate_taxes' ],
+ )
+ %>
+<SCRIPT TYPE="text/javascript">
+
+function show_taxes(arg) {
+ var argsHash = eval('(' + arg + ')');
+
+ var button = document.getElementById('tax_button');
+ button.disabled = false;
+ button.innerHTML = 'Calculate Tax';
+
+ var error = argsHash['error'];
+
+ var paragraph = document.getElementById('ErrorMessage');
+ if (error) {
+ paragraph.innerHTML = 'Error: ' + error;
+ paragraph.style.color = '#ff0000';
+ } else {
+ paragraph.innerHTML = '';
+ }
+ var taxlines = argsHash['taxlines'];
+
+ var table = document.getElementById('ApplicationTable');
+
+ var aFoundRow = 0;
+ for (i = 0; taxlines[i]; i++) {
+ var itemdesc = taxlines[i][0];
+ var locnum = taxlines[i][2];
+ if (taxlines[i][3]) {
+ locnum = taxlines[i][3];
+ }
+
+ var found = 0;
+ for (var row = 2; table.rows[row]; row++) {
+ var inputs = table.rows[row].getElementsByTagName('input');
+ if (! inputs.length) {
+ while ( table.rows[row] ) {
+ table.deleteRow(row);
+ }
+ break;
+ }
+ if ( inputs.item(4).value == itemdesc && inputs.item(2).value == locnum )
+ {
+ inputs.item(0).value = taxlines[i][1];
+ aFoundRow = found = row;
+ break;
+ }
+ }
+ if (! found) {
+ var row = table.insertRow(table.rows.length);
+ var warning_cell = document.createElement('TD');
+ warning_cell.style.color = '#ff0000';
+ warning_cell.colSpan = 2;
+ warning_cell.innerHTML = 'Calculated Tax - ' + itemdesc + ' - ' +
+ taxlines[i][1] + ' will not be applied';
+ row.appendChild(warning_cell);
+ }
+ }
+
+ if (aFoundRow) {
+ sub_changed(table.rows[aFoundRow].getElementsByTagName('input').item(0));
+ }
+
+}
+
+function do_calculate_tax (what) {
+ what.innerHTML = 'Calculating....';
+ what.disabled = true;
+ var taxed_items = new Array();
+ var table = document.getElementById('ApplicationTable');
+ for (var row = 2; table.rows[row]; row++)
+ {
+ var inputs = table.rows[row].getElementsByTagName('input');
+ if ( !inputs.length ) {
+ break;
+ }
+ var taxed_item = new Array(
+ inputs.item(1).value, // billpkgnum
+ inputs.item(3).value, // s_or_r
+ inputs.item(0).value // amount
+ );
+ taxed_items.push(taxed_item);
+ }
+
+ var args = new Array(
+ 'crednum', '<% $src_pkeyvalue %>',
+ 'items', taxed_items
+ );
+ calculate_taxes ( args, show_taxes );
+}
+
+function do_change_amount (what) {
+ var amount_cell = document.getElementById('original_amount');
+ var inputs = amount_cell.getElementsByTagName('input');
+ if (inputs.length) {
+ src_amount_changed();
+ amount_cell.innerHTML = '<B><% $money_char %></B>' + inputs.item(0).value;
+ } else {
+ amount_cell.innerHTML = '<% $money_char %>';
+ var amount_input = document.createElement('INPUT');
+ amount_input.setAttribute('name', 'entered_amount');
+ amount_input.setAttribute('id', 'entered_amount');
+ amount_input.style.textAlign = 'right';
+ amount_input.setAttribute('size', 8);
+ amount_input.setAttribute('maxlength', 8);
+ amount_input.setAttribute('value', what.form.src_amount.value);
+ amount_input.setAttribute('onChange', "src_amount_changed(this);");
+ amount_cell.appendChild(amount_input);
+ }
+}
+
+function src_amount_changed () {
+ //alert('src_amount_changed called');
+ var entered_amount = document.getElementById('entered_amount');
+ if ( entered_amount ) {
+ entered_amount.form.src_amount.value = entered_amount.value;
+ var unapplied_cell = document.getElementById('unapplied_amount');
+ unapplied_cell.innerHTML = '<B><% $money_char %>' + entered_amount.value + '</B>';
+ set_amount_color(entered_amount);
+ }
+ return true;
+}
+
+</SCRIPT>
+
+%}
+
+<TABLE ID="ApplicationTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
+
+<TR>
+ <TD ALIGN="right"><% $dst_thing %>: </TD>
+ <TD><SELECT NAME="<% $dst_pkey %>" SIZE=1 onChange="changed(this)">
+<OPTION VALUE="">Select <% $dst_thing %>
+
+% foreach my $dst ( @dst ) {
+ <OPTION<% $dst->$dst_pkey eq $dst_pkeyvalue ? ' SELECTED' : '' %> VALUE="<% $dst->$dst_pkey %>">#<% $dst->$dst_pkey %> - <% time2str("%D", $dst->_date) %> - $<% $dst->$dst_unapplied %>
+% }
+
+</SELECT>
+ </TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right">Amount: </TD>
+ <TD><% $money_char %><INPUT TYPE="text" NAME="<% $use_sub_dst_thing ? 'display_' : '' %>amount" VALUE="<% $amount %>" SIZE=8 MAXLENGTH=8 <% $use_sub_dst_thing ? 'DISABLED' : '' %> STYLE="text-align:right;"></TD>
+% if ($use_sub_dst_thing) {
+ <INPUT TYPE="hidden" NAME="amount" VALUE="<% $amount %>" >
+% }
+</TR>
+
+</TABLE>
+
+<BR>
+<CENTER><INPUT TYPE="submit"
+ VALUE="Apply"
+ NAME="submit"
+ ID="submit"
+% if ($use_sub_dst_thing && $can_change_credit) {
+ onClick="src_amount_changed()"
+% }
+ DISABLED
+></CENTER>
+
+</FORM>
+
+<SCRIPT TYPE="text/javascript">
+
+function myOnLoadFunction () {
+ <% $onload %>
+}
+
+</SCRIPT>
+
+<% include('/elements/footer.html') %>
+
+<%init>
+
+my %opt = @_;
+
+my $conf = new FS::Conf;
+my $money_char = $conf->config('money_char') || '$';
+
+my $src_thing = ucfirst($opt{'src_thing'});
+my $src_table = $opt{'src_table'};
+my $src_pkey = dbdef->table($src_table)->primary_key;
+
+my $dst_thing = ucfirst($opt{'dst_thing'});
+my $dst_table = $opt{'dst_table'};
+my $dst_pkey = dbdef->table($dst_table)->primary_key;
+my $dst_unapplied = $dst_table eq 'cust_bill' ? 'owed' : 'unapplied';
+
+$opt{form_action} =~ /^process\/(.*)\./ or die "bad form action";
+my $link_table = $1;
+
+my $use_sub_dst_thing = 0;
+$use_sub_dst_thing = 1
+ if ( $dst_table eq 'cust_bill' && $conf->exists("${link_table}_pkg-manual") );
+
+my $can_change_credit = 0;
+$can_change_credit = 1
+ if ( $src_table eq 'cust_credit' &&
+ $FS::CurrentUser::CurrentUser->access_right('Post credit') &&
+ $FS::CurrentUser::CurrentUser->access_right('Delete credit')
+ );
+
+my $to = $dst_table eq 'cust_refund' ? ' to Refund' : '';
+
+my($src_pkeyvalue, $amount, $dst_pkeyvalue, $src_amount);
+if ( $cgi->param('error') ) {
+ $src_pkeyvalue = $cgi->param($src_pkey);
+ $amount = $cgi->param('amount');
+ $dst_pkeyvalue = $cgi->param($dst_pkey);
+ $src_amount = $cgi->param('src_amount');
+} else {
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/;
+ $src_pkeyvalue = $1;
+ $amount = '';
+ $dst_pkeyvalue = '';
+}
+
+my $otaker = getotaker;
+
+my $p1 = popurl(1);
+
+my $src = qsearchs($src_table, { $src_pkey => $src_pkeyvalue } );
+die "$src_thing $src_pkeyvalue not found!" unless $src;
+
+$src_amount = $src->amount unless $cgi->param('error');
+
+my $unapplied = $src->unapplied;
+
+my @dst = sort { $a->_date <=> $b->_date
+ or $a->$dst_pkey <=> $b->$dst_pkey
+ }
+ grep { $_->$dst_unapplied != 0 }
+ qsearch($dst_table, { 'custnum' => $src->custnum } );
+
+my $row_generator = sub {
+ my ($key, $cust_bill_pkg, $desc, $owed, $amount, $taxXnum) = @_;
+ my ($num, $s_or_r, $taxlinenum) = split(':', $key);
+ my $id = $cust_bill_pkg->pkgnum || 'Tax';
+ my $billpkgnum = $cust_bill_pkg->billpkgnum;
+ my $s_or_r = $cust_bill_pkg->setup > 0 ? 'setup' : 'recur';
+
+ $amount = sprintf("%.2f", $amount);
+ qq!
+ var tablebody = document.getElementsByTagName('tbody').item(0);
+ var row = table.insertRow(rownum+2);
+ var pkg_cell = document.createElement('TD');
+ pkg_cell.style.textAlign = 'right';
+ pkg_cell.innerHTML = "$id - $desc - $owed:";
+ var amount_cell = document.createElement('TD');
+ amount_cell.innerHTML = "$money_char";
+ var amount_input = document.createElement('INPUT');
+ amount_input.setAttribute('name', 'subamount'+rownum);
+ amount_input.setAttribute('id', 'subamount'+rownum);
+ amount_input.style.textAlign = 'right';
+ amount_input.setAttribute('size', 8);
+ amount_input.setAttribute('maxlength', 8);
+ amount_input.setAttribute('rownum', rownum);
+ amount_input.setAttribute('value', "$amount");
+ amount_input.setAttribute('onChange', "sub_changed(this);");
+ amount_cell.appendChild(amount_input);
+ var subnum_input = document.createElement('INPUT');
+ subnum_input.setAttribute('name', 'subnum'+rownum);
+ subnum_input.setAttribute('id', 'subnum'+rownum);
+ subnum_input.setAttribute('type', 'hidden');
+ subnum_input.setAttribute('rownum', rownum);
+ subnum_input.setAttribute('value', "$billpkgnum");
+ amount_cell.appendChild(subnum_input);
+ var taxnum_input = document.createElement('INPUT');
+ taxnum_input.setAttribute('name', 'taxXlocationnum'+rownum);
+ taxnum_input.setAttribute('id', 'taxXlocationnum'+rownum);
+ taxnum_input.setAttribute('type', 'hidden');
+ taxnum_input.setAttribute('rownum', rownum);
+ taxnum_input.setAttribute('value', "$taxXnum");
+ amount_cell.appendChild(taxnum_input);
+ var s_or_r_input = document.createElement('INPUT');
+ s_or_r_input.setAttribute('name', 's_or_r'+rownum);
+ s_or_r_input.setAttribute('id', 's_or_r'+rownum);
+ s_or_r_input.setAttribute('type', 'hidden');
+ s_or_r_input.setAttribute('rownum', rownum);
+ s_or_r_input.setAttribute('value', "$s_or_r");
+ amount_cell.appendChild(s_or_r_input);
+ var itemdesc_input = document.createElement('INPUT');
+ itemdesc_input.setAttribute('name', 'itemdesc'+rownum);
+ itemdesc_input.setAttribute('id', 'itemdesc'+rownum);
+ itemdesc_input.setAttribute('type', 'hidden');
+ itemdesc_input.setAttribute('rownum', rownum);
+ itemdesc_input.setAttribute('value', "$desc");
+ amount_cell.appendChild(itemdesc_input);
+ row.appendChild(pkg_cell);
+ row.appendChild(amount_cell);
+ rownum++;
+ !;
+};
+
+my $key_generator = sub {
+ my $listref = shift;
+ my ($cust_bill_pkg, $amount, $hashref) = @$listref;
+ my $setup_or_recur = $cust_bill_pkg->setup > 0 ? 'setup' : 'recur';
+ my $taxlinenum = $hashref->{billpkgtaxlocationnum} ||
+ $hashref->{billpkgtaxratelocationnum} ||
+ '';
+
+ join(':', $cust_bill_pkg->billpkgnum, $setup_or_recur, $taxlinenum);
+};
+
+my $onload = 'return true;';
+
+if ($cgi->param('error')) {
+
+ my $set_sub_amounts =
+ join(';', map { "myform.subamount$_.value = ". $cgi->param("subamount$_") }
+ grep { /.+/ }
+ map { /^subnum(\d+)$/ ? $1 : '' }
+ $cgi->param
+ );
+ $set_sub_amounts &&= "$set_sub_amounts;sub_changed(myform.subamount0)";
+
+ $onload = qq!
+ var myform = document.getElementById('ApplicationForm');
+ changed(myform.elements['$dst_pkey']);
+ $set_sub_amounts;
+ return true;
+ !;
+}
+
+</%init>
diff --git a/httemplate/edit/elements/category_Common.html b/httemplate/edit/elements/category_Common.html
new file mode 100644
index 000000000..8bbdcd15b
--- /dev/null
+++ b/httemplate/edit/elements/category_Common.html
@@ -0,0 +1,24 @@
+<% include( 'edit.html',
+ 'fields' => [
+ 'categoryname',
+ 'weight',
+ { field=>'disabled', type=>'checkbox', value=>'Y', },
+ ],
+ 'labels' => {
+ 'categorynum' => 'Category number',
+ 'categoryname' => 'Category name',
+ 'weight' => 'Weight',
+ 'disabled' => 'Disable category',
+ },
+ 'viewall_dir' => 'browse',
+ %opt,
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my %opt = @_;
+
+</%init>
diff --git a/httemplate/edit/elements/class_Common.html b/httemplate/edit/elements/class_Common.html
new file mode 100644
index 000000000..b5f493991
--- /dev/null
+++ b/httemplate/edit/elements/class_Common.html
@@ -0,0 +1,32 @@
+<% include( 'edit.html',
+ 'fields' => [
+ 'classname',
+ (scalar(@category)
+ ? { field=>'categorynum', type=>'select-table', 'empty_label'=>'(none)', 'table'=>'pkg_category', 'name_col'=>'categoryname' }
+ : { field=>'categorynum', type=>'hidden' }
+ ),
+ { field=>'disabled', type=>'checkbox', value=>'Y', },
+ ],
+ 'labels' => {
+ 'classnum' => 'Class number',
+ 'classname' => 'Class name',
+ 'categorynum' => 'Category',
+ 'disabled' => 'Disable class',
+ },
+ 'viewall_dir' => 'browse',
+ %opt,
+ )
+
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my %opt = @_;
+
+my $table = $opt{'table'};
+( my $category_table = $table ) =~ s/class/category/ or die;
+
+my @category = qsearch($category_table, { 'disabled' => '' });
+</%init>
diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html
new file mode 100644
index 000000000..fd73e031e
--- /dev/null
+++ b/httemplate/edit/elements/edit.html
@@ -0,0 +1,816 @@
+<%doc>
+
+Example:
+
+ include( 'elements/edit.html',
+ 'name_singular' => #singular name for the record
+ # (preferred, will be pluralized automatically)
+ 'name' => #name for the record
+ # (deprecated, will be pluralized simplistically)
+ 'table' =>
+
+ #? 'primary_key' => #required when the dbdef doesn't know...???
+ 'labels' => {
+ 'column' => 'Label',
+ }
+
+ #listref - each item is a literal column name (or method) or hashref
+ # or (notyet) coderef
+ #if not specified all columns (except for the primary key) will be editable
+ 'fields' => [
+ 'columname',
+ { 'field' => 'another_columname',
+ 'type' => 'text', #text
+ #password
+ #money
+ #percentage
+ #checkbox
+ #select
+ #selectlayers (can now use after a tablebreak-tr-title... but not inside columnstart/columnnext/columnend)
+ #title
+ #tablebreak-tr-title
+ #columnstart
+ #columnnext
+ #columnend
+ #hidden - hidden value from object
+ #fixed - display fixed value from object or here
+ #fixed-country
+ #fixed-state
+ 'value' => 'Y', #for checkbox, title, fixed, hidden
+ 'disabled' => 0,
+ 'onchange' => 'javascript_function',
+
+ #m2 stuff only tested w/selectlayers so far
+ #might work w/select too, dunno others
+ 'm2name_table' => 'table_name',
+ 'm2name_namecol' => 'name_column',
+ #OR#
+ 'm2m_method' =>
+ #'m2m_srccol' => #opt, if not the same as this table
+ 'm2m_dstcol' => #required for now, eventuaully opt, if not the same as target table
+
+ 'm2_label' => 'Label', #
+ 'm2_new_default' => \@table_name_objects, #default
+ #m2 objects for
+ #new records
+ 'm2_error_callback' => sub { my($cgi, $object) = @_; },
+ 'm2_remove_warnings' => \%warnings, #hashref of warning
+ #messages for m2
+ #removal
+ 'm2_new_js' => 'function_name', #javascript function called
+ #on spawned rows (one arg:
+ #new_element)
+ 'm2_remove_js' => 'function_name', #js function called when
+ #a row is deleted (three
+ #args: value, text,
+ #'no_match')
+ #layer_fields & layer_values_callback only for selectlayer
+ 'layer_fields' => [
+ 'fieldname' => 'Label',
+ 'another_field' => {
+ label=>'Label',
+ type =>'text', #text, money
+ },
+ ],
+ 'layer_values_callback' =>
+ sub {
+ my( $cgi, $object ) = @_;
+ { 'layer' => { 'fieldname' => 'current_value',
+ 'fieldname2' => 'field2value',
+ ...
+ },
+ 'layer2' => { 'l2fieldname' => 'l2value',
+ ...
+ },
+ ...
+ };
+ },
+ },
+ ]
+
+ 'menubar' => '', #menubar arrayref
+
+ #agent virtualization
+ 'agent_virt' => 1,
+ 'agent_null_right' => 'Access Right Name',
+ 'agent_clone_extra_sql' => '', #if provided, this overrides the extra_sql
+ #implementing agent virt, for clone
+ #operations. i.e. pass "1=1" to allow
+ #cloning anything
+
+ 'viewall_dir' => '', #'search' or 'browse', defaults to 'search'
+
+ # overrides default popurl(1)."process/$table.html"
+ 'post_url' => popurl(1).'process/something',
+
+ #we're in a popup (no title/menu/searchboxes)
+ 'popup' => 1,
+
+ ###
+ # HTML callbacks
+ ###
+
+ 'body_etc' => '', # Additional BODY attributes, i.e. onLoad=""
+
+ 'html_init' => '', #after the header/menubar
+
+ #string or coderef of additional HTML to add before </TABLE>
+ 'html_table_bottom' => '',
+
+ #after </TABLE> but before the submit
+ 'html_bottom' => '', #string
+ 'html_bottom' => sub {
+ my $object = shift;
+ # ...
+ "html_string";
+ },
+
+ #at the very bottom (well, as low as you can go from here)
+ 'html_foot' => '',
+
+ ###
+ # initialization callbacks
+ ###
+
+ ###global callbacks, always run if provided
+
+ #after decoding long CGI "redirect=" responses but
+ # before object creation/search
+ # (useful if you have a long form that might trigger redirect= and you need
+ # to do things with $cgi params - they're not decoded in the calling
+ # <%init> block yet)
+ 'begin_callback' = sub { my( $cgi, $fields_listref, $opt_hashref ) = @_; },
+
+ #after the mode-specific object creation/search
+ 'end_callback' = sub { my( $cgi, $object, $fields_listref, $opt_hashref ) = @_; },
+
+ ###mode-specific callbacks. one (and only one) of these four is called
+
+ #run when adding
+ 'new_callback' => sub { my( $cgi, $object, $fields_listref, $opt_hashref ) = @_; },
+
+ #run when editing
+ 'edit_callback' => sub { my( $cgi, $object, $fields_listref, $opt_hashref ) = @_; },
+
+ #run when re-displaying with an error
+ 'error_callback' => sub { my( $cgi, $object, $fields_listref, $opt_hashref ) = @_; },
+
+ #run when cloning
+ 'clone_callback' => sub { my( $cgi, $object, $fields_listref, $opt_hashref ) = @_; },
+
+ ###callbacks called in new mode only
+
+ # returns a hashref for the new object
+ 'new_hashref_callback'
+
+ # returns the new object iself (otherwise, ->new is called)
+ 'new_object_callback'
+
+ ###display callbacks
+
+ #run before display to return a different value
+ 'value_callback' => sub { my( $columname, $value ) = @_; },
+
+ #run before display to manipulate element of the 'fields' arrayref
+ 'field_callback' => sub { my( $cgi, $object, $field_hashref ) = @_; },
+
+ );
+
+</%doc>
+
+<% include('/elements/header'. ( $opt{popup} ? '-popup' : '' ). '.html',
+ $title,
+ include( '/elements/menubar.html', @menubar ),
+ $opt{'body_etc'},
+ )
+%>
+
+<% defined($opt{'html_init'})
+ ? ( ref($opt{'html_init'})
+ ? &{$opt{'html_init'}}()
+ : $opt{'html_init'}
+ )
+ : ''
+%>
+
+<% include('/elements/error.html') %>
+
+% my $url = $opt{'post_url'} || popurl(1)."process/$table.html";
+
+<FORM ACTION="<% $url %>" METHOD=POST NAME="edit_topform">
+
+<INPUT TYPE="hidden" NAME="svcdb" VALUE="<% $table %>">
+<INPUT TYPE="hidden" NAME="<% $pkey %>" VALUE="<% $clone ? '' : $object->$pkey() %>">
+
+<FONT SIZE="+1"><B>
+<% ( $opt{labels} && exists $opt{labels}->{$pkey} )
+ ? $opt{labels}->{$pkey}
+ : $pkey
+%>
+</B></FONT>
+#<% ( !$clone && $object->$pkey() ) || "(NEW)" %>
+
+% my $tablenum = 0;
+<TABLE ID="TableNumber<% $tablenum++ %>" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
+
+% my $g_row = 0;
+% my @g_row_stack = ();
+% foreach my $f ( map { ref($_) ? $_ : {'field'=>$_} }
+% @$fields
+% ) {
+%
+% my $trash = &{ $opt{'field_callback'} }( $cgi, $object, $f )
+% if $opt{'field_callback'};
+%
+% my $field = $f->{'field'};
+% my $type = $f->{'type'} ||= 'text';
+%
+% my $label = ( $opt{labels} && exists $opt{labels}->{$field} )
+% ? $opt{labels}->{$field}
+% : $field;
+%
+% my $onchange = $f->{'onchange'};
+%
+% my $layer_values = {};
+% $layer_values = &{ $f->{'layer_values_callback'} }( $cgi, $object )
+% if $f->{'layer_values_callback'}
+% && ! $f->{'m2name_table'}
+% && ! $f->{'m2m_method'};
+%
+% warn "layer values: ". Dumper($layer_values)
+% if $opt{'debug'};
+%
+% my %include_common = (
+%
+% #text and derivitives
+% 'size' => $f->{'size'},
+% 'maxlength' => $f->{'maxlength'},
+%
+% #checkbox, title, fixed, hidden
+% #& deprecated weird value hashref used only by reason.html
+% 'value' => $f->{'value'},
+%
+% #select(-*)
+% 'options' => $f->{'options'},
+% 'labels' => $f->{'labels'},
+% 'multiple' => $f->{'multiple'},
+% 'disable_empty' => $f->{'disable_empty'},
+% #select-reason
+% 'reason_class' => $f->{'reason_class'},
+%
+% #selectlayers
+% 'layer_fields' => $f->{'layer_fields'},
+% 'layer_values' => $layer_values,
+% 'html_between' => $f->{'html_between'},
+%
+% #umm. for select-agent_types at least
+% 'disabled' => $f->{'disabled'},
+% );
+%
+% #selectlayers, others?
+% $include_common{$_} = $f->{$_}
+% foreach grep exists($f->{$_}),
+% qw( js_only html_only select_only layers_only cell_style);
+%
+% #select-*
+% $include_common{$_} = $f->{$_}
+% foreach grep exists($f->{$_}), qw( empty_label );
+%
+% #select-table, checkboxes-table
+% $include_common{$_} = $f->{$_}
+% foreach grep exists($f->{$_}), qw( table name_col );
+%
+% #checkboxes-table
+% $include_common{$_} = $f->{$_}
+% foreach grep exists($f->{$_}), qw( target_table link_table );
+%
+% #*-table
+% $include_common{$_} = $f->{$_}
+% foreach grep exists($f->{$_}), qw( hashref agent_virt agent_null_right );
+%
+% if ( $type eq 'tablebreak-tr-title' ) {
+% $include_common{'table_id'} = 'TableNumber'. $tablenum++;
+% $include_common{'colspan'} = $f->{colspan} if $f->{colspan};
+% }
+%
+% my $layer_prefix_on = '';
+%
+% my $include_sub = sub {
+% my %opt = @_;
+%
+% my $fieldnum = delete $opt{'fieldnum'};
+%
+% my $include = $type;
+% $include = "input-$include" if $include =~ /^(text|money|percentage)$/;
+% $include = "tr-$include" unless $include =~ /^(hidden|tablebreak|column)/;
+%
+% $include_common{'layer_prefix'} = "$field$fieldnum."
+% if $layer_prefix_on;
+%
+% my @include =
+% ( "/elements/$include.html",
+% 'field' => "$field$fieldnum",
+% 'id' => "$field$fieldnum", #separate?
+% 'label_id' => $field."_label$fieldnum", #don't want field0_label0...
+% %include_common,
+% %opt,
+% );
+% @include;
+% };
+%
+% my $column_sub = sub {
+% my %opt = @_;
+%
+% my $column = delete($opt{field});
+% my $fieldnum = delete($opt{fieldnum});
+% my $include = delete($opt{type}) || 'text';
+% $include = "input-$include" if $include =~ /^(text|money|percentage)$/;
+%
+% ( "/elements/$include.html",
+% 'field' => $field.'__'.$column.$fieldnum,
+% 'id' => $field.'__'.$column.$fieldnum,
+% 'layer_prefix' => $field.'__'.$column.$fieldnum.".",
+% ( $fieldnum
+% ? ('cell_style' => 'border-top:1px solid black')
+% : ()
+% ),
+% 'cgi' => $cgi,
+% %opt,
+% );
+% };
+%
+% unless ( $type =~ /^column/ ) {
+% $g_row = 1 if $type eq 'tablebreak-tr-title';
+% $g_row++;
+% $g_row++ if $type eq 'title';
+% } else {
+% if ( $type eq 'columnstart' ) {
+% push @g_row_stack, $g_row;
+% $g_row = 0;
+% #} elsif ( $type eq 'columnnext' ) {
+% } elsif ( $type eq 'columnend' ) {
+% $g_row = pop @g_row_stack;
+% }
+%
+% }
+%
+% my $fieldnum = '';
+% my $curr_value = '';
+% if ( $f->{'m2name_table'} || $f->{'m2m_method'} ) { #XXX test this for all
+% #types of fields
+% my($table, $col);
+% if ( $f->{'m2name_table'} ) {
+% $table = $f->{'m2name_table'};
+% $col = $f->{'m2name_namecol'};
+% } elsif ( $f->{'m2m_method'} ) {
+% $table = $f->{'m2m_method'};
+% $col = $f->{'m2m_dstcol'};
+% }
+% $fieldnum = 0;
+% $layer_prefix_on = 1;
+% #print out the fields for the existing m2s
+% my @existing = ();
+% if ( $mode eq 'error' ) {
+% @existing = &{ $f->{'m2_error_callback'} }( $cgi, $object );
+% } elsif ( $object->$pkey() ) { # $mode eq 'edit'||'clone'
+% @existing = $object->$table();
+% warn scalar(@existing). " from $object->$table: ". join('/', @existing)
+% if $opt{'debug'};
+% } elsif ( $f->{'m2_new_default'} ) { # && $mode eq 'new'
+% @existing = @{ $f->{'m2_new_default'} };
+% }
+% foreach my $name_obj ( @existing ) {
+%
+% my $ex_label = '<INPUT TYPE="button" VALUE="X" TITLE="Remove this '.
+% lc($f->{'m2_label'}).
+% qq(" onClick="remove_$field($fieldnum);").
+% ' STYLE="color:#ff0000;font-weight:bold;'.
+% 'padding-left:2px;padding-right:2px"'.
+% '>&nbsp;'. ($f->{'m2_label'} || $field ). ' ';
+%
+% if ( $f->{'layer_values_callback'} ) {
+% my %switches = ( 'mode' => $mode );
+% $layer_values =
+% &{ $f->{'layer_values_callback'} }( $cgi, $name_obj, \%switches );
+% }
+% warn "layer values: ". Dumper($layer_values)
+% if $opt{'debug'};
+%
+% my @existing = &{ $include_sub }(
+% 'label' => $ex_label,
+% 'fieldnum' => $fieldnum,
+% 'curr_value' => $name_obj->$col(),
+% 'onchange' => $onchange,
+% 'layer_values' => $layer_values,
+% 'cell_style' => ( $fieldnum ? 'border-top:1px solid black' : '' ),
+% );
+% $existing[0] =~ s(^/elements/tr-)(/elements/);
+% my @label = @existing;
+% $label[0] = '/elements/tr-td-label.html';
+
+ <% include( @label ) %>
+ <TD>
+ <% include( @existing ) %>
+ </TD>
+
+% if ( $f->{'m2_fields'} ) {
+% foreach my $c ( @{ $f->{'m2_fields'} } ) {
+% my $column = $c->{field};
+% my @column = &{ $column_sub }( %$c,
+% 'fieldnum' => $fieldnum,
+% 'curr_value' => $name_obj->$column()
+% );
+
+ <TD id='<% $field %>__<% $column %>_label<% $fieldnum %>'
+ style='text-align:right;vertical-align:top;
+ border-top:1px solid black;padding-top:5px;'>
+ <% $c->{'label'} || '' %>
+ </TD>
+ <TD style='border-top:1px solid black;padding-top:3px;'>
+ <% include( @column ) %>
+ </TD>
+% }
+% }
+
+ </TR>
+
+% $fieldnum++;
+% $g_row++;
+% }
+% #$field .= $fieldnum;
+% $onchange .= "\nspawn_$field(what);";
+% } else {
+% if ( $f->{curr_value_callback} ) {
+% $curr_value = &{ $f->{curr_value_callback} }( $cgi, $object, $field ),
+% } else {
+% $curr_value = $object->$field();
+% }
+% $curr_value = &{ $opt{'value_callback'} }( $f->{'field'}, $curr_value )
+% if $opt{'value_callback'} && $mode ne 'error';
+% }
+%
+% my @include = &{ $include_sub }(
+% 'label' => $label,
+% 'fieldnum' => $fieldnum,
+% 'curr_value' => $curr_value,
+% 'object' => $object,
+% 'cgi' => $cgi,
+% 'onchange' => $onchange,
+% ( $fieldnum ? ('cell_style' => 'border-top:1px solid black') : () ),
+% );
+%
+% if ( $f->{'m2name_table'} || $f->{'m2m_method'} ) {
+% $include[0] =~ s(^/elements/tr-)(/elements/);
+% my @label = @include;
+% $label[0] = '/elements/tr-td-label.html';
+
+ <% include( @label ) %>
+ <TD>
+ <% include( @include ) %>
+ </TD>
+
+% if ( $f->{'m2_fields'} ) {
+% foreach my $c ( @{ $f->{'m2_fields'} } ) {
+% my $column = $c->{field};
+% my @column = &{ $column_sub }( %$c, 'fieldnum' => $fieldnum );
+
+ <TD id='<% $field %>__<% $column %>_label<% $fieldnum %>'
+ style='text-align:right;vertical-align:top;
+ border-top:1px solid black;padding-top:5px;'>
+ <% $c->{'label'} || '' %>
+ </TD>
+ <TD style='border-top:1px solid black;padding-top:3px;'>
+ <% include( @column ) %>
+ </TD>
+% }
+% }
+
+ </TR>
+
+% } else {
+
+ <% include( @include ) %>
+
+% }
+% if ( $f->{'m2name_table'} || $f->{'m2m_method'} ) {
+
+ <SCRIPT TYPE="text/javascript">
+
+ var <%$field%>_rownum = <% $g_row %>;
+ var <%$field%>_fieldnum = <% $fieldnum %>;
+
+ function spawn_<%$field%>(what) {
+
+ // only spawn if we're the last element... return if not
+
+ var field_regex = /(\d+)$/;
+ var match = field_regex.exec(what.name);
+ if ( !match ) {
+ alert(what.name + " didn't match?!");
+ return;
+ }
+ if ( match[1] != <%$field%>_fieldnum ) {
+ return;
+ }
+
+ // change the label on the last entry & add a remove button
+ var prev_label = document.getElementById('<% $field %>_label' + <%$field%>_fieldnum );
+ prev_label.innerHTML = '<INPUT TYPE="button" VALUE="X" TITLE="Remove this <% lc($f->{'m2_label'}) %>" onClick="remove_<% $field %>(' + <%$field%>_fieldnum + ');" STYLE="color:#ff0000;font-weight:bold;padding-left:2px;padding-right:2px" >&nbsp;<% $f->{'m2_label'} || $field %>';
+
+ <%$field%>_fieldnum++;
+
+ //get the new widget
+
+% $include[0] =~ s(^/elements/tr-)(/elements/);
+% my @layer_opt = ( @include,
+% 'field' => $field."MAGIC_NUMBER",
+% 'id' => $field."MAGIC_NUMBER",
+% 'layer_prefix' => $field."MAGIC_NUMBER.",
+% );
+% warn @layer_opt if $opt{'debug'};
+
+ var newrow = <% include(@layer_opt, html_only=>1) |js_string %>;
+
+% if ( $type eq 'selectlayers' ) { #until the rest have html/js_only
+ var newfunc = <% include(@layer_opt, js_only =>1) |js_string %>;
+% } else {
+ var newfunc = '';
+% }
+
+ // substitute in the new field name
+ var magic_regex = /MAGIC_NUMBER/g;
+ newrow = newrow.replace( magic_regex, <%$field%>_fieldnum );
+ newfunc = newfunc.replace( magic_regex, <%$field%>_fieldnum );
+
+ // evaluate new_func
+ if (window.ActiveXObject) {
+ window.execScript(newfunc);
+ } else { /* (window.XMLHttpRequest) */
+ //window.eval(newfunc);
+ setTimeout(newfunc, 0);
+ }
+
+ // add new row
+
+ //hmm, can't use selectlayers after a tablebreak-title for now
+ var table = document.getElementById('TableNumber<% $tablenum-1 %>');
+
+ var row = table.insertRow(<%$field%>_rownum++);
+
+ var label_cell = document.createElement('TD');
+
+ label_cell.id = '<% $field %>_label' + <%$field%>_fieldnum;
+
+ label_cell.style.textAlign = "right";
+ label_cell.style.verticalAlign = "top";
+ label_cell.style.borderTop = "1px solid black";
+ label_cell.style.paddingTop = "5px";
+
+ label_cell.innerHTML = '<% $label %>';
+
+ row.appendChild(label_cell);
+
+ var widget_cell = document.createElement('TD');
+
+ widget_cell.style.borderTop = "1px solid black";
+ widget_cell.style.paddingTop = "3px";
+
+ widget_cell.innerHTML = newrow;
+
+ row.appendChild(widget_cell);
+
+% if ( $f->{'m2_fields'} ) {
+% foreach my $c ( @{ $f->{'m2_fields'} } ) {
+% my $column = $c->{field};
+% my @column = &{ $column_sub }(%$c, 'fieldnum' => 'MAGIC_NUMBER');
+
+ var column = <% include(@column, html_only=>1) |js_string %>;
+ column = column.replace( magic_regex, <%$field%>_fieldnum );
+
+ var column_label = document.createElement('TD');
+ column_label.id =
+ '<% $field %>__<% $column %>_label' + <%$field%>_fieldnum;
+
+ column_label.style.textAlign = "right";
+ column_label.style.verticalAlign = "top";
+ column_label.style.borderTop = "1px solid black";
+ column_label.style.paddingTop = "5px";
+
+ column_label.innerHTML = '<% $c->{'label'} || '' %>';
+
+ row.appendChild(column_label);
+
+ var column_widget = document.createElement('TD');
+
+ column_widget.style.borderTop = "1px solid black";
+ column_widget.style.paddingTop = "3px";
+
+ column_widget.innerHTML = column;
+
+ row.appendChild(column_widget);
+
+% }
+% }
+
+% if ( $f->{'m2_new_js'} ) {
+ // take out items selected in previous dropdowns
+ var new_element = document.getElementById("<%$field%>" + <%$field%>_fieldnum );
+ <% $f->{'m2_new_js'} %>(new_element);
+
+ if ( new_element.length < 2 ) {
+ //just the ** Select new **, so don't display the row
+ row.style.display = 'none';
+ }
+% }
+
+ }
+
+ function remove_<%$field%>(remove_fieldnum) {
+ //alert("remove <%$field%> " + remove_fieldnum);
+ var select = document.getElementById('<%$field%>' + remove_fieldnum);
+
+ if ( ! select ) {
+ alert("can't find element <%$field%>" + remove_fieldnum);
+ return;
+ }
+
+% my $warnings = $f->{'m2_remove_warnings'};
+% if ( $warnings ) {
+ var sel_value = select.options[select.selectedIndex].value;
+% foreach my $value ( keys %$warnings ) {
+ if ( sel_value == '<% $value %>' ) {
+ if ( ! confirm( <% $warnings->{$value} |js_string %> ) ) {
+ return;
+ }
+ }
+% }
+% }
+
+ select.disabled = 'disabled'; // this seems to prevent it from being submitted on tested browsers so far (IE, moz, konq at least)
+ var label_td = document.getElementById('<%$field%>_label' + remove_fieldnum );
+ label_td.parentNode.style.display = 'none';
+
+% if ( $f->{m2_remove_js} ) {
+ var opt = select.options[select.selectedIndex];
+ <% $f->{m2_remove_js} %>( opt.value, opt.text, 'no_match');
+% }
+
+ }
+
+ </SCRIPT>
+
+% }
+
+% }
+
+<% ref( $opt{'html_table_bottom'} )
+ ? &{ $opt{'html_table_bottom'} }( $object )
+ : $opt{'html_table_bottom'}
+%>
+
+</TABLE>
+
+<% ref( $opt{'html_bottom'} )
+ ? &{ $opt{'html_bottom'} }( $object )
+ : $opt{'html_bottom'}
+%>
+
+<BR>
+
+<INPUT TYPE="submit" ID="submit" VALUE="<% ( !$clone && $object->$pkey() ) ? "Apply changes" : "Add ". ( $opt{'name'} || $opt{'name_singular'} ) %>">
+
+</FORM>
+
+<% ref( $opt{'html_foot'} )
+ ? &{ $opt{'html_foot'} }( $object )
+ : $opt{'html_foot'}
+%>
+
+<% include("/elements/footer.html") %>
+<%init>
+
+my(%opt) = @_;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+#false laziness w/process.html
+my $table = $opt{'table'};
+my $class = "FS::$table";
+my $pkey = dbdef->table($table)->primary_key; #? $opt{'primary_key'} ||
+my $fields = $opt{'fields'}
+ #|| [ grep { $_ ne $pkey } dbdef->table($table)->columns ];
+ || [ grep { $_ ne $pkey } fields($table) ];
+#my @actualfields = map { ref($_) ? $_->{'field'} : $_ } @$fields;
+
+if ( $cgi->param('redirect') ) {
+ my $session = $cgi->param('redirect');
+ my $pref = $curuser->option("redirect$session");
+ die "unknown redirect session $session\n" unless length($pref);
+ $cgi = new CGI($pref);
+}
+
+&{$opt{'begin_callback'}}( $cgi, $fields, \%opt )
+ if $opt{'begin_callback'};
+
+my %qsearch = (
+ 'table' => $table,
+ 'extra_sql' => ( $opt{'agent_virt'}
+ ? ' AND '. $curuser->agentnums_sql(
+ 'null_right' => $opt{'agent_null_right'}
+ )
+ : ''
+ ),
+);
+
+my $mode;
+my $object;
+my $clone = '';
+if ( $cgi->param('error') ) {
+
+ $mode = 'error';
+
+ $object = $class->new( {
+ map { $_ => scalar($cgi->param($_)) } fields($table)
+ });
+
+ &{$opt{'error_callback'}}( $cgi, $object, $fields, \%opt )
+ if $opt{'error_callback'};
+
+} elsif ( $cgi->param('clone') =~ /^(\d+)$/ ) {
+
+ $mode = 'clone';
+
+ $clone = $1;
+
+ $qsearch{'extra_sql'} = ' AND '. $opt{'agent_clone_extra_sql'}
+ if $opt{'agent_clone_extra_sql'};
+
+ $object = qsearchs({ %qsearch, 'hashref' => { $pkey => $clone } })
+ or die "$pkey $clone not found in $table";
+
+ &{$opt{'clone_callback'}}( $cgi, $object, $fields, \%opt )
+ if $opt{'clone_callback'};
+
+ #$object->$pkey('');
+
+ $opt{action} ||= 'Add';
+
+} elsif ( $cgi->keywords || $cgi->param($pkey) ) { #editing
+
+ $mode = 'edit';
+
+ my $value;
+ if ( $cgi->param($pkey) ) {
+ $value = $cgi->param($pkey)
+ } else {
+ my( $query ) = $cgi->keywords;
+ $value = $query;
+ }
+ $value =~ /^(\d+)$/ or die "unparsable $pkey";
+ $object = qsearchs({ %qsearch, 'hashref' => { $pkey => $1 } })
+ or die "$pkey $1 not found in $table";
+
+ warn "$table $pkey => $1"
+ if $opt{'debug'};
+
+ &{$opt{'edit_callback'}}( $cgi, $object, $fields, \%opt )
+ if $opt{'edit_callback'};
+
+} else { #adding
+
+ $mode = 'new';
+
+ my $hashref = $opt{'new_hashref_callback'}
+ ? &{$opt{'new_hashref_callback'}}
+ : {};
+
+ $object = $opt{'new_object_callback'}
+ ? &{$opt{'new_object_callback'}}( $cgi, $hashref, $fields, \%opt )
+ : $class->new( $hashref );
+
+ &{$opt{'new_callback'}}( $cgi, $object, $fields, \%opt )
+ if $opt{'new_callback'};
+
+}
+
+&{$opt{'end_callback'}}( $cgi, $object, $fields, \%opt )
+ if $opt{'end_callback'};
+
+$opt{action} ||= $object->$pkey() ? 'Edit' : 'Add';
+
+my $title = $opt{action}. ' '. ( $opt{name} || $opt{'name_singular'} );
+
+my $viewall_url = $p . ( $opt{'viewall_dir'} || 'search' ) . "/$table.html";
+$viewall_url = $opt{'viewall_url'} if $opt{'viewall_url'};
+
+my @menubar = ();
+if ( $opt{'menubar'} ) {
+ @menubar = @{ $opt{'menubar'} };
+} else {
+ my $items = $opt{'name'} ? $opt{'name'}.'s' : PL($opt{'name_singular'});
+ @menubar = (
+ "View all $items" => $viewall_url,
+ );
+}
+
+</%init>
diff --git a/httemplate/edit/elements/svc_Common.html b/httemplate/edit/elements/svc_Common.html
new file mode 100644
index 000000000..ef04bd04a
--- /dev/null
+++ b/httemplate/edit/elements/svc_Common.html
@@ -0,0 +1,144 @@
+<% include( 'edit.html',
+
+ 'menubar' => [],
+
+ 'error_callback' => sub {
+ my( $cgi, $svc_x, $fields, $opt ) = @_;
+ #$svcnum = $svc_x->svcnum;
+ $pkgnum = $cgi->param('pkgnum');
+ $svcpart = $cgi->param('svcpart');
+
+ $part_svc = qsearchs( 'part_svc', { svcpart=>$svcpart });
+ die "No part_svc entry!" unless $part_svc;
+
+ label_fixup($part_svc, $opt);
+
+ $svc_x->setfield('svcpart', $svcpart);
+ },
+
+ 'edit_callback' => sub {
+ my( $cgi, $svc_x, $fields, $opt ) = @_;
+ #$svcnum = $svc_x->svcnum;
+ my $cust_svc = $svc_x->cust_svc
+ or die "Unknown (cust_svc) svcnum!";
+
+ $pkgnum = $cust_svc->pkgnum;
+ $svcpart = $cust_svc->svcpart;
+
+ $part_svc = qsearchs ('part_svc', { svcpart=>$svcpart });
+ die "No part_svc entry!" unless $part_svc;
+
+ label_fixup($part_svc, $opt);
+ },
+
+ 'new_hashref_callback' => sub {
+ #my( $cgi, $svc_x ) = @_;
+
+ { svcpart => $svcpart };
+
+ },
+
+ 'new_callback' => sub {
+ my( $cgi, $svc_x, $fields, $opt ) = @_;;
+
+ $part_svc = qsearchs( 'part_svc', { svcpart=>$svcpart });
+ die "No part_svc entry!" unless $part_svc;
+
+ label_fixup($part_svc, $opt);
+
+ #$svcnum='';
+
+ $svc_x->set_default_and_fixed;
+
+ },
+
+ 'field_callback' => sub {
+ my ($cgi, $object, $f) = @_;
+ my $columndef = $part_svc->part_svc_column($f->{'field'});
+ my $flag = $columndef->columnflag;
+ if ( $flag eq 'F' ) {
+ $f->{'type'} = length($columndef->columnvalue)
+ ? 'fixed'
+ : 'hidden';
+ $f->{'value'} = $columndef->columnvalue;
+ }
+ },
+
+ 'html_init' => sub {
+ my $cust_main;
+ if ( $pkgnum ) {
+ my $cust_pkg = qsearchs('cust_pkg', {'pkgnum' => $pkgnum});
+ $cust_main = $cust_pkg->cust_main if $cust_pkg;
+ }
+ $cust_main
+ ? include( '/elements/small_custview.html',
+ $cust_main,
+ '',
+ 1,
+ popurl(2). "view/cust_main.cgi"
+ ). '<BR>'
+ : '';
+
+ },
+
+ 'html_table_bottom' => sub {
+ my $svc_x = shift;
+ my $html = '';
+ foreach my $field ($svc_x->virtual_fields) {
+ if ($part_svc->part_svc_column($field)->columnflag ne 'F'){
+ # If the flag is X, it won't even show up
+ # in $svc_acct->virtual_fields.
+ $html .=
+ $svc_x->pvf($field)->widget( 'HTML',
+ 'edit',
+ $svc_x->getfield($field)
+ );
+ }
+ }
+ $html;
+ },
+
+ 'html_bottom' => sub {
+ qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!.
+ qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">!;
+ },
+
+ %opt #pass through/override params
+ )
+%>
+<%once>
+
+sub label_fixup {
+ my( $part_svc, $opt ) = @_;
+
+ #false laziness w/view/svc_Common.html
+ #override default labels with service-definition labels if applicable
+ my $labels = $opt->{labels}; # with -> here
+ foreach my $field ( keys %$labels ) {
+ my $col = $part_svc->part_svc_column($field);
+ $labels->{$field} = $col->columnlabel if $col->columnlabel !~ /^\S*$/;
+ }
+
+}
+
+</%once>
+<%init>
+
+my %opt = @_;
+
+#my( $svcnum, $pkgnum, $svcpart, $part_svc );
+my( $pkgnum, $svcpart, $part_svc );
+
+#get & untaint pkgnum & svcpart
+if ( ! $cgi->param('error')
+ && $cgi->param('pkgnum') && $cgi->param('svcpart')
+ )
+{
+ $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+ $pkgnum = $1;
+ $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+ $svcpart = $1;
+ #$cgi->delete_all(); #so edit.html treats this correctly as new??
+}
+
+</%init>