new package editor
authorivan <ivan>
Tue, 15 Apr 2008 13:42:40 +0000 (13:42 +0000)
committerivan <ivan>
Tue, 15 Apr 2008 13:42:40 +0000 (13:42 +0000)
23 files changed:
FS/FS/Schema.pm
FS/FS/cust_event.pm
FS/FS/cust_main.pm
FS/FS/cust_pkg.pm
FS/FS/option_Common.pm
FS/FS/part_event_option.pm
FS/FS/part_pkg.pm
FS/FS/svc_Common.pm
FS/FS/svc_acct.pm
FS/FS/svc_domain.pm
httemplate/edit/elements/edit.html
httemplate/edit/part_event.html
httemplate/edit/part_pkg.cgi
httemplate/edit/process/elements/process.html
httemplate/edit/process/part_pkg.cgi
httemplate/elements/select-agent_types.html [new file with mode: 0644]
httemplate/elements/select-taxproduct.html
httemplate/elements/selectlayers.html
httemplate/elements/tr-input-text.html
httemplate/elements/tr-part_pkg_freq.html
httemplate/elements/tr-pkg_svc.html [new file with mode: 0644]
httemplate/elements/tr-select-agent_types.html [new file with mode: 0644]
httemplate/elements/tr-title.html

index d4a51a6..2666c53 100644 (file)
@@ -1007,6 +1007,18 @@ sub tables_hashref {
       'index' => [ [ 'promo_code' ], [ 'disabled' ], [ 'agentnum' ], ],
     },
 
+    'part_pkg_link' => {
+      'columns' => [
+        'pkglinknum',  'serial',  '',      '', '', '',
+        'src_pkgpart', 'int',     '',      '', '', '',
+        'dst_pkgpart', 'int',     '',      '', '', '', 
+        'link_type',   'varchar', '', $char_d, '', '',
+      ],
+      'primary_key' => 'pkglinknum',
+      'unique' => [ [ 'src_pkgpart', 'dst_pkgpart', 'link_type' ] ],
+      'index'  => [ [ 'src_pkgpart' ] ],
+    },
+
     'part_pkg_taxclass' => {
       'columns' => [
         'taxclassnum',  'serial', '',       '', '', '',
index 5924851..5ca8167 100644 (file)
@@ -344,6 +344,7 @@ sub process_re_X {
 
 }
 
+#this needs some updating based on the 1.7 cust_bill_event.pm still, i think
 sub re_X {
   my($method, $beginning, $ending, $failed, $job) = @_;
 
index ceefeaf..3490e46 100644 (file)
@@ -8,6 +8,7 @@ use vars qw( $realtime_bop_decline_quiet ); #ugh
 use Safe;
 use Carp;
 use Exporter;
+use Scalar::Util qw( blessed );
 use Time::Local qw(timelocal_nocheck);
 use Data::Dumper;
 use Tie::IxHash;
@@ -1052,7 +1053,7 @@ sub delete {
 
 }
 
-=item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
+=item replace [ OLD_RECORD ] [ INVOICING_LIST_ARYREF ]
 
 Replaces the OLD_RECORD with this one in the database.  If there is an error,
 returns the error, otherwise returns false.
@@ -1068,23 +1069,16 @@ check_invoicing_list first.  Here's an example:
 
 sub replace {
   my $self = shift;
-  my $old = shift;
+
+  my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+              ? shift
+              : $self->replace_old;
+
   my @param = @_;
+
   warn "$me replace called\n"
     if $DEBUG;
 
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-  local $SIG{PIPE} = 'IGNORE';
-
-  # We absolutely have to have an old vs. new record to make this work.
-  if (!defined($old)) {
-    $old = qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
-  }
-
   my $curuser = $FS::CurrentUser::CurrentUser;
   if (    $self->payby eq 'COMP'
        && $self->payby ne $old->payby
@@ -1099,6 +1093,13 @@ sub replace {
     && $self->payby =~ /^(CARD|DCRD)$/
     && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
 
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
   my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
index d413596..09808a4 100644 (file)
@@ -2,6 +2,7 @@ package FS::cust_pkg;
 
 use strict;
 use vars qw(@ISA $disable_agentcheck $DEBUG);
+use Scalar::Util qw( blessed );
 use List::Util qw(max);
 use Tie::IxHash;
 use FS::UID qw( getotaker dbh );
@@ -301,12 +302,17 @@ Calls
 =cut
 
 sub replace {
-  my( $new, $old, %options ) = @_;
+  my $new = shift;
+
+  my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+              ? shift
+              : $new->replace_old;
+
+  my $options = 
+    ( ref($_[0]) eq 'HASH' )
+      ? shift
+      : { @_ };
 
-  # We absolutely have to have an old vs. new record to make this work.
-  if (!defined($old)) {
-    $old = qsearchs( 'cust_pkg', { 'pkgnum' => $new->pkgnum } );
-  }
   #return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
   return "Can't change otaker!" if $old->otaker ne $new->otaker;
 
@@ -331,8 +337,8 @@ sub replace {
   my $dbh = dbh;
 
   foreach my $method ( qw(adjourn expire) ) {  # How many reasons?
-    if ($options{'reason'} && $new->$method && $old->$method ne $new->$method) {
-      my $error = $new->insert_reason( 'reason' => $options{'reason'},
+    if ($options->{'reason'} && $new->$method && $old->$method ne $new->$method) {
+      my $error = $new->insert_reason( 'reason' => $options->{'reason'},
                                        'date'   => $new->$method,
                                      );
       if ( $error ) {
@@ -357,7 +363,7 @@ sub replace {
   }
 
   my $error = $new->SUPER::replace($old,
-                                   $options{options} ? ${options{options}} : ()
+                                   $options->{options} ? $options->{options} : ()
                                   );
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
index 441e798..a786ae3 100644 (file)
@@ -161,8 +161,8 @@ sub delete {
 Replaces the OLD_RECORD with this one in the database.  If there is an error,
 returns the error, otherwise returns false.
 
-If a list hash reference of options is supplied, option records are created or
-modified.
+If a list or hash reference of options is supplied, option records are created
+or modified.
 
 =cut
 
index 43e1da9..09b7756 100644 (file)
@@ -2,6 +2,7 @@ package FS::part_event_option;
 
 use strict;
 use vars qw( @ISA );
+use Scalar::Util qw( blessed );
 use FS::UID qw( dbh );
 use FS::Record qw( qsearch qsearchs );
 use FS::part_event;
index 1e16f29..cffdc88 100644 (file)
@@ -3,6 +3,7 @@ package FS::part_pkg;
 use strict;
 use vars qw( @ISA %plans $DEBUG );
 use Carp qw(carp cluck confess);
+use Scalar::Util qw( blessed );
 use Tie::IxHash;
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs dbh dbdef );
@@ -16,11 +17,9 @@ use FS::pkg_class;
 use FS::agent;
 use FS::part_pkg_taxoverride;
 use FS::part_pkg_taxproduct;
+#XXX#use FS::part_pkg_link;
 
-@ISA = qw( FS::m2m_Common FS::Record ); # FS::option_Common ); # this can use option_Common
-                                                # when all the plandata bs is
-                                                # gone
-
+@ISA = qw( FS::m2m_Common FS::option_Common );
 $DEBUG = 0;
 
 =head1 NAME
@@ -168,61 +167,13 @@ sub insert {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  warn "  saving legacy plandata" if $DEBUG;
-  my $plandata = $self->get('plandata');
-  $self->set('plandata', '');
-
   warn "  inserting part_pkg record" if $DEBUG;
-  my $error = $self->SUPER::insert;
+  my $error = $self->SUPER::insert( $options{options} );
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   }
 
-  if ( $plandata ) {
-
-    warn "  inserting part_pkg_option records for plandata" if $DEBUG;
-    foreach my $part_pkg_option ( 
-      map { /^(\w+)=(.*)$/ or do { $dbh->rollback if $oldAutoCommit;
-                                   return "illegal plandata: $plandata";
-                                 };
-            new FS::part_pkg_option {
-              'pkgpart'     => $self->pkgpart,
-              'optionname'  => $1,
-              'optionvalue' => $2,
-            };
-          }
-      split("\n", $plandata)
-    ) {
-      my $error = $part_pkg_option->insert;
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return $error;
-      }
-    }
-
-  } elsif ( $options{'options'} ) {
-
-    warn "  inserting part_pkg_option records for options hashref" if $DEBUG;
-    foreach my $optionname ( keys %{$options{'options'}} ) {
-
-      my $part_pkg_option =
-        new FS::part_pkg_option {
-          'pkgpart'     => $self->pkgpart,
-          'optionname'  => $optionname,
-          'optionvalue' => $options{'options'}->{$optionname},
-        };
-
-      my $error = $part_pkg_option->insert;
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return $error;
-      }
-
-    }
-
-  }
-
   my $conf = new FS::Conf;
   if ( $conf->exists('agent_defaultpkg') ) {
     warn "  agent_defaultpkg set; allowing all agents to purchase package"
@@ -314,16 +265,19 @@ FS::pkg_svc record will be updated.
 =cut
 
 sub replace {
-  my( $new, $old ) = ( shift, shift );
-  my %options = @_;
+  my $new = shift;
 
-  # We absolutely have to have an old vs. new record to make this work.
-  if (!defined($old)) {
-    $old = qsearchs( 'part_pkg', { 'pkgpart' => $new->pkgpart } );
-  }
+  my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+              ? shift
+              : $new->replace_old;
 
-  warn "FS::part_pkg::replace called on $new to replace $old ".
-       "with options %options"
+  my $options = 
+    ( ref($_[0]) eq 'HASH' )
+      ? shift
+      : { @_ };
+
+  warn "FS::part_pkg::replace called on $new to replace $old with options".
+       join(', ', map "$_ => ". $options->{$_}, keys %$options)
     if $DEBUG;
 
   local $SIG{HUP} = 'IGNORE';
@@ -337,6 +291,8 @@ sub replace {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  #plandata shit stays in replace for upgrades until after 2.0 (or edit
+  #_upgrade_data)
   warn "  saving legacy plandata" if $DEBUG;
   my $plandata = $new->get('plandata');
   $new->set('plandata', '');
@@ -351,13 +307,13 @@ sub replace {
   }
 
   warn "  replacing part_pkg record" if $DEBUG;
-  my $error = $new->SUPER::replace($old);
+  my $error = $new->SUPER::replace($old, $options->{options} );
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   }
 
-  warn "  inserting part_pkg_option records for plandata" if $DEBUG;
+  warn "  inserting part_pkg_option records for plandata: $plandata|" if $DEBUG;
   foreach my $part_pkg_option ( 
     map { /^(\w+)=(.*)$/ or do { $dbh->rollback if $oldAutoCommit;
                                  return "illegal plandata: $plandata";
@@ -378,10 +334,10 @@ sub replace {
   }
 
   warn "  replacing pkg_svc records" if $DEBUG;
-  my $pkg_svc = $options{'pkg_svc'} || {};
+  my $pkg_svc = $options->{'pkg_svc'} || {};
   foreach my $part_svc ( qsearch('part_svc', {} ) ) {
     my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
-    my $primary_svc = $options{'primary_svc'} == $part_svc->svcpart ? 'Y' : '';
+    my $primary_svc = $options->{'primary_svc'} == $part_svc->svcpart ? 'Y' : '';
 
     my $old_pkg_svc = qsearchs('pkg_svc', {
       'pkgpart' => $old->pkgpart,
@@ -429,7 +385,8 @@ sub check {
 
   for (qw(setup recur plandata)) {
     #$self->set($_=>0) if $self->get($_) =~ /^\s*$/; }
-    return "Use of $_ field is deprecated; set a plan and options"
+    return "Use of $_ field is deprecated; set a plan and options: ".
+           $self->get($_)
       if length($self->get($_));
     $self->set($_, '');
   }
@@ -476,6 +433,22 @@ sub check {
   '';
 }
 
+=item pkg_comment
+
+Returns an (internal) string representing this package.  Currently,
+"pkgpart: pkg - comment", is returned.  "pkg - comment" may be returned in the
+future, omitting pkgpart.
+
+=cut
+
+sub pkg_comment {
+  my $self = shift;
+
+  #$self->pkg. ' - '. $self->comment;
+  #$self->pkg. ' ('. $self->comment. ')';
+  $self->pkgpart. ': '. $self->pkg. ' - '. $self->comment;
+}
+
 =item pkg_class
 
 Returns the package class, as an FS::pkg_class object, or the empty string
@@ -728,9 +701,20 @@ sub option {
   '';
 }
 
+=item dst_pkgpart
+
+=cut
+
+sub part_pkg_link {
+  ();
+  #XXX
+  #my $self = shift;
+  #qsearch('part_pkg_link', { 'src_pkgpart' => $self->pkgpart } );
+}
+
 =item part_pkg_taxoverride
 
-Returns all options as FS::part_pkg_taxoverride objects (see
+Returns all associated FS::part_pkg_taxoverride objects (see
 L<FS::part_pkg_taxoverride>).
 
 =cut
@@ -799,7 +783,7 @@ sub _rebless {
   my $self = shift;
   my $plan = $self->plan;
   unless ( $plan ) {
-    confess "no price plan found for pkgpart ". $self->pkgpart. "\n"
+    cluck "no price plan found for pkgpart ". $self->pkgpart. "\n"
       if $DEBUG;
     return $self;
   }
@@ -849,6 +833,74 @@ sub calc_cancel { 0; }
 
 =back
 
+=cut
+
+# _upgrade_data
+#
+# Used by FS::Upgrade to migrate to a new database.
+
+sub _upgrade_data { # class method
+  my($class, %opts) = @_;
+
+  warn "[FS::part_pkg] upgrading $class\n" if $DEBUG;
+
+  my @part_pkg = qsearch({
+    'table'     => 'part_pkg',
+    'extra_sql' => "WHERE ". join(' OR ',
+                     ( map "($_ IS NOT NULL AND $_ != '' )",
+                           qw( plandata setup recur ) ),
+                     'plan IS NULL', "plan = '' ",
+                   ),
+  });
+
+  foreach my $part_pkg (@part_pkg) {
+
+    unless ( $part_pkg->plan ) {
+
+      $part_pkg->plan('flat');
+
+      if ( $part_pkg->setup =~ /^\s*([\d\.]+)\s*$/ ) {
+
+        my $opt = new FS::part_pkg_option {
+          'pkgpart'     => $part_pkg->pkgpart,
+          'optionname'  => 'setup_fee',
+          'optionvalue' => $1,
+        };
+        my $error = $opt->insert;
+        die $error if $error;
+
+        $part_pkg->setup('');
+
+      } else {
+        die "Can't parse part_pkg.setup for fee; convert pkgnum ".
+            $part_pkg->pkgnum. " manually: ". $part_pkg->setup. "\n";
+      }
+
+      if ( $part_pkg->recur =~ /^\s*([\d\.]+)\s*$/ ) {
+
+        my $opt = new FS::part_pkg_option {
+          'pkgpart'     => $part_pkg->pkgpart,
+          'optionname'  => 'recur_fee',
+          'optionvalue' => $1,
+        };
+        my $error = $opt->insert;
+        die $error if $error;
+
+        $part_pkg->recur('');
+
+      } else {
+        die "Can't parse part_pkg.setup for fee; convert pkgnum ".
+            $part_pkg->pkgnum. " manually: ". $part_pkg->setup. "\n";
+      }
+
+    }
+
+    $part_pkg->replace; #this should take care of plandata, right?
+
+  }
+
+}
+
 =head1 SUBROUTINES
 
 =over 4
index d830f2f..31e53db 100644 (file)
@@ -3,6 +3,7 @@ package FS::svc_Common;
 use strict;
 use vars qw( @ISA $noexport_hack $DEBUG $me );
 use Carp qw( cluck carp croak ); #specify cluck have to specify them all..
+use Scalar::Util qw( blessed );
 use FS::Record qw( qsearch qsearchs fields dbh );
 use FS::cust_main_Mixin;
 use FS::cust_svc;
@@ -359,7 +360,7 @@ sub delete {
   '';
 }
 
-=item replace OLD_RECORD
+=item replace [ OLD_RECORD ] [ HASHREF | OPTION => VALUE ]
 
 Replaces OLD_RECORD with this one.  If there is an error, returns the error,
 otherwise returns false.
@@ -367,8 +368,16 @@ otherwise returns false.
 =cut
 
 sub replace {
-  my ($new, $old) = (shift, shift);
-  my %options = @_;
+  my $new = shift;
+
+  my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+              ? shift
+              : $new->replace_old;
+
+  my $options = 
+    ( ref($_[0]) eq 'HASH' )
+      ? shift
+      : { @_ };
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
@@ -381,9 +390,6 @@ sub replace {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  # We absolutely have to have an old vs. new record to make this work.
-  $old = $new->replace_old unless defined($old);
-
   my $error = $new->set_auto_inventory;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
@@ -399,7 +405,7 @@ sub replace {
   #new-style exports!
   unless ( $noexport_hack ) {
 
-    my $export_args = $options{'export_args'} || [];
+    my $export_args = $options->{'export_args'} || [];
 
     #not quite false laziness, but same pattern as FS::svc_acct::replace and
     #FS::part_export::sqlradius::_export_replace.  List::Compare or something
index 4343df5..3e3ecb5 100644 (file)
@@ -14,6 +14,7 @@ use vars qw( @ISA $DEBUG $me $conf $skip_fuzzyfiles
              $radius_password $radius_ip
              $dirhash
              @saltset @pw_set );
+use Scalar::Util qw( blessed );
 use Carp;
 use Fcntl qw(:flock);
 use Date::Format;
@@ -734,14 +735,15 @@ contain an arrayref of group names.  See L<FS::radius_usergroup>.
 =cut
 
 sub replace {
-  my ( $new, $old ) = ( shift, shift );
-  my $error;
+  my $new = shift;
+
+  my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+              ? shift
+              : $new->replace_old;
+
   warn "$me replacing $old with $new\n" if $DEBUG;
 
-  # We absolutely have to have an old vs. new record to make this work.
-  if (!defined($old)) {
-    $old = qsearchs( 'svc_acct', { 'svcnum' => $new->svcnum } );
-  }
+  my $error;
 
   return "can't modify system account" if $old->_check_system;
 
index 758b399..47aa8f3 100644 (file)
@@ -6,6 +6,7 @@ use vars qw( @ISA $whois_hack $conf
   $soarefresh $soaretry
 );
 use Carp;
+use Scalar::Util qw( blessed );
 use Date::Format;
 #use Net::Whois::Raw;
 use Net::Domain::TLD qw(tld_exists);
@@ -289,10 +290,11 @@ returns the error, otherwise returns false.
 =cut
 
 sub replace {
-  my ( $new, $old ) = ( shift, shift );
+  my $new = shift;
 
-  # We absolutely have to have an old vs. new record to make this work.
-  $old = $new->replace_old unless defined($old);
+  my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+              ? shift
+              : $new->replace_old;
 
   return "Can't change domain - reorder."
     if $old->getfield('domain') ne $new->getfield('domain'); 
index c80586a..150ed85 100644 (file)
@@ -22,9 +22,12 @@ Example:
                                        #percentage
                                        #checkbox
                                        #select
-                                       #selectlayers (can't use after a tablebreak-tr-title yet... grep "OneTrueTable")
+                                       #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
@@ -32,29 +35,32 @@ Example:
                     'value' => 'Y', #for checkbox, title, fixed, fixedhidden
                     'disabled' => 0,
                     'onchange' => 'javascript_function',
-                    'm2name_table'   => 'table_name',   #only tested w/
-                                                        # selectlayers so far
-                                                        # might work w/select
-                                                        # dunno others
-                    'm2name_namecol' => 'name_column',  # 
-                    'm2name_label'   => 'Label',        #
-                    'm2name_new_default' => \@table_name_objects, #default
-                                                                  #m2name
-                                                                  #objects for
-                                                                  #new records
-                    'm2name_error_callback' => sub { my($cgi, $object) = @_; },
-                    'm2name_remove_warnings' => \%warnings, #hashref of warning
-                                                            #messages for
-                                                            #m2name removal
-                    'm2name_new_js' => 'function_name', #javascript function
-                                                        #called on spawned rows
-                                                        #(one arg: new_element)
-                    'm2name_remove_js' => 'function_name', #js function called
-                                                           #when a row is
-                                                           #deleted
-                                                           #(three args:
-                                                           # value, text,
-                                                           #  'no_match')
+
+                    #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_table'        =>
+                    'm2m_target_table' =>
+                    'm2m_srccol'  => #opt, if not the same as this table
+                    'm2m_dstcol'  => #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',
@@ -93,6 +99,9 @@ Example:
    
     # returns a hashref for the new object
     'new_hashref_callback'
+
+    # returns the new object iself (otherwise, ->new is called)
+    'new_object_callback'
    
     #run when adding
     'new_callback' => sub { my( $cgi, $object, $fields_listref ) = @_; },
@@ -103,18 +112,24 @@ Example:
     #XXX describe
     'field_callback' => sub { },
 
+    'viewall_dir' => '', #'search' or 'browse', defaults to 'search'
+
+    'html_init'   => '', #after the header/menubar
+
     #string or coderef of additional HTML to add before </TABLE>
     'html_table_bottom' => '',
-   
-    'viewall_dir' => '', #'search' or 'browse', defaults to 'search'
-   
+
+    #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'  => '',
+
     # overrides default popurl(1)."process/$table.html"
     'post_url' => popurl(1).'process/something', 
 
@@ -131,6 +146,14 @@ Example:
            )
 %>
 
+<% 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";
@@ -148,10 +171,11 @@ Example:
 </B></FONT>
 #<% $object->$pkey() || "(NEW)" %>
 
-%# <% ntable("#cccccc",0) %>
-<TABLE ID="OneTrueTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
+% 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
 %                 ) {
@@ -169,9 +193,11 @@ Example:
 %   my $onchange = $f->{'onchange'};
 %
 %   my $layer_values = {};
-%   if ( $f->{'layer_values_callback'} && ! $f->{'m2name_table'} ) {
-%     $layer_values = &{ $f->{'layer_values_callback'} }( $cgi, $object );
-%   }
+%   $layer_values = &{ $f->{'layer_values_callback'} }( $cgi, $object )
+%     if $f->{'layer_values_callback'}
+%     && ! $f->{'m2name_table'}
+%     && ! $f->{'m2m_table'};
+%
 %   warn "layer values: ". Dumper($layer_values)
 %     if $opt{'debug'};
 %
@@ -196,8 +222,19 @@ Example:
 %     'layer_fields'  => $f->{'layer_fields'},
 %     'layer_values'  => $layer_values,
 %     'html_between'  => $f->{'html_between'},
+%
+%     #umm.
+%     'disabled'      => $f->{'disabled'},
 %   );
 %
+%   #select-table
+%   $include_common{$_} = $f->{$_}
+%     foreach grep exists($f->{$_}), qw( table name_col );
+%
+%   if ( $type eq 'tablebreak-tr-title' ) {
+%     $include_common{'table_id'} = 'TableNumber'. $tablenum++
+%   }
+%
 %   my $layer_prefix_on = '';
 %
 %   my $include_sub = sub {
@@ -207,7 +244,7 @@ Example:
 %
 %     my $include = $type;
 %     $include = "input-$include" if $include =~ /^(text|money|percentage)$/;
-%     $include = "tr-$include" unless $include =~ /^(hidden|tablebreak)/;
+%     $include = "tr-$include" unless $include =~ /^(hidden|tablebreak|column)/;
 %
 %     $include_common{'layer_prefix'} = "$field$fieldnum."
 %       if $layer_prefix_on;
@@ -223,33 +260,56 @@ Example:
 %     @include;
 %   };
 %
-%   $g_row++;
-%   $g_row++ if $type eq 'title';
+%   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'} ) { #XXX test this for all types of fields
-%     my $table = $f->{'m2name_table'};
-%     my $col   = $f->{'m2name_namecol'};
+%   if ( $f->{'m2name_table'} || $f->{'m2m_table'} ) { #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_table'} ) {
+%       $table = $f->{'m2m_table'}; #XXX method name
+%       $col   = $f->{'m2m_dstcol'}; #XXX or get it from schema, defualt to
+%                                    #same name as primary key of
+%                                    #m2m_target_table
+%     }
 %     $fieldnum = 0;
 %     $layer_prefix_on = 1;
-%     #print out the fields for the existing m2names
+%     #print out the fields for the existing m2s
 %     my @existing = ();
 %     if ( $mode eq 'error' ) {
-%       @existing = &{ $f->{'m2name_error_callback'} }( $cgi, $object );
+%       @existing = &{ $f->{'m2_error_callback'} }( $cgi, $object );
 %     } elsif ( $object->$pkey() ) { # $mode eq 'edit'
 %       @existing = $object->$table();
-%     } elsif ( $f->{'m2name_new_default'} ) { # && $mode eq 'new'
-%       @existing = @{ $f->{'m2name_new_default'} };
+%      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->{'m2name_label'}).
+%                      lc($f->{'m2_label'}).
 %                      qq(" onClick="remove_$field($fieldnum);").
 %                      ' STYLE="color:#ff0000;font-weight:bold;'.
 %                              'padding-left:2px;padding-right:2px"'.
-%                      '>&nbsp;'. ($f->{'m2name_label'} || $field ). ' ';
+%                      '>&nbsp;'. ($f->{'m2_label'} || $field ). ' ';
 %       
 %       if ( $f->{'layer_values_callback'} ) {
 %         my %switches = ( 'mode' => $mode );
@@ -276,10 +336,13 @@ Example:
 %     #$field .= $fieldnum;
 %     $onchange .= "\nspawn_$field(what);";
 %   } else {
-%     $curr_value =
-%       ($opt{'value_callback'} && $mode ne 'error')
-%         ? &{ $opt{'value_callback'} }( $f->{'field'}, $object->$field() )
-%         : $object->$field();
+%     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 }(
@@ -287,18 +350,19 @@ Example:
 %     'fieldnum'   => $fieldnum,
 %     'curr_value' => $curr_value,
 %     'object'     => $object,
+%     'cgi'        => $cgi,
 %     'onchange'   => $onchange,
 %     'cell_style'   => ( $fieldnum ? 'border-top:1px solid black' : '' ),
 %   );
 
     <% include( @include ) %>
 
-%   if ( $f->{'m2name_table'} ) {
+%   if ( $f->{'m2name_table'} || $f->{'m2m_table'} ) {
 
       <SCRIPT TYPE="text/javascript">
 
-        var rownum = <% $g_row %>;
-        var fieldnum = <% $fieldnum %>;
+        var <%$field%>_rownum = <% $g_row %>;
+        var <%$field%>_fieldnum = <% $fieldnum %>;
 
         function spawn_<%$field%>(what) {
 
@@ -310,23 +374,25 @@ Example:
             alert(what.name + " didn't match?!");
             return;
           }
-          if ( match[1] != fieldnum ) {
+          if ( match[1] != <%$field%>_fieldnum ) {
             return;
           }
 
           // change the label on the last entry & add a remove button
-          var prev_label = document.getElementById('<% $field %>_label' + fieldnum );
-          prev_label.innerHTML = '<INPUT TYPE="button" VALUE="X" TITLE="Remove this <% lc($f->{'m2name_label'}) %>" onClick="remove_<% $field %>(' + fieldnum + ');" STYLE="color:#ff0000;font-weight:bold;padding-left:2px;padding-right:2px" >&nbsp;<% $f->{'m2name_label'} || $field %>';
+          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 %>';
 
-          fieldnum++;
+          <%$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 %>;
 
@@ -338,8 +404,8 @@ Example:
 
           // substitute in the new field name
           var magic_regex = /MAGIC_NUMBER/g;
-          newrow  = newrow.replace(  magic_regex, fieldnum );
-          newfunc = newfunc.replace( magic_regex, fieldnum );
+          newrow  = newrow.replace(  magic_regex, <%$field%>_fieldnum );
+          newfunc = newfunc.replace( magic_regex, <%$field%>_fieldnum );
 
           // evaluate new_func
           if (window.ActiveXObject) {
@@ -352,13 +418,13 @@ Example:
           // add new row
 
           //hmm, can't use selectlayers after a tablebreak-title for now
-          var table = document.getElementById('OneTrueTable');
+          var table = document.getElementById('TableNumber<% $tablenum-1 %>');
 
-          var row = table.insertRow(rownum++);
+          var row = table.insertRow(<%$field%>_rownum++);
 
           var label_cell = document.createElement('TD');
 
-          label_cell.id = '<% $field %>_label' + fieldnum;
+          label_cell.id = '<% $field %>_label' + <%$field%>_fieldnum;
 
           label_cell.style.textAlign = "right";
           label_cell.style.verticalAlign = "top";
@@ -378,10 +444,10 @@ Example:
 
           row.appendChild(widget_cell);
 
-%         if ( $f->{'m2name_new_js'} ) {
+%         if ( $f->{'m2_new_js'} ) {
             // take out items selected in previous dropdowns
-            var new_element = document.getElementById("<%$field%>" + fieldnum );
-            <% $f->{'m2name_new_js'} %>(new_element);
+            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
@@ -395,7 +461,12 @@ Example:
           //alert("remove <%$field%> " + remove_fieldnum);
           var select = document.getElementById('<%$field%>' + remove_fieldnum);
 
-%         my $warnings = $f->{'m2name_remove_warnings'};
+          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 ) {
@@ -411,9 +482,9 @@ Example:
           var label_td = document.getElementById('<%$field%>_label' + remove_fieldnum );
           label_td.parentNode.style.display = 'none';
 
-%         if ( $f->{m2name_remove_js} ) {
+%         if ( $f->{m2_remove_js} ) {
             var opt = select.options[select.selectedIndex];
-            <% $f->{m2name_remove_js} %>( opt.value, opt.text, 'no_match');
+            <% $f->{m2_remove_js} %>( opt.value, opt.text, 'no_match');
 %         }
 
         }
@@ -442,6 +513,11 @@ Example:
 
 </FORM>
 
+<% ref( $opt{'html_foot'} )
+      ? &{ $opt{'html_foot'} }( $object )
+      : $opt{'html_foot'}
+%>
+
 <% include("/elements/footer.html") %>
 <%init>
 
@@ -471,7 +547,6 @@ if ( $cgi->param('error') ) {
 
   $mode = 'error';
 
-
   $object = $class->new( {
     map { $_ => scalar($cgi->param($_)) } fields($table)
   });
@@ -515,16 +590,18 @@ if ( $cgi->param('error') ) {
                   ? &{$opt{'new_hashref_callback'}}
                   : {};
 
-  $object = $class->new( $hashref );
+  $object = $opt{'new_object_callback'}
+              ? &{$opt{'new_object_callback'}}( $cgi, $hashref, $fields, \%opt )
+              : $class->new( $hashref );
 
   &{$opt{'new_callback'}}($cgi, $object, $fields)
     if $opt{'new_callback'};
 
 }
 
-my $action = $object->$pkey() ? 'Edit' : 'Add';
+$opt{action} ||= $object->$pkey() ? 'Edit' : 'Add';
 
-my $title = "$action $opt{'name'}";
+my $title = $opt{action}. ' '. $opt{name};
 
 my $viewall_url = $p . ( $opt{'viewall_dir'} || 'search' ) . "/$table.html";
 $viewall_url = $opt{'viewall_url'} if $opt{'viewall_url'};  
index 5b14edf..6a53222 100644 (file)
                               html_between       => n_a('action'),
                               m2name_table       => 'part_event_condition',
                               m2name_namecol     => 'conditionname',
-                              m2name_label       => 'Condition',
-                              m2name_new_default => \@implicit_condition_objs,
-                              m2name_error_callback  =>
-                                $condition_error_callback,
-                              m2name_remove_warnings =>
-                                \%condition_remove_warnings,
-                              m2name_new_js      => 'condition_repop',
-                              m2name_remove_js   => 'condition_add',
+                              m2_label           => 'Condition',
+                              m2_new_default     => \@implicit_condition_objs,
+                              m2_error_callback  => $condition_error_callback,
+                              m2_remove_warnings => \%condition_remove_warnings,
+                              m2_new_js          => 'condition_repop',
+                              m2_remove_js       => 'condition_add',
                             },
                             { type    => 'title',
                               value   => 'Event Action',
@@ -639,7 +637,7 @@ my $condition_error_callback = sub {
 };
 
 my $condition_layer_values = sub {
-  #m2name_table option causes this to be
+  #m2_table option causes this to be
   # part_event_condition instead of part_event
   my ( $cgi, $part_event_condition, $switches ) = @_;
   scalar( #force hashref
index c00af19..959fb30 100755 (executable)
-<% include('/elements/header.html', "$action Package Definition", menubar(
-  'View all packages' => popurl(2). 'browse/part_pkg.cgi',
-)) %>
-% #), ' onLoad="visualize()"'); 
-
-<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws.js"></SCRIPT>
-<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws_iframe.js"></SCRIPT>
-<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws_draggable.js"></SCRIPT>
-<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/iframecontentmws.js"></SCRIPT>
-
-<% include('/elements/error.html') %>
-
-<FORM NAME="dummy">
-
-<% itable('',8,1) %><TR><TD VALIGN="top">
-
-Package information
-
-<% ntable("#cccccc",2) %>
-  <TR>
-    <TD ALIGN="right">Package Definition #</TD>
-    <TD BGCOLOR="#ffffff">
-      <% $hashref->{pkgpart} ? $hashref->{pkgpart} : "(NEW)" %>
-    </TD>
-  </TR>
-  <TR>
-    <TD ALIGN="right">Package (customer-visible)</TD>
-    <TD>
-      <INPUT TYPE="text" NAME="pkg" SIZE=32 VALUE="<% $part_pkg->pkg %>">
-    </TD>
-  </TR>
-  <TR>
-    <TD ALIGN="right">Comment (customer-hidden)</TD>
-    <TD>
-      <INPUT TYPE="text" NAME="comment" SIZE=32 VALUE="<%$part_pkg->comment%>">
-    </TD>
-  </TR>
-  <% include( '/elements/tr-select-pkg_class.html',
-                'curr_value' => $part_pkg->classnum,
-            )
-  %>
-  <TR>
-    <TD ALIGN="right">Promotional code</TD>
-    <TD>
-      <INPUT TYPE="text" NAME="promo_code" SIZE=32 VALUE="<%$part_pkg->promo_code%>">
-    </TD>
-  </TR>
-  <TR>
-    <TD ALIGN="right">Disable new orders</TD>
-    <TD>
-      <INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<% $hashref->{disabled} eq 'Y' ? ' CHECKED' : '' %>
-    </TD>
-  </TR>
-
-</TABLE>
-
-</TD><TD VALIGN="top">
-
-Tax information
-<% ntable("#cccccc", 2) %>
-  <TR>
-    <TD ALIGN="right">Setup fee tax exempt</TD>
-    <TD>
-      <INPUT TYPE="checkbox" NAME="setuptax" VALUE="Y" <% $hashref->{setuptax} eq 'Y' ? ' CHECKED' : '' %>>
-    </TD>
-  </TR>
-  <TR>
-    <TD ALIGN="right">Recurring fee tax exempt</TD>
-    <TD>
-      <INPUT TYPE="checkbox" NAME="recurtax" VALUE="Y" <% $hashref->{recurtax} eq 'Y' ? ' CHECKED' : '' %>>
-    </TD>
-  </TR>
-
-% if ( $conf->exists('enable_taxclasses') ) { 
-
-  <TR>
-    <TD align="right">Tax class</TD>
-    <TD>
-      <% include('/elements/select-taxclass.html', $hashref->{taxclass} ) %>
-    </TD>
-  </TR>
-
-% } else { 
-
-  <% include('/elements/select-taxclass.html', $hashref->{taxclass} ) %>
-
-% } 
-
-% if ( $conf->exists('enable_taxproducts') ) { 
-
-  <TR><TD colspan="2">
-    <% ntable("#cccccc", 2) %>
-      <TR>
-        <TD align="right">Tax product</TD>
-        <TD>
-          <INPUT name="part_pkg_taxproduct_taxproductnum" id="taxproductnum" type="hidden" value="<% $hashref->{'taxproductnum'}%>">
-          <INPUT name="part_pkg_taxproduct_description" id="taxproductnum_description" type="text" value="<% $taxproduct_description %>" size="12" onclick="overlib( OLiframeContent('<% $p %>/browse/part_pkg_taxproduct.cgi?_type=select&id=taxproductnum&taxproductnum='+document.getElementById('taxproductnum').value, 1000, 400, 'tax_product_popup'), CAPTION, 'Select product', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK); return false;">
-        </TD>
-      </TR>
-      <TR>
-        <TD colspan="2" align="right">
-          <INPUT name="tax_override" id="tax_override" type="hidden" value="<% $tax_override %>">
-          <A href="javascript:void(0)" onclick="overlib( OLiframeContent('part_pkg_taxoverride.html?selected='+document.getElementById('tax_override').value, 1100, 600, 'tax_product_popup'), CAPTION, 'Edit product tax overrides', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK); return false;">
-            <% $tax_override ? 'Edit tax overrides' : 'Override taxes' %>
-          </A>
-        </TD>
-      </TR>
-    </TABLE>
-  </TD></TR>
-
-% } else { 
-
-  <INPUT TYPE="hidden" NAME="taxproductnum" VALUE="<% $hashref->{taxproductnum} %>">
-  <INPUT TYPE="hidden" NAME="tax_override" VALUE="<% $tax_override %>">
-
-% } 
-
-</TABLE>
-<BR>
-
-Line-item revenue recognition
-<% ntable("#cccccc", 2) %>
-% tie my %weight, 'Tie::IxHash',
-%   'pay_weight'    => 'Payment',
-%   'credit_weight' => 'Credit'
-% ;
-% foreach my $weight (keys %weight) {
-    <TR>
-      <TD ALIGN="right"><% $weight{$weight} %> weight</TD>
-      <TD>
-        <INPUT TYPE="text" NAME="<% $weight %>" SIZE=6 VALUE=<% $hashref->{$weight} || 0 %>>
-      </TD>
-    </TR>
-% }
-</TABLE>
-
-</TD><TD VALIGN="top">
-
-% if ( $cgi->param('clone') ) {
-
-    <INPUT TYPE="hidden" NAME="agent_type" VALUE="">
-
-% } elsif ( scalar(@all_agent_types) == 1) {
-
-    <INPUT TYPE="hidden" NAME="agent_type" VALUE="<% $all_agent_types[0] %>">
-
-% } else {
-
-    Reseller information
-    <% ntable("#cccccc", 2) %>
-      <TR>
-        <TD ALIGN="right"><% 'Agent Types' %></TD>
-        <TD>
-          <% include( '/elements/select-table.html',
-                      'element_name' => 'agent_type',
-                      'table'        => 'agent_type',
-                      'name_col'     => 'atype',
-                      'value'        => \@agent_type,
-                      'multiple'     =>  '1',
-                      'element_etc'  => 'size="10"',
-                    )
-          %>
-        </TD>
-      </TR>
-    </TABLE>
-
-% }
-
-</TD></TR></TABLE>
-
-%my $thead =  "\n\n". ntable('#cccccc', 2).
-%             '<TR><TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Quan.</FONT></TH>'.
-%             '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Primary</FONT></TH>'.
-%             '<TH BGCOLOR="#dcdcdc">Service</TH></TR>';
-
-<BR><BR>Services included
-<% itable('', 4, 1) %><TR><TD VALIGN="top">
-<% $thead %>
-%
-%
-%my $where =  "WHERE disabled IS NULL OR disabled = ''";
-%if ( $pkgpart ) {
-%  $where .=  "   OR 0 < ( SELECT quantity FROM pkg_svc
-%                           WHERE pkg_svc.svcpart = part_svc.svcpart
-%                             AND pkgpart = $pkgpart
-%                        )";
-%}
-%my @part_svc = qsearch('part_svc', {}, '', $where);
-%my $q_part_pkg = $clone_part_pkg || $part_pkg;
-%my %pkg_svc = map { $_->svcpart => $_ } $q_part_pkg->pkg_svc;
-%
-%my @fixups = ();
-%my $count = 0;
-%my $columns = 3;
-%foreach my $part_svc ( @part_svc ) {
-%  my $svcpart = $part_svc->svcpart;
-%  my $pkg_svc = $pkg_svc{$svcpart}
-%             || new FS::pkg_svc ( {
-%                                   'pkgpart'     => $pkgpart,
-%                                   'svcpart'     => $svcpart,
-%                                   'quantity'    => 0,
-%                                   'primary_svc' => '',
-%                                } );
-%  if ( $cgi->param('error') ) {
-%    my $primary_svc = ( $pkg_svc->primary_svc =~ /^Y/i );
-%    my $pkg_svc_primary = scalar($cgi->param('pkg_svc_primary'));
-%    $pkg_svc->primary_svc('')
-%      if $primary_svc && $pkg_svc_primary != $svcpart;
-%    $pkg_svc->primary_svc('Y')
-%      if ! $primary_svc && $pkg_svc_primary == $svcpart;
-%  }
-%
-%  push @fixups, "pkg_svc$svcpart";
-%
-%  my $quan = 0;
-%  if ( $cgi->param("pkg_svc$svcpart") =~ /^\s*(\d+)\s*$/ ) {
-%    $quan = $1;
-%  } elsif ( $pkg_svc->quantity ) {
-%    $quan = $pkg_svc->quantity;
-%  }
-
-
-  <TR>
-    <TD>
-      <INPUT TYPE="text" NAME="pkg_svc<% $svcpart %>" SIZE=4 MAXLENGTH=3 VALUE="<% $quan %>">
-    </TD>
-   
-    <TD>
-      <INPUT TYPE="radio" NAME="pkg_svc_primary" VALUE="<% $svcpart %>" <% $pkg_svc->primary_svc =~ /^Y/i ? ' CHECKED' : '' %>>
-    </TD>
-
-    <TD>
-      <A HREF="part_svc.cgi?<% $part_svc->svcpart %>"><% $part_svc->svc %></A>      <% $part_svc->disabled =~ /^Y/i ? ' (DISABLED' : '' %>
-    </TD>
-  </TR>
-% foreach ( 1 .. $columns-1 ) {
-%       if ( $count == int( $_ * scalar(@part_svc) / $columns ) ) { 
-%  
-
-         </TABLE></TD><TD VALIGN="top"><% $thead %>
-%   }
-%     }
-%     $count++;
-%  
-% } 
-
-
-</TR></TABLE></TD></TR></TABLE>
-% foreach my $f ( qw( clone pkgnum ) ) { #safe, these were untained in %init 
-    <INPUT TYPE="hidden" NAME="<% $f %>" VALUE="<% $cgi->param($f) %>">
-% }
-
-<INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $part_pkg->pkgpart %>">
-%
-%
-%# prolly should be in database
-%tie my %plans, 'Tie::IxHash', %{ FS::part_pkg::plan_info() };
-%
-%my %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); }
-%                    split("\n", ($clone_part_pkg||$part_pkg)->plandata );
-%#warn join("\n", map { "$_: $plandata{$_}" } keys %plandata ). "\n";
-%
-%tie my %options, 'Tie::IxHash', map { $_=>$plans{$_}->{'name'} } keys %plans;
-%
-%#my @form_select = ('classnum');
-%#if ( $conf->exists('enable_taxclasses') ) {
-%#  push @form_select, 'taxclass';
-%#} else {
-%#  push @fixups, 'taxclass'; #hidden
-%#}
-%my @form_elements = ( 'classnum', 'taxclass', 'agent_type', 'tax_override' );
-%
-%my @form_radio = ( 'pkg_svc_primary' );
-%
-%tie my %freq, 'Tie::IxHash', %{FS::part_pkg->freqs_href()};
-%if ( $part_pkg->dbdef_table->column('freq')->type =~ /(int)/i ) {
-%  delete $freq{$_} foreach grep { ! /^\d+$/ } keys %freq;
-%}
-%
-%#this should be replaced by /elements/selectlayers.html
-%my $widget = new HTML::Widgets::SelectLayers(
-%  'selected_layer' => $part_pkg->plan,
-%  'options'        => \%options,
-%  'form_name'      => 'dummy',
-%  'form_action'    => 'process/part_pkg.cgi',
-%  'form_elements'  => \@form_elements,
-%  'form_text'      => [ qw(pkg comment promo_code clone pkgnum pkgpart),
-%                        qw(pay_weight credit_weight), #keys(%weight),
-%                        qw(taxproductnum),
-%                        @fixups,
-%                      ],
-%  'form_checkbox'  => [ qw(setuptax recurtax disabled) ],
-%  'form_radio'     => \@form_radio,
-%  'layer_callback' => sub {
-%    my $layer = shift;
-%    my $html = qq!<INPUT TYPE="hidden" NAME="plan" VALUE="$layer">!.
-%               ntable("#cccccc",2);
-%    $html .= '
-%      <TR>
-%        <TD ALIGN="right">Recurring fee frequency </TD>
-%        <TD><SELECT NAME="freq">
-%    ';
-%
-%    my @freq = keys %freq;
-%    @freq = grep { /^\d+$/ } @freq
-%      if exists($plans{$layer}->{'freq'}) && $plans{$layer}->{'freq'} eq 'm';
-%    foreach my $freq ( @freq ) {
-%      $html .= qq(<OPTION VALUE="$freq");
-%      $html .= ' SELECTED' if $freq eq $part_pkg->freq;
-%      $html .= ">$freq{$freq}";
-%    }
-%    $html .= '</SELECT></TD></TR>';
-%
-%    my $href = $plans{$layer}->{'fields'};
-%    foreach my $field ( exists($plans{$layer}->{'fieldorder'})
-%                          ? @{$plans{$layer}->{'fieldorder'}}
-%                          : keys %{ $href }
-%                      ) {
-%
-%      $html .= '<TR><TD ALIGN="right">'. $href->{$field}{'name'}. '</TD><TD>';
-%
-%      my $format = sub { shift };
-%      $format = $href->{$field}{'format'} if exists($href->{$field}{'format'});
-%
-%      if ( ! exists($href->{$field}{'type'}) ) {
-%
-%        $html .= qq!<INPUT TYPE="text" NAME="$field" VALUE="!.
-%                 ( exists($plandata{$field})
-%                     ? &$format($plandata{$field})
-%                     : $href->{$field}{'default'} ).
-%                 qq!" onChange="fchanged(this)">!;
-%
-%      } elsif ( $href->{$field}{'type'} eq 'checkbox' ) {
-%
-%        $html .= qq!<INPUT TYPE="checkbox" NAME="$field" VALUE=1 !.
-%                 ( exists($plandata{$field}) && $plandata{$field}
-%                   ? ' CHECKED'
-%                   : ''
-%                 ). '>';
-%
-%      } elsif ( $href->{$field}{'type'} =~ /^select/ ) {
-%
-%        $html .= '<SELECT';
-%        $html .= ' MULTIPLE'
-%          if $href->{$field}{'type'} eq 'select_multiple';
-%        $html .= qq! NAME="$field" onChange="fchanged(this)">!;
-%
-%        if ( $href->{$field}{'select_table'} ) {
-%          foreach my $record (
-%            qsearch( $href->{$field}{'select_table'},
-%                     $href->{$field}{'select_hash'}   )
-%          ) {
-%            my $value = $record->getfield($href->{$field}{'select_key'});
-%            $html .= qq!<OPTION VALUE="$value"!.
-%                     (  $plandata{$field} =~ /(^|, *)$value *(,|$)/
-%                          ? ' SELECTED'
-%                          : ''
-%                     ).
-%                     '>'. $record->getfield($href->{$field}{'select_label'});
-%          }
-%        } elsif ( $href->{$field}{'select_options'} ) {
-%          foreach my $key ( keys %{ $href->{$field}{'select_options'} } ) {
-%            my $label = $href->{$field}{'select_options'}{$key};
-%            $html .= qq!<OPTION VALUE="$key"!.
-%                     ( $plandata{$field} =~ /(^|, *)$key *(,|$)/ #XXX fix
-%                         ? ' SELECTED'
-%                         : ''
-%                     ).
-%                     '>'. $label;
-%          }
-%
-%        } else {
-%          $html .= '<font color="#ff0000">warning: '.
-%                   "don't know how to retreive options for $field select field".
-%                   '</font>';
-%        }
-%        $html .= '</SELECT>';
-%
-%      } elsif ( $href->{$field}{'type'} eq 'radio' ) {
-%
-%        my $radio =
-%          qq!<INPUT TYPE="radio" NAME="$field" onChange="fchanged(this)"!;
-%
-%        foreach my $key ( keys %{ $href->{$field}{'options'} } ) {
-%          my $label = $href->{$field}{'options'}{$key};
-%          $html .= qq!$radio VALUE="$key"!.
-%                   ( $plandata{$field} =~ /(^|, *)$key *(,|$)/ #XXX fix
-%                       ? ' CHECKED'
-%                       : ''
-%                   ).
-%                   "> $label<BR>";
-%        }
-%
-%      }
-%
-%      $html .= '</TD></TR>';
-%    }
-%    $html .= '</TABLE>';
-%
-%    $html .= '<INPUT TYPE="hidden" NAME="plandata" VALUE="'.
-%             join(',', keys %{ $href } ). '">'.
-%             '<BR><BR>';
-%             
-%    $html .= '<INPUT TYPE="submit" VALUE="'.
-%               ( $action eq 'Custom'
-%                   ? 'Customize package'
-%                   : ( $hashref->{pkgpart} ? "Apply changes" : "Add package" )
-%               ).
-%             '" onClick="fchanged(this)">';
-%
-%    $html;
-%
-%  },
-%);
-%
-%
-
-
-<BR><BR>Price plan <% $widget->html %>
-
-<% include('/elements/footer.html') %>
+<% include( 'elements/edit.html',
+              'post_url'    => popurl(1).'process/part_pkg.cgi',
+              'name'        => "Package definition",
+              'table'       => 'part_pkg',
+              #'viewall_dir' => 'browse',
+              'viewall_url' => $p.'browse/part_pkg.cgi',
+              'html_init'   => include('/elements/init_overlib.html').
+                               $freq_changed,
+              'html_bottom' => $html_bottom,
+              'new_hashref_callback' => $new_hashref_callback,
+              'new_object_callback'  => $new_object_callback,
+              'new_callback'         => $new_callback,
+              'edit_callback'        => $edit_callback,
+              'error_callback'       => $error_callback,
+
+              'labels' => { 
+                            'pkgpart'          => 'Package Definition',
+                            'pkg'              => 'Package (customer-visible)',
+                            'comment'          => 'Comment (customer-hidden)',
+                            'classnum'         => 'Package class',
+                            'promo_code'       => 'Promotional code',
+                            'freq'             => 'Recurring fee frequency',
+                            'setuptax'         => 'Setup fee tax exempt',
+                            'recurtax'         => 'Recurring fee tax exempt',
+                            'taxclass'         => 'Tax class',
+                            'plan'             => 'Price plan',
+                            'disabled'         => 'Disable new orders',
+                            'pay_weight'       => 'Payment weight',
+                            'credit_weight'    => 'Credit weight',
+                            'agentnum'         => '',
+                            'setup_fee'        => 'Setup fee',
+                            'recur_fee'        => 'Recurring fee',
+                            'bill_dst_pkgpart' => 'Include line item(s) from package',
+                            'svc_dst_pkgpart'  => 'Include services of package',
+                          },
+
+              'fields' => [
+                            { field=>'clone',  type=>'hidden' },
+                            { field=>'pkgnum', type=>'hidden' },
+
+                            { type => 'columnstart' },
+                            
+                              {field=>'pkg',      type=>'text', size=>40 }, #32
+                              {field=>'comment',  type=>'text', size=>40 }, #32
+                              {field=>'classnum', type=>'select-pkg_class' },
+                              {field=>'disabled', type=>'checkbox', value=>'Y'},
+
+                              { type  => 'tablebreak-tr-title',
+                                value => 'Pricing', #better name?
+                              },
+                              { field => 'plan',
+                                type  => 'selectlayers-select',
+                                options => [ keys %plan_labels ],
+                                labels  => \%plan_labels,
+                              },
+                              { field => 'setup_fee',
+                                type  => 'money',
+                              },
+                              { field    => 'freq',
+                                type     => 'part_pkg_freq',
+                                onchange => 'freq_changed', #XXX enable recurring fee
+                              },
+                              { field    => 'recur_fee',
+                                type     => 'money',
+                                disabled => sub { $recur_disabled },
+                              },
+                                
+                              #price plan
+                              #setup fee
+                              #recurring frequency
+                              #recurring fee (auto-disable)
+
+                            { type => 'columnnext' },
+
+                              {type=>'justtitle', value=>'Taxation' },
+                              {field=>'setuptax', type=>'checkbox', value=>'Y'},
+                              {field=>'recurtax', type=>'checkbox', value=>'Y'},
+                              {field=>'classnum', type=>'select-taxclass' },
+                              {field=>'taxproductnum', type=>'select-taxproduct' },
+
+                              { type  => 'tablebreak-tr-title',
+                                value => 'Promotions', #XXX better name?
+                              },
+                              { field=>'promo_code', type=>'text', size=>15 },
+
+                              { type  => 'tablebreak-tr-title',
+                                value => 'Line-item revenue recogition', #XXX better name?
+                              },
+                              { field=>'pay_weight',    type=>'text', size=>6 },
+                              { field=>'credit_weight', type=>'text', size=>6 },
+
+                            { type => 'columnnext' },
+
+                              { field=>'agent_type',
+                                type => 'select-agent_types',
+                                curr_value_callback => sub {
+                                  my($cgi, $object, $field) = @_;
+                                  #in the other callbacks..?  hmm.
+                                  \@agent_type;
+                                },
+                              },
+
+                            { type => 'columnend' },
+
+                            { 'type'  => 'tablebreak-tr-title',
+                              'value' => 'Pricing add-ons',
+                            },
+                            { 'field'    => 'bill_dst_pkgpart',
+                              'type'     => 'select-part_pkg',
+                              'm2_label' => 'Include line item(s) from package',
+                              'm2m_table'        => 'part_pkg_link',
+                              'm2m_target_table' => 'part_pkg', #XXX actually just the method name...
+                              'm2m_dstcol'       => 'dst_pkgpart',
+                              'm2m_static_or_something' => { 'link_type' => 'bill' }, #XXX
+                              'm2_error_callback' => sub { (); }, #XXX existing!
+                            },
+
+                            { type  => 'tablebreak-tr-title',
+                              value => 'Services',
+                            },
+                            { type => 'pkg_svc', },
+
+                            { 'field'    => 'svc_dst_pkgpart',
+                              'label'    => 'Also include services from package: ',
+                              'type'     => 'select-part_pkg',
+                              'm2_label' => 'Include services of package: ',
+                              'm2m_table'        => 'part_pkg_link',
+                              'm2m_target_table' => 'part_pkg', #XXX actually just the method name...
+                              'm2m_dstcol'       => 'dst_pkgpart',
+                              'm2m_static_or_something' => { 'link_type' => 'svc' }, #XXX
+                              'm2_error_callback' => sub { (); }, #XXX existing!
+                            },
+
+                            { type  => 'tablebreak-tr-title',
+                              value => 'Price plan options',
+                            },
+
+                          ],
+
+           )
+%>
 <%init>
 
-if ( $cgi->param('clone') && $cgi->param('clone') =~ /^(\d+)$/ ) {
-  $cgi->param('clone', $1);
-} else {
-  $cgi->param('clone', '');
-}
-if ( $cgi->param('pkgnum') && $cgi->param('pkgnum') =~ /^(\d+)$/ ) {
-  $cgi->param('pkgnum', $1);
-} else {
-  $cgi->param('pkgnum', '');
-}
-
 my $curuser = $FS::CurrentUser::CurrentUser;
 
 die "access denied"
@@ -439,61 +148,264 @@ die "access denied"
       || $curuser->access_right('Edit global package definitions')
       || ( $cgi->param('pkgnum') && $curuser->access_right('Customize customer package') );
 
-my ($query) = $cgi->keywords;
+#XXX
+# - part_pkg.pm bits (need separate access methods not just part_pkg_link)
+# - tr-part_pkg_freq: month_increments_only (from price plans)
+# - test editing 
+#   - write edit bits for m2ms
+# - display add-ons in browse... yeah
+# -QIS- thank goodness
+# - test cloning
+# - test custom pricing
+#recur_flat->recur_fee migration, ugh
+# - move the selectlayer divs away from lame layer_callback
+
+#my ($query) = $cgi->keywords;
+#
+#my $part_pkg = '';
 
-my $conf = new FS::Conf; 
-my $part_pkg = '';
 my @agent_type = ();
 my $tax_override;
-my @all_agent_types = map {$_->typenum} qsearch('agent_type',{});
-if ( $cgi->param('error') ) {
-  $part_pkg = new FS::part_pkg ( {
-    map { $_, scalar($cgi->param($_)) } fields('part_pkg')
-  } );
-  (@agent_type) = $cgi->param('agent_type');
-  $tax_override = $cgi->param('tax_override');
-}
 
-my $action = '';
 my $clone_part_pkg = '';
-my $pkgpart = '';
-if ( $cgi->param('clone') ) {
-  $pkgpart = $cgi->param('clone');
-  #$action = 'Custom Pricing';
-  $action = 'Custom';
+
+my %options = ();
+my $recur_disabled = 1;
+my $error_callback = sub {
+  my($cgi, $object, $fields) = @_;
+  (@agent_type) = $cgi->param('agent_type');
+  $tax_override = $cgi->param('tax_override');
   $clone_part_pkg= qsearchs('part_pkg', { 'pkgpart' => $cgi->param('clone') } );
-  $part_pkg ||= $clone_part_pkg->clone;
-  $part_pkg->disabled('Y') unless $cgi->param('error');
-} elsif ( $query && $query =~ /^(\d+)$/ ) {
-  (@agent_type) = map {$_->typenum} qsearch('type_pkgs',{'pkgpart'=>$1})
-    unless $part_pkg;
-  unless ($part_pkg) {
-    $tax_override =
+
+  $recur_disabled = $cgi->param('freq') ? 0 : 1;
+
+  #some false laziness w/process
+  $cgi->param('plan') =~ /^(\w+)$/ or die 'unparsable plan';
+  my $plan = $1;
+  my $options = $cgi->param($plan."__OPTIONS");
+  my @options = split(',', $options);
+  %options =
+    map { my $optionname = $_;
+          my $param = $plan."__$optionname";
+          my $value = join(', ', $cgi->param($param));
+          ( $optionname => $value );
+        }
+        @options;
+
+  $cgi->param($_, $options{$_}) foreach (qw( setup_fee recur_fee ));
+
+};
+
+my $new_hashref_callback = sub { { 'plan' => 'flat' }; };
+
+my $new_object_callback = sub {
+  my( $cgi, $hashref, $fields, $opt ) = @_;
+
+  if ( $cgi->param('clone') ) {
+    $opt->{action} = 'Custom';
+    $clone_part_pkg = qsearchs('part_pkg', { 'pkgpart' => $cgi->param('clone') } );
+    my $part_pkg = $clone_part_pkg->clone;
+    $part_pkg->disabled('Y');
+    %options = $clone_part_pkg->options;
+    $part_pkg;
+  } else {
+    FS::part_pkg->new( $hashref );
+  }
+
+};
+
+my $edit_callback = sub {
+  my( $cgi, $object, $fields ) = @_;
+
+  $recur_disabled = $object->freq ? 0 : 1;
+
+  (@agent_type) = map {$_->typenum} qsearch('type_pkgs',{'pkgpart'=>$1});
+  $tax_override =
     join (",", map {$_->taxclassnum}
                qsearch( 'part_pkg_taxoverride', {'pkgpart' => $1} )
          );
+
 #    join (",", map {$_->taxclassnum}
 #               $part_pkg->part_pkg_taxrate( 'cch', $conf->config('defaultloc')
 #         );
 #      unless $tax_override;
+
+  %options = $object->options;
+
+};
+
+my $new_callback = sub {
+  my( $cgi, $object, $fields ) = @_;
+
+  my $conf = new FS::Conf; 
+  if ( $conf->exists('agent_defaultpkg') ) {
+    #my @all_agent_types = map {$_->typenum} qsearch('agent_type',{});
+    @agent_type = map {$_->typenum} qsearch('agent_type',{});
   }
-  $part_pkg ||= qsearchs('part_pkg',{'pkgpart'=>$1});
-  $pkgpart = $part_pkg->pkgpart;
-} else {
-  unless ( $part_pkg ) {
-    $part_pkg = new FS::part_pkg {};
-    $part_pkg->plan('flat');
-    @agent_type = @all_agent_types if $conf->exists('agent_defaultpkg');
-      
-  }
-}
-unless ( $part_pkg->plan ) { #backwards-compat
-  $part_pkg->plan('flat');
-  $part_pkg->plandata("setup_fee=". $part_pkg->setup. "\n".
-                      "recur_fee=". $part_pkg->recur. "\n");
-}
-$action ||= $part_pkg->pkgpart ? 'Edit' : 'Add';
-my $hashref = $part_pkg->hashref;
-my $taxproduct_description = $part_pkg->taxproduct_description;
+
+};
+
+my $freq_changed = <<'END';
+  <SCRIPT TYPE="text/javascript">
+
+    function freq_changed(what) {
+      var freq = what.options[what.selectedIndex].value;
+
+      if ( freq == '0' ) {
+        what.form.recur_fee.disabled = true;
+        what.form.recur_fee.style.backgroundColor = '#dddddd';
+      } else {
+        what.form.recur_fee.disabled = false;
+        what.form.recur_fee.style.backgroundColor = '#ffffff';
+      }
+
+    }
+
+  </SCRIPT>
+END
+
+tie my %plans, 'Tie::IxHash', %{ FS::part_pkg::plan_info() };
+
+tie my %plan_labels, 'Tie::IxHash',
+  map {  $_ => ( $plans{$_}->{'shortname'} || $plans{$_}->{'name'} ) }
+      keys %plans;
+
+my $html_bottom = sub {
+  my( $object ) = @_;
+
+  #warn join("\n", map { "$_: $options{$_}" } keys %options ). "\n";
+
+  my $layer_callback = sub {
+  
+    my $layer = shift;
+    my $html = ntable("#cccccc",2);
+  
+    #$html .= '
+    #  <TR>
+    #    <TD ALIGN="right">Recurring fee frequency </TD>
+    #    <TD><SELECT NAME="freq">
+    #';
+    #
+    #my @freq = keys %freq;
+    #@freq = grep { /^\d+$/ } @freq
+  #XXX this bit#  #  if exists($plans{$layer}->{'freq'}) && $plans{$layer}->{'freq'} eq 'm';
+    #foreach my $freq ( @freq ) {
+    #  $html .= qq(<OPTION VALUE="$freq");
+    #  $html .= ' SELECTED' if $freq eq $part_pkg->freq;
+    #  $html .= ">$freq{$freq}";
+    #}
+    #$html .= '</SELECT></TD></TR>';
+  
+    my $href = $plans{$layer}->{'fields'};
+    my @fields = exists($plans{$layer}->{'fieldorder'})
+                   ? @{$plans{$layer}->{'fieldorder'}}
+                   : keys %{ $href };
+  
+    foreach my $field ( grep $_ !~ /^(setup|recur)_fee$/, @fields ) {
+  
+      $html .= '<TR><TD ALIGN="right">'. $href->{$field}{'name'}. '</TD><TD>';
+  
+      my $format = sub { shift };
+      $format = $href->{$field}{'format'} if exists($href->{$field}{'format'});
+
+      #XXX these should use elements/ fields... (or this whole thing should
+      #just use layer_fields instead of layer_callback)
+  
+      if ( ! exists($href->{$field}{'type'}) ) {
+  
+        $html .= qq!<INPUT TYPE="text" NAME="${layer}__$field" VALUE="!.
+                 ( exists($options{$field})
+                     ? &$format($options{$field})
+                     : $href->{$field}{'default'} ).
+                 qq!">!;
+  
+      } elsif ( $href->{$field}{'type'} eq 'checkbox' ) {
+  
+        $html .= qq!<INPUT TYPE="checkbox" NAME="${layer}__$field" VALUE=1 !.
+                 ( exists($options{$field}) && $options{$field}
+                   ? ' CHECKED'
+                   : ''
+                 ). '>';
+  
+      } elsif ( $href->{$field}{'type'} =~ /^select/ ) {
+  
+        $html .= '<SELECT';
+        $html .= ' MULTIPLE'
+          if $href->{$field}{'type'} eq 'select_multiple';
+        $html .= qq! NAME="${layer}__$field">!;
+  
+        if ( $href->{$field}{'select_table'} ) {
+          foreach my $record (
+            qsearch( $href->{$field}{'select_table'},
+                     $href->{$field}{'select_hash'}   )
+          ) {
+            my $value = $record->getfield($href->{$field}{'select_key'});
+            $html .= qq!<OPTION VALUE="$value"!.
+                     (  $options{$field} =~ /(^|, *)$value *(,|$)/ #XXX fix?
+                          ? ' SELECTED'
+                          : ''
+                     ).
+                     '>'. $record->getfield($href->{$field}{'select_label'});
+          }
+        } elsif ( $href->{$field}{'select_options'} ) {
+          foreach my $key ( keys %{ $href->{$field}{'select_options'} } ) {
+            my $label = $href->{$field}{'select_options'}{$key};
+            $html .= qq!<OPTION VALUE="$key"!.
+                     ( $options{$field} =~ /(^|, *)$key *(,|$)/ #XXX fix?
+                         ? ' SELECTED'
+                         : ''
+                     ).
+                     '>'. $label;
+          }
+  
+        } else {
+          $html .= '<font color="#ff0000">warning: '.
+                   "don't know how to retreive options for $field select field".
+                   '</font>';
+        }
+        $html .= '</SELECT>';
+  
+      } elsif ( $href->{$field}{'type'} eq 'radio' ) {
+  
+        my $radio =
+          qq!<INPUT TYPE="radio" NAME="${layer}__$field"!;
+  
+        foreach my $key ( keys %{ $href->{$field}{'options'} } ) {
+          my $label = $href->{$field}{'options'}{$key};
+          $html .= qq!$radio VALUE="$key"!.
+                   ( $options{$field} =~ /(^|, *)$key *(,|$)/ #XXX fix?
+                       ? ' CHECKED'
+                       : ''
+                   ).
+                   "> $label<BR>";
+        }
+  
+      }
+  
+      $html .= '</TD></TR>';
+    }
+    $html .= '</TABLE>';
+  
+    $html .= qq(<INPUT TYPE="hidden" NAME="${layer}__OPTIONS" VALUE=").
+             join(',', keys %{ $href } ). '">';
+  
+    $html;
+  
+  };
+
+  my %selectlayers = (
+    field          => 'plan',
+    options        => [ keys %plan_labels ],
+    labels         => \%plan_labels,
+    curr_value     => $object->plan,
+    layer_callback => $layer_callback,
+  );
+
+  include('/elements/selectlayers.html', %selectlayers, 'layers_only'=>1 ).
+  '<SCRIPT TYPE="text/javascript">'.
+    include('/elements/selectlayers.html', %selectlayers, 'js_only'=>1 ).
+  '</SCRIPT>';
+
+};
 
 </%init>
index d29ffcf..7964dfa 100644 (file)
@@ -32,8 +32,13 @@ Example:
 
    'clear_on_error' => [ 'form_field1', 'form_field2', ... ],
 
+                    #pass an arrayref of hashrefs for multiple m2ms or m2names
+
    'process_m2m' => { 'link_table'   => 'link_table_name',
                       'target_table' => 'target_table_name',
+                      #optional (see m2m_Common::process_m2m), if not specified
+                      # all CGI params will be passed)
+                      'params'       => 
                     },
    'process_m2name' => { 'link_table'   => 'link_table_name',
                          'link_static' => { 'column' => 'value' },
@@ -49,6 +54,10 @@ Example:
 
                        },
 
+   #checks CGI params and whatever else before much else runs
+   #return an error string or empty for no error
+   'precheck_callback' => sub { my( $cgi ) = @_; },
+
    #supplies arguments to insert() and replace()
    # for use with tables that are FS::option_Common
    'args_callback' => sub { my( $cgi, $object ) = @_; },
@@ -107,8 +116,8 @@ Example:
 <% $cgi->redirect( $opt{'redirect'}. $pkeyvalue ) %>
 %
 %} else { 
-%
-<% $cgi->redirect( popurl(3). ($opt{viewall_dir}||'search'). "/$table.html" ) %>
+%  my $ext = $opt{'viewall_ext'} || 'html';
+<% $cgi->redirect( popurl(3). ($opt{viewall_dir}||'search'). "/$table.$ext" ) %>
 %}
 <%once>
 
@@ -121,6 +130,11 @@ my(%opt) = @_;
 
 my $curuser = $FS::CurrentUser::CurrentUser;
 
+my $error = '';
+if ( $opt{'precheck_callback'} ) {
+  $error = &{ $opt{'precheck_callback'} }( $cgi );
+}
+
 #false laziness w/edit.html
 my $table = $opt{'table'};
 my $class = "FS::$table";
@@ -146,7 +160,7 @@ if ( $pkeyvalue ) {
 }
 
 my %hash =
-  map { my @entry = ( $_ => $cgi->param($_) );
+  map { my @entry = ( $_ => scalar($cgi->param($_)) );
         $opt{'value_callback'} ? ( $_ => &{ $opt{'value_callback'} }( @entry ))
                                : ( @entry )
       } @$fields;
@@ -168,7 +182,7 @@ if ($old && exists($opt{'copy_on_empty'})) {
   }
 }
 
-my $error = $new->check;
+$error ||= $new->check;
 
 my @args = ();
 if ( !$error && $opt{'args_callback'} ) {
@@ -192,28 +206,42 @@ if ( !$error ) {
 
 if ( !$error && $opt{'process_m2m'} ) {
 
-  if ( $opt{'debug'} ) {
-    warn "$me processing m2m:\n". Dumper( %{ $opt{'process_m2m'} },
-                                          'params' => scalar($cgi->Vars),
-                                        );
+  my @process_m2m = ref($opt{'process_m2m'}) eq 'ARRAY'
+                      ? @{ $opt{'process_m2m'} }
+                      :  ( $opt{'process_m2m'} );
+
+  foreach my $process_m2m (@process_m2m) {
+
+    $process_m2m->{'params'} ||= scalar($cgi->Vars);
+
+    warn "$me processing m2m:\n". Dumper( %$process_m2m )
+      if $opt{'debug'};
+
+    $error = $new->process_m2m( %$process_m2m );
   }
 
-  $error = $new->process_m2m( %{ $opt{'process_m2m'} },
-                              'params' => scalar($cgi->Vars),
-                            );
 }
 
 if ( !$error && $opt{'process_m2name'} ) {
 
-  if ( $opt{'debug'} ) {
-    warn "$me processing m2name:\n". Dumper( %{ $opt{'process_m2name'} },
-                                             'params' => scalar($cgi->Vars),
-                                           );
+  my @process_m2name = ref($opt{'process_m2name'}) eq 'ARRAY'
+                         ? @{ $opt{'process_m2name'} }
+                         :  ( $opt{'process_m2name'} );
+
+
+  foreach my $process_m2name (@process_m2name) {
+
+    if ( $opt{'debug'} ) {
+      warn "$me processing m2name:\n". Dumper( %{ $process_m2name },
+                                               'params' => scalar($cgi->Vars),
+                                             );
+    }
+
+    $error = $new->process_m2name( %{ $process_m2name },
+                                   'params' => scalar($cgi->Vars),
+                                 );
   }
 
-  $error = $new->process_m2name( %{ $opt{'process_m2name'} },
-                                 'params' => scalar($cgi->Vars),
-                               );
 }
 
 
index 94bff0f..be6e2f8 100755 (executable)
-%if ( $error ) {
-%  $dbh->rollback if $oldAutoCommit;
-%  $cgi->param('error', $error );
-<% $cgi->redirect(popurl(2). "part_pkg.cgi?". $cgi->query_string ) %>
-%} elsif ( $custnum )  {
-%  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
-<% $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum") %>
-%} else {
-%  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
-<% $cgi->redirect(popurl(3). "browse/part_pkg.cgi") %>
-%}
+<% include( 'elements/process.html',
+              #'debug'             => 1,
+              'table'             => 'part_pkg',
+              'viewall_dir'       => 'browse',
+              'viewall_ext'       => 'cgi',
+              'edit_ext'          => 'cgi',
+              #XXX usable with cloning? #'agent_null_right'  => 'Edit global package definitions',
+              'precheck_callback' => $precheck_callback,
+              'args_callback'     => $args_callback,
+              'process_m2m'       => \@process_m2m,
+              'debug' => 1,
+          )
+%>
 <%init>
 
-my $dbh = dbh;
-my $conf = new FS::Conf;
+my $curuser = $FS::CurrentUser::CurrentUser;
 
-my $pkgpart = $cgi->param('pkgpart');
-
-my $old = qsearchs('part_pkg',{'pkgpart'=>$pkgpart}) if $pkgpart;
-
-tie my %plans, 'Tie::IxHash', %{ FS::part_pkg::plan_info() };
-my $href = $plans{$cgi->param('plan')}->{'fields'};
-
-#fixup plandata
-my $error;
-my $plandata = $cgi->param('plandata');
-my @plandata = split(',', $plandata);
-$cgi->param('plandata', 
-  join('', map { my $parser = sub { shift };
-                 $parser = $href->{$_}{parse} if exists($href->{$_}{parse});
-                 my $value = join(', ', &$parser($cgi->param($_)));
-                 my $check = $href->{$_}{check};
-                 if ( $check && ! &$check($value) ) {
-                   $value = join(', ', $cgi->param($_));
-                   $error ||= "Illegal ". ($href->{$_}{name}||$_). ": $value";
-                 }
-                 "$_=$value\n";
-               } @plandata )
-);
+die "access denied"
+  unless $curuser->access_right('Edit package definitions')
+      || $curuser->access_right('Edit global package definitions')
+      || ( ! $cgi->param('pkgpart') && $cgi->param('pkgnum') && $curuser->access_right('Customize customer package') );
 
-foreach (qw( setuptax recurtax disabled )) {
-  $cgi->param($_, '') unless defined $cgi->param($_);
-}
+my $precheck_callback = sub {
+  my( $cgi ) = @_;
 
-my @agents;
-foreach ($cgi->param('agent_type')) {
-  /^(\d+)$/;
-  push @agents, $1 if $1;
-}
-$error = "At least one agent type must be specified."
-  unless( scalar(@agents) ||
-          $cgi->param('clone') && $cgi->param('clone') =~ /^\d+$/ ||
-          !$pkgpart && $conf->exists('agent-defaultpkg')
-        );
+  my $conf = new FS::Conf;
 
-$cgi->param('tax_override') =~ /^([\d,]+)$/;
-my (@tax_overrides) = (grep "$_", split (",", $1));
+  foreach (qw( setuptax recurtax disabled )) {
+    $cgi->param($_, '') unless defined $cgi->param($_);
+  }
 
-my $new = new FS::part_pkg ( {
-  map {
-    $_ => scalar($cgi->param($_));
-  } fields('part_pkg')
-} );
+  return 'Must select a tax class'
+    if $cgi->param('taxclass') eq '(select)';
 
-my $oldAutoCommit = $FS::UID::AutoCommit;
-local $FS::UID::AutoCommit = 0;
+  my @agents = ();
+  foreach ($cgi->param('agent_type')) {
+    /^(\d+)$/;
+    push @agents, $1 if $1;
+  }
+  return "At least one agent type must be specified."
+    unless( scalar(@agents) ||
+            $cgi->param('clone') && $cgi->param('clone') =~ /^\d+$/ ||
+            !$cgi->param('pkgpart') && $conf->exists('agent-defaultpkg')
+          );
 
-my %pkg_svc = map { $_ => scalar($cgi->param("pkg_svc$_")) }
-              map { $_->svcpart }
-              qsearch('part_svc', {} );
+  return '';
 
-my $curuser = $FS::CurrentUser::CurrentUser;
+};
 
 my $custnum = '';
-if ( $error ) {
-
- # fall through
-
-} elsif ( $cgi->param('taxclass') eq '(select)' ) {
 
-  $error = 'Must select a tax class';
+my $args_callback = sub {
+  my( $cgi, $new ) = @_;
+  
+  my @args = ( 'primary_svc' => scalar($cgi->param('pkg_svc_primary')) );
+
+  ##
+  #options
+  ##
+  
+  $cgi->param('plan') =~ /^(\w+)$/ or die 'unparsable plan';
+  my $plan = $1;
+  
+  tie my %plans, 'Tie::IxHash', %{ FS::part_pkg::plan_info() };
+  my $href = $plans{$plan}->{'fields'};
+  
+  my $error = '';
+  my $options = $cgi->param($plan."__OPTIONS");
+  my @options = split(',', $options);
+  my %options =
+    map { my $optionname = $_;
+          my $param = $plan."__$optionname";
+          my $parser = exists($href->{$optionname}{parse})
+                         ? $href->{$optionname}{parse}
+                         : sub { shift };
+          my $value = join(', ', &$parser($cgi->param($param)));
+          my $check = $href->{$optionname}{check};
+          if ( $check && ! &$check($value) ) {
+            $value = join(', ', $cgi->param($param));
+            $error ||= "Illegal ".
+                         ($href->{$optionname}{name}||$optionname). ": $value";
+          }
+          ( $optionname => $value );
+        }
+        @options;
+
+  $options{$_} = scalar($cgi->param($_)) for (qw( setup_fee recur_fee ));
+  
+  push @args, 'options' => \%options;
+
+  ###
+  #pkg_svc
+  ###
+
+  my %pkg_svc = map { $_ => scalar($cgi->param("pkg_svc$_")) }
+                map { $_->svcpart }
+                qsearch('part_svc', {} );
+
+  push @args, 'pkg_svc' => \%pkg_svc;
+
+  ###
+  # cust_pkg and custnum_ref (inserts only)
+  ###
+  unless ( $cgi->param('pkgpart') ) {
+    push @args, 'cust_pkg'    => scalar($cgi->param('pkgnum')),
+                'custnum_ref' => \$custnum;
+  }
+
+  @args;
+
+};
 
-} elsif ( $pkgpart ) {
-
-  die "access denied"
-    unless $curuser->access_right('Edit package definitions')
-        || $curuser->access_right('Edit global package definitions');
-
-  $error = $new->replace( $old,
-                          pkg_svc     => \%pkg_svc,
-                          primary_svc => scalar($cgi->param('pkg_svc_primary')),
-                        );
-} else {
+$cgi->param('tax_override') =~ /^([\d,]+)$/;
+my (@tax_overrides) = (grep "$_", split (",", $1));
 
-  die "access denied"
-    unless $curuser->access_right('Edit package definitions')
-        || $curuser->access_right('Edit global package definitions')
-        || ( $cgi->param('pkgnum') && $curuser->access_right('Customize customer package') );
+my @process_m2m = (
+  {
+    'link_table'   => 'part_pkg_taxoverride',
+    'target_table' => 'tax_class',
+    'params'       => \@tax_overrides,
+  }
+);
 
-  $error = $new->insert(  pkg_svc     => \%pkg_svc,
-                          primary_svc => scalar($cgi->param('pkg_svc_primary')),
-                          cust_pkg    => $cgi->param('pkgnum'),
-                          custnum_ref => \$custnum,
-                       );
-  $pkgpart = $new->pkgpart;
-}
+my $conf = new FS::Conf;
 
-unless ( $error || $conf->exists('agent_defaultpkg') ) {
-  $error = $new->process_m2m(
+if ( $cgi->param('pkgpart') || ! $conf->exists('agent_defaultpkg') ) {
+  my @agents = ();
+  foreach ($cgi->param('agent_type')) {
+    /^(\d+)$/;
+    push @agents, $1 if $1;
+  }
+  warn "AGENTS: @agents";
+  push @process_m2m, {
     'link_table'   => 'type_pkgs',
     'target_table' => 'agent_type',
     'params'       => \@agents,
-  );
-}
-
-unless ( $error  ) {
-  $error = $new->process_m2m(
-    'link_table'   => 'part_pkg_taxoverride',
-    'target_table' => 'tax_class',
-    'params'       => \@tax_overrides,
-  );
+  };
 }
 
 </%init>
diff --git a/httemplate/elements/select-agent_types.html b/httemplate/elements/select-agent_types.html
new file mode 100644 (file)
index 0000000..e56fee4
--- /dev/null
@@ -0,0 +1,30 @@
+%# if ( $cgi->param('clone') ) { #XXX
+% if ( $opt{'disable'} ) { 
+
+    <INPUT TYPE="hidden" NAME="agent_type" VALUE="">
+
+% } elsif ( scalar(@all_agent_types) == 1) {
+
+    <INPUT TYPE="hidden" NAME="agent_type" VALUE="<% $all_agent_types[0] %>">
+
+% } else {
+
+    <% include( 'select-table.html',
+                  'element_name' => 'agent_type',
+                  'table'        => 'agent_type',
+                  'name_col'     => 'atype',
+                  #'value'        => \@agent_type,
+                  'element_etc'  => 'size="10"',
+                  %opt,
+                  'multiple'     =>  '1', #cause edit.html is dum
+              )
+    %>
+
+% }
+<%init>
+
+my %opt = @_;
+
+my @all_agent_types = map {$_->typenum} qsearch('agent_type',{});
+
+</%init>
index f2ae9eb..dc7ab97 100644 (file)
@@ -20,4 +20,7 @@
 
 my %opt = @_;
 
+$opt{'taxproduct_description'} ||= $opt{'object'}->taxproduct_description
+  if $opt{'object'};
+
 </%init>
index 3026217..82f5dd1 100644 (file)
@@ -44,6 +44,10 @@ Example:
                         ...
                       },
 
+    #or manual control, instead of layer_fields and layer_values above
+    #called with args: my( $layer, $layer_fields, $layer_values, $layer_prefix )
+    'layer_callback' => 
+
     'html_between  => '', #optional HTML displayed between the SELECT and the
                           #layers, scalar or coderef ('field' passed as a param)
     'onchange'     => '', #javascript code run when the SELECT changes
@@ -129,7 +133,7 @@ Example:
                   %>"
       >
 
-        <% layer_callback($layer, $layer_fields, $layer_values, $layer_prefix) %>
+        <% &{$layer_callback}($layer, $layer_fields, $layer_values, $layer_prefix) %>
 
       </DIV>
 
@@ -165,6 +169,8 @@ my $layer_fields = $opt{layer_fields};
 my $layer_values = $opt{layer_values};
 my $layer_prefix = $opt{layer_prefix};
 
+my $layer_callback = $opt{layer_callback} || \&layer_callback;
+
 sub layer_callback {
   my( $layer, $layer_fields, $layer_values, $layer_prefix ) = @_;
 
index 49ae166..f71f2f7 100644 (file)
@@ -35,6 +35,11 @@ my $maxlength = $opt{'maxlength'}
                 ? 'MAXLENGTH="'. $opt{'maxlength'}. '"'
                 : '';
 
+$opt{'disabled'} = &{ $opt{'disabled'} }( \%opt )
+  if ref($opt{'disabled'}) eq 'CODE';
+$opt{'disabled'} = 'DISABLED'
+  if $opt{'disabled'} && $opt{'disabled'} !~ /disabled/i; # uuh... yeah?
+
 my @style = ();
 
 push @style, 'text-align: '. $opt{'text-align'}
@@ -43,9 +48,6 @@ push @style, 'text-align: '. $opt{'text-align'}
 push @style, 'background-color: #dddddd'
   if $opt{'disabled'};
 
-$opt{'disabled'} = 'DISABLED'
-  if $opt{'disabled'} && $opt{'disabled'} !~ /disabled/i; # uuh... yeah?
-
 my $style = scalar(@style) ? 'STYLE="'. join(';', @style). '"' : '';
 
 my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
index 98b7da5..649f8a2 100644 (file)
@@ -1,15 +1,10 @@
-<% include('tr-td-label.html', @_ ) %>
-
-  <TD>
-    <SELECT NAME="freq">
-%     foreach my $freq ( @freq ) {
-        <OPTION VALUE="<% $freq %>" <% $freq eq $curr_value ? 'SELECTED' : '' %>><% $freq{$freq} %>
-%     }
-    </SELECT>
-  </TD>
-
-</TR>
-
+<% include('tr-select.html', @_, 
+             'field'      => 'freq',
+             'options'    => \@freq,
+             'labels'     => \%freq,
+             'curr_value' => $curr_value,
+          )
+%>
 <%init>
 
 my %opt = @_;
diff --git a/httemplate/elements/tr-pkg_svc.html b/httemplate/elements/tr-pkg_svc.html
new file mode 100644 (file)
index 0000000..4c8a839
--- /dev/null
@@ -0,0 +1,93 @@
+<TR>
+  <TD BGCOLOR="#e8e8e8" COLSPAN=99>
+
+<% itable('', 4, 1) %><TR><TD VALIGN="top">
+<% $thead %>
+
+%foreach my $part_svc ( @part_svc ) {
+%  my $svcpart = $part_svc->svcpart;
+%  my $pkg_svc = $pkg_svc{$svcpart}
+%             || new FS::pkg_svc ( {
+%                                   'pkgpart'     => $pkgpart,
+%                                   'svcpart'     => $svcpart,
+%                                   'quantity'    => 0,
+%                                   'primary_svc' => '',
+%                                } );
+%  if ( $cgi->param('error') ) {
+%    my $primary_svc = ( $pkg_svc->primary_svc =~ /^Y/i );
+%    my $pkg_svc_primary = scalar($cgi->param('pkg_svc_primary'));
+%    $pkg_svc->primary_svc('')
+%      if $primary_svc && $pkg_svc_primary != $svcpart;
+%    $pkg_svc->primary_svc('Y')
+%      if ! $primary_svc && $pkg_svc_primary == $svcpart;
+%  }
+%
+%  push @fixups, "pkg_svc$svcpart";
+%
+%  my $quan = 0;
+%  if ( $cgi->param("pkg_svc$svcpart") =~ /^\s*(\d+)\s*$/ ) {
+%    $quan = $1;
+%  } elsif ( $pkg_svc->quantity ) {
+%    $quan = $pkg_svc->quantity;
+%  }
+
+  <TR>
+    <TD>
+      <INPUT TYPE="text" NAME="pkg_svc<% $svcpart %>" SIZE=4 MAXLENGTH=3 VALUE="<% $quan %>">
+    </TD>
+   
+    <TD ALIGN="center">
+      <INPUT TYPE="radio" NAME="pkg_svc_primary" VALUE="<% $svcpart %>" <% $pkg_svc->primary_svc =~ /^Y/i ? ' CHECKED' : '' %>>
+    </TD>
+
+    <TD>
+      <A HREF="part_svc.cgi?<% $part_svc->svcpart %>"><% $part_svc->svc %></A>      <% $part_svc->disabled =~ /^Y/i ? ' (DISABLED' : '' %>
+    </TD>
+  </TR>
+% foreach ( 1 .. $columns-1 ) {
+%       if ( $count == int( $_ * scalar(@part_svc) / $columns ) ) { 
+%  
+
+         </TABLE></TD><TD VALIGN="top"><% $thead %>
+%   }
+%     }
+%     $count++;
+%  
+% } 
+
+</TR></TABLE></TD></TR></TABLE>
+
+  </TD>
+</TR>
+
+<%init>
+
+my %opt = @_;
+my $cgi = $opt{'cgi'};
+
+my $thead =  "\n\n". ntable('#cccccc', 2).
+             '<TR><TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Quan.</FONT></TH>'.
+             '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-2>Primary</FONT></TH>'.
+             '<TH BGCOLOR="#dcdcdc">Service</TH></TR>';
+
+my $part_pkg = $opt{'object'};
+my $pkgpart = $part_pkg->pkgpart;
+
+my $where =  "WHERE disabled IS NULL OR disabled = ''";
+if ( $pkgpart ) {
+  $where .=  "   OR 0 < ( SELECT quantity FROM pkg_svc
+                           WHERE pkg_svc.svcpart = part_svc.svcpart
+                             AND pkgpart = $pkgpart
+                        )";
+}
+my @part_svc = qsearch('part_svc', {}, '', $where);
+
+#my $q_part_pkg = $clone_part_pkg || $part_pkg;
+#my %pkg_svc = map { $_->svcpart => $_ } $q_part_pkg->pkg_svc;
+my %pkg_svc = map { $_->svcpart => $_ } $part_pkg->pkg_svc;
+
+my @fixups = ();
+my $count = 0;
+my $columns = 3;
+
+</%init>
diff --git a/httemplate/elements/tr-select-agent_types.html b/httemplate/elements/tr-select-agent_types.html
new file mode 100644 (file)
index 0000000..29ac7f1
--- /dev/null
@@ -0,0 +1,19 @@
+% unless ( $opt{'disable'} || scalar(@all_agent_types) == 1 ) {
+
+<% include('/elements/tr-justtitle.html', value=>'Agent (reseller) types') %>
+
+% }
+
+<TR>
+  <TD COLSPAN=2>
+    <% include('select-agent_types.html', %opt) %>
+  </TD>
+</TR>
+
+<%init>
+
+my %opt = @_;
+
+my @all_agent_types = map {$_->typenum} qsearch('agent_type',{});
+
+</%init>
index 6e2f58f..8517737 100644 (file)
@@ -2,14 +2,4 @@
   <TD BGCOLOR="#e8e8e8" COLSPAN=2>&nbsp;</TD>
 </TR>
 
-<TR>
-  <TH BGCOLOR="#e8e8e8" COLSPAN=2 ALIGN="left">
-    <FONT SIZE="+1"><% $opt{value} %></FONT>
-  </TH>
-</TR>
-
-<%init>
-
-my %opt = @_;
-
-</%init>
+<% include('tr-justtitle.html', @_) %>