[freeside-commits] branch master updated. 3a2d8bbc434fbcb96563bd4d437b31db38c76f09

Ivan ivan at 420.am
Thu Apr 30 06:58:20 PDT 2015


The branch, master has been updated
       via  3a2d8bbc434fbcb96563bd4d437b31db38c76f09 (commit)
       via  a9bd30b5b5231db37360e285fbbfa237195cd064 (commit)
       via  f1446e6af463609c1488ba367d437b728556f35a (commit)
       via  8c974f743b0312b5dc9ef26f403b937abb5003d3 (commit)
       via  47cb646c89d4a798d35063f04db39c707eac4f4c (commit)
       via  3ef3c5061e6de2dd885987e844ea3122aafc4df6 (commit)
       via  b83ef1787970f9117a540cfa707ccd2fdcde1175 (commit)
       via  76e217e6a453518ae9aef2b63fae49629101470c (commit)
       via  67b56f17cc51d10394a986fb3d105213b097ee79 (commit)
       via  7d34aacffa38c4cac09b54080487a66c264e4668 (commit)
      from  58cdf1b78c0cd419eacf7db8661564af050e0f12 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 3a2d8bbc434fbcb96563bd4d437b31db38c76f09
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Thu Apr 30 06:58:09 2015 -0700

    service dependencies: cust_svc_suspend_cascade, RT#33685

diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 9352362..5859727 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -1404,7 +1404,7 @@ sub suspend {
       }
     }
 
-    my @cust_svc = qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
+    my @cust_svc = qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } );
 
     #attempt ordering ala cust_svc_suspend_cascade (without infinite-looping
     # on the circular dep case)
@@ -1413,7 +1413,7 @@ sub suspend {
     my %svcpart = ();
     $svcpart{$_->svcpart} = 0 foreach @cust_svc;
     foreach my $svcpart ( keys %svcpart ) {
-      foreach my $part_pkg_link (
+      foreach my $part_svc_link (
         FS::part_svc_link->by_agentnum($self->cust_main->agentnum,
                                          src_svcpart => $svcpart,
                                          link_type => 'cust_svc_suspend_cascade'
diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index 0465580..986c5ae 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -202,7 +202,7 @@ sub suspend {
   my $svc = qsearchs( $svcdb, { 'svcnum' => $self->svcnum } )
     or return '';
 
-  $error = $svc->suspend;
+  my $error = $svc->suspend;
   return $error if $error;
 
   if ( $opt{labels_arryref} ) {

commit a9bd30b5b5231db37360e285fbbfa237195cd064
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Thu Apr 30 06:52:43 2015 -0700

    service dependencies: cust_svc_suspend_cascade, RT#33685

diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 07e02f9..9352362 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -1404,31 +1404,34 @@ sub suspend {
       }
     }
 
-    my @labels = ();
-
-    foreach my $cust_svc (
-      qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
-    ) {
-      my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
-
-      $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
-        $dbh->rollback if $oldAutoCommit;
-        return "Illegal svcdb value in part_svc!";
-      };
-      my $svcdb = $1;
-      require "FS/$svcdb.pm";
-
-      my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
-      if ($svc) {
-        $error = $svc->suspend;
-        if ( $error ) {
-          $dbh->rollback if $oldAutoCommit;
-          return $error;
-        }
-        my( $label, $value ) = $cust_svc->label;
-        push @labels, "$label: $value";
+    my @cust_svc = qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
+
+    #attempt ordering ala cust_svc_suspend_cascade (without infinite-looping
+    # on the circular dep case)
+    #  (this is too simple for multi-level deps, we need to use something
+    #   to resolve the DAG properly when possible)
+    my %svcpart = ();
+    $svcpart{$_->svcpart} = 0 foreach @cust_svc;
+    foreach my $svcpart ( keys %svcpart ) {
+      foreach my $part_pkg_link (
+        FS::part_svc_link->by_agentnum($self->cust_main->agentnum,
+                                         src_svcpart => $svcpart,
+                                         link_type => 'cust_svc_suspend_cascade'
+                                      )
+      ) {
+        $svcpart{$part_svc_link->dst_svcpart} = max(
+          $svcpart{$part_svc_link->dst_svcpart},
+          $svcpart{$part_svc_link->src_svcpart} + 1
+        );
       }
     }
+    @cust_svc = sort { $svcpart{ $a->svcpart } <=> $svcpart{ $b->svcpart } }
+                  @cust_svc;
+
+    my @labels = ();
+    foreach my $cust_svc ( @cust_svc ) {
+      $cust_svc->suspend( 'labels_arrayref' => \@labels );
+    }
 
     # suspension fees: if there is a feepart, and it's not an unsuspend fee,
     # and this is not a suspend-before-cancel
diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index 5922e32..0465580 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -183,6 +183,37 @@ sub delete {
 
 }
 
+=item suspend
+
+Suspends the relevant service by calling the B<suspend> method of the associated
+FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object).
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub suspend {
+  my( $self, %opt ) = @_;
+
+  $self->part_svc->svcdb =~ /^([\w\-]+)$/ or return 'Illegal part_svc.svcdb';
+  my $svcdb = $1;
+  require "FS/$svcdb.pm";
+
+  my $svc = qsearchs( $svcdb, { 'svcnum' => $self->svcnum } )
+    or return '';
+
+  $error = $svc->suspend;
+  return $error if $error;
+
+  if ( $opt{labels_arryref} ) {
+    my( $label, $value ) = $self->label;
+    push @{ $opt{labels_arrayref} }, "$label: $value";
+  }
+
+  '';
+
+}
+
 =item cancel
 
 Cancels the relevant service by calling the B<cancel> method of the associated
diff --git a/FS/FS/part_svc_link.pm b/FS/FS/part_svc_link.pm
index a7f1b0f..e8c2cc5 100644
--- a/FS/FS/part_svc_link.pm
+++ b/FS/FS/part_svc_link.pm
@@ -88,7 +88,7 @@ unprovisioned
 
 =item cust_svc_suspend_cascade
 
-Suspend the destination service before the source service
+Suspend the destination service after the source service
 
 =back
 
@@ -205,7 +205,7 @@ sub description {
    and return "Automatically unprovision $dst when $src is unprovisioned";
 
   $l eq 'cust_svc_suspend_cascade'
-   and return "Suspend $dst before $src";
+   and return "Suspend $dst after $src";
 
   warn "WARNING: unknown part_svc_link.link_type $l\n";
   return "$src (unknown link_type $l) $dst";
diff --git a/httemplate/edit/part_svc_link.html b/httemplate/edit/part_svc_link.html
index c8c385d..980b24e 100644
--- a/httemplate/edit/part_svc_link.html
+++ b/httemplate/edit/part_svc_link.html
@@ -35,7 +35,7 @@ my @fields = (
       cust_svc_provision_restrict => 'Require the target service to be provisioned before the source service',
       cust_svc_unprovision_restrict => 'Require the target service to be unprovisioned before the source service',
       cust_svc_unprovision_cascade => 'Automatically unprovision the target service when the source service is unprovisioned',
-      cust_svc_suspend_cascade => 'Suspend the target service before the source service',
+      cust_svc_suspend_cascade => 'Suspend the target service after the source service',
     },
   },
   { field => 'disabled', type => 'checkbox', value => 'Y' }

commit f1446e6af463609c1488ba367d437b728556f35a
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Thu Apr 30 06:17:28 2015 -0700

    service dependencies: cust_svc_unprovision_cascade, RT#33685

diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index 2cf9103..5922e32 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -118,8 +118,42 @@ sub delete {
   my $cust_pkg = $self->cust_pkg;
   my $custnum = $cust_pkg->custnum if $cust_pkg;
 
+  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;
+
   my $error = $self->SUPER::delete;
-  return $error if $error;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  foreach my $part_svc_link ( $self->part_svc_link(
+                                link_type   => 'cust_svc_unprovision_cascade',
+                              )
+  ) {
+    foreach my $cust_svc ( qsearch( 'cust_svc', {
+                             'pkgnum'  => $self->pkgnum,
+                             'svcpart' => $part_svc_link->dst_svcpart,
+                           })
+    ) {
+      my $error = $cust_svc->svc_x->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   if ( $ticket_system eq 'RT_Internal' ) {
     unless ( $rt_session ) {
@@ -144,6 +178,9 @@ sub delete {
       warn "error unlinking ticket $svcnum: $msg\n" if !$val;
     }
   }
+
+  '';
+
 }
 
 =item cancel

commit 8c974f743b0312b5dc9ef26f403b937abb5003d3
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Thu Apr 30 06:10:11 2015 -0700

    service dependencies: cust_svc_unprovision_restrict, RT#33685

diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index a7aeada..2cf9103 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -456,6 +456,35 @@ sub check {
   $self->SUPER::check;
 }
 
+=item check_part_svc_link_unprovision
+
+Checks service dependency unprovision rules for this service.
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub check_part_svc_link_unprovision {
+  my $self = shift;
+
+  foreach my $part_svc_link ( $self->part_svc_link(
+                                link_type   => 'cust_svc_unprovision_restrict',
+                              )
+  ) {
+    return $part_svc_link->dst_svc. ' must be unprovisioned before '.
+           $part_svc_link->src_svc
+      if qsearchs({
+        'table'    => 'cust_svc',
+        'hashref'  => { 'pkgnum'  => $self->pkgnum,
+                        'svcpart' => $part_svc_link->dst_svcpart,
+                      },
+        'order_by' => 'LIMIT 1',
+      });
+  }
+
+  '';
+}
+
 =item part_svc_link
 
 Returns the service dependencies (see L<FS::part_svc_link>) for the given
diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm
index b1f9d14..8d3b535 100644
--- a/FS/FS/svc_Common.pm
+++ b/FS/FS/svc_Common.pm
@@ -395,7 +395,8 @@ sub delete {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  my $error = 	$self->predelete_hook_first 
+  my $error =    $self->cust_svc->check_part_svc_link_unprovision
+              || $self->predelete_hook_first 
 	      || $self->SUPER::delete
               || $self->export('delete', @$export_args)
 	      || $self->return_inventory

commit 47cb646c89d4a798d35063f04db39c707eac4f4c
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Thu Apr 30 05:34:17 2015 -0700

    service dependencies: cust_svc_provision_restrict, RT#33685

diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index 96409c3..a7aeada 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -11,6 +11,7 @@ use FS::Record qw( qsearch qsearchs dbh str2time_sql str2time_sql_closing );
 use FS::part_pkg;
 use FS::part_svc;
 use FS::pkg_svc;
+use FS::part_svc_link;
 use FS::domain_record;
 use FS::part_export;
 use FS::cdr;
@@ -431,11 +432,47 @@ sub check {
            " services for pkgnum ". $self->pkgnum
       if $num_avail <= 0;
 
+    #part_svc_link rules (only make sense in pkgpart context, and 
+    # skipping this when ignore_quantity is set DTRT when we're "forcing"
+    # an implicit change here (location change triggered pkgpart change, 
+    # ->overlimit, bulk customer service changes)
+    foreach my $part_svc_link ( $self->part_svc_link(
+                                  link_type   => 'cust_svc_provision_restrict',
+                                )
+    ) {
+      return $part_svc_link->dst_svc. ' must be provisioned before '.
+             $part_svc_link->src_svc
+        unless qsearchs({
+          'table'    => 'cust_svc',
+          'hashref'  => { 'pkgnum'  => $self->pkgnum,
+                          'svcpart' => $part_svc_link->dst_svcpart,
+                        },
+          'order_by' => 'LIMIT 1',
+        });
+    }
+
   }
 
   $self->SUPER::check;
 }
 
+=item part_svc_link
+
+Returns the service dependencies (see L<FS::part_svc_link>) for the given
+search options, taking into account this service definition as source and
+this customer's agent.
+
+Available options are any field in part_svc_link.  Typically used options are
+link_type.
+
+=cut
+
+sub part_svc_link {
+  my $self = shift;
+  my $agentnum = $self->pkgnum ? $self->cust_pkg->cust_main->agentnum : '';
+  FS::part_svc_link->by_agentnum($agentnum, src_svcpart=>$self->svcpart, @_);
+}
+
 =item display_svcnum 
 
 Returns the displayed service number for this service: agent_svcid if it has a
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index e473d09..4407ec6 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -797,17 +797,7 @@ src_svcpart and link_type.
 =cut
 
 sub part_svc_link {
-  my( $self, %opt ) = @_;
-
-  my $agentnum = $self->agentnum;
-
-  qsearch({ 'table'     => 'part_svc_link',
-            'hashref'   => \%opt,
-            'extra_sql' =>
-              $agentnum
-                ? "AND ( agentnum IS NULL OR agentnum = $agentnum )"
-                : 'AND agentnum IS NULL',
-         });
+  FS::part_svc_link->by_agentnum( shift->agentnum, @_ );
 }
 
 =item supersede OLD [, OPTION => VALUE ... ]
diff --git a/FS/FS/part_svc_link.pm b/FS/FS/part_svc_link.pm
index af70d8f..a7f1b0f 100644
--- a/FS/FS/part_svc_link.pm
+++ b/FS/FS/part_svc_link.pm
@@ -2,7 +2,7 @@ package FS::part_svc_link;
 use base qw( FS::Record );
 
 use strict;
-use FS::Record qw( qsearchs ); # qw( qsearch qsearchs );
+use FS::Record qw( qsearch qsearchs );
 
 =head1 NAME
 
@@ -107,10 +107,31 @@ points to.  You can ask the object for a copy with the I<hash> method.
 
 =cut
 
-# the new method can be inherited from FS::Record, if a table method is defined
-
 sub table { 'part_svc_link'; }
 
+=item by_agentnum AGENTNUM, KEY => VALUE, ...
+
+Alternate search consructor.  Given an agentnum then a list of keys and values,
+searches for part_svc_link records with the given agentnum (or no agentnum).
+
+Additional keys and values are searched for in the part_pkg_link table
+(typically src_svcpart and link_type).
+
+=cut
+
+sub by_agentnum {
+  my( $class, $agentnum, %opt ) = @_;
+
+  qsearch({ 'table'     => 'part_svc_link', #$class->table,
+            'hashref'   => \%opt,
+            'extra_sql' =>
+              $agentnum
+                ? "AND ( agentnum IS NULL OR agentnum = $agentnum )"
+                : 'AND agentnum IS NULL',
+         });
+
+}
+
 =item insert
 
 Adds this record to the database.  If there is an error, returns the error,
@@ -166,26 +187,28 @@ sub description {
   #  (and hooks each place we have manual checks for the various rules)
   # but this will do for now
 
-  $self->link_type eq 'part_pkg_restrict'
+  my $l = $self->link_type;
+
+  $l eq 'part_pkg_restrict'
    and return "In package definitions, $dst is required when $src is included";
 
-  $self->link_type eq 'part_pkg_restrict_soft'
+  $l eq 'part_pkg_restrict_soft'
    and return "In package definitions, $dst is suggested when $src is included";
 
-  $self->link_type eq 'cust_svc_provision_restrict'
+  $l eq 'cust_svc_provision_restrict'
    and return "Require $dst provisioning before $src";
 
-  $self->link_type eq 'cust_svc_unprovision_restrict'
+  $l eq 'cust_svc_unprovision_restrict'
    and return "Require $dst unprovisioning before $src";
 
-  $self->link_type eq 'cust_svc_unprovision_cascade'
+  $l eq 'cust_svc_unprovision_cascade'
    and return "Automatically unprovision $dst when $src is unprovisioned";
 
-  $self->link_type eq 'cust_svc_suspend_cascade'
+  $l eq 'cust_svc_suspend_cascade'
    and return "Suspend $dst before $src";
 
-  warn "WARNING: unknown part_svc_link.link_type ". $self->link_type. "\n";
-  return "$src (unknown link_type ". $self->link_type. ") $dst";
+  warn "WARNING: unknown part_svc_link.link_type $l\n";
+  return "$src (unknown link_type $l) $dst";
 
 }
 

commit 3ef3c5061e6de2dd885987e844ea3122aafc4df6
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Thu Apr 30 04:30:20 2015 -0700

    service dependencies: part_pkg_restrict / part_pkg_restrict_soft, RT#33685

diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 9d024b6..e473d09 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -30,6 +30,7 @@ use FS::part_pkg_link;
 use FS::part_pkg_discount;
 use FS::part_pkg_vendor;
 use FS::part_pkg_currency;
+use FS::part_svc_link;
 
 $DEBUG = 0;
 $setup_hack = 0;

commit b83ef1787970f9117a540cfa707ccd2fdcde1175
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Thu Apr 30 04:13:17 2015 -0700

    service dependencies: part_pkg_restrict / part_pkg_restrict_soft, RT#33685

diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 2fba2f4..9d024b6 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -753,17 +753,12 @@ sub check_pkg_svc {
 
   foreach my $svcpart ( keys %pkg_svc ) {
 
-    warn 'checking '. $pkg_svc{$svcpart}->svcpart;
-
     foreach my $part_svc_link ( $self->part_svc_link(
                                   'src_svcpart' => $svcpart,
                                   'link_type'   => 'part_pkg_restrict',
                                 )
     ) {
 
-      use Data::Dumper;
-      warn 'checking '. Dumper($part_svc_link);
-
       return $part_svc_link->dst_svc. ' must be included with '.
              $part_svc_link->src_svc
         unless $pkg_svc{ $part_svc_link->dst_svcpart };

commit 76e217e6a453518ae9aef2b63fae49629101470c
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Thu Apr 30 04:01:54 2015 -0700

    doc

diff --git a/FS/FS/part_pkg_vendor.pm b/FS/FS/part_pkg_vendor.pm
index bb37f1b..4629bad 100644
--- a/FS/FS/part_pkg_vendor.pm
+++ b/FS/FS/part_pkg_vendor.pm
@@ -55,7 +55,7 @@ vendor_pkg_id
 
 =item new HASHREF
 
-Creates a new example.  To add the example to the database, see L<"insert">.
+Creates a new record.  To add the record to the database, see L<"insert">.
 
 Note that this stores the hash reference, not a distinct copy of the hash it
 points to.  You can ask the object for a copy with the I<hash> method.
@@ -94,7 +94,7 @@ returns the error, otherwise returns false.
 
 =item check
 
-Checks all fields to make sure this is a valid example.  If there is
+Checks all fields to make sure this is a valid record.  If there is
 an error, returns the error, otherwise returns false.  Called by the insert
 and replace methods.
 

commit 67b56f17cc51d10394a986fb3d105213b097ee79
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Thu Apr 30 04:01:21 2015 -0700

    service dependencies: part_pkg_restrict / part_pkg_restrict_soft, RT#33685

diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 114acb8..93220ad 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -3630,7 +3630,7 @@ sub tables_hashref {
       ],
       'primary_key'  => 'svclinknum',
       'unique'       => [ ['agentnum','src_svcpart','dst_svcpart','link_type'] ],
-      'index'        => [ [ 'src_svcpart' ] ],
+      'index'        => [ [ 'src_svcpart' ], [ 'src_svcpart', 'link_type' ], [ 'disabled' ] ],
       'foreign_keys' => [
                           { columns    => [ 'src_svcpart' ],
                             table      => 'part_svc',
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 540919e..2fba2f4 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -301,6 +301,12 @@ sub insert {
       }
     }
 
+    my $error = $self->check_pkg_svc(%options);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+
   }
 
   if ( $options{'cust_pkg'} ) {
@@ -514,6 +520,7 @@ sub replace {
   my $hidden_svc = $options->{'hidden_svc'} || {};
   my $bulk_skip  = $options->{'bulk_skip'} || {};
   if ( $pkg_svc ) { # if it wasn't passed, don't change existing pkg_svcs
+
     foreach my $part_svc ( qsearch('part_svc', {} ) ) {
       my $quantity  = $pkg_svc->{$part_svc->svcpart} || 0;
       my $hidden    = $hidden_svc->{$part_svc->svcpart} || '';
@@ -564,6 +571,13 @@ sub replace {
         return $error;
       }
     } #foreach $part_svc
+
+    my $error = $new->check_pkg_svc(%$options);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+
   } #if $options->{pkg_svc}
   
   my @part_pkg_vendor = $old->part_pkg_vendor;
@@ -722,6 +736,84 @@ sub check {
   '';
 }
 
+=item check_pkg_svc
+
+Checks pkg_svc records as a whole (for part_svc_link dependencies).
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub check_pkg_svc {
+  my( $self, %opt ) = @_;
+
+  my $agentnum = $self->agentnum;
+
+  my %pkg_svc = map { $_->svcpart => $_ } $self->pkg_svc;
+
+  foreach my $svcpart ( keys %pkg_svc ) {
+
+    warn 'checking '. $pkg_svc{$svcpart}->svcpart;
+
+    foreach my $part_svc_link ( $self->part_svc_link(
+                                  'src_svcpart' => $svcpart,
+                                  'link_type'   => 'part_pkg_restrict',
+                                )
+    ) {
+
+      use Data::Dumper;
+      warn 'checking '. Dumper($part_svc_link);
+
+      return $part_svc_link->dst_svc. ' must be included with '.
+             $part_svc_link->src_svc
+        unless $pkg_svc{ $part_svc_link->dst_svcpart };
+    }
+
+  }
+
+  return '' if $opt{part_pkg_restrict_soft_override};
+
+  foreach my $svcpart ( keys %pkg_svc ) {
+
+    foreach my $part_svc_link ( $self->part_svc_link(
+                                  'src_svcpart' => $svcpart,
+                                  'link_type'   => 'part_pkg_restrict_soft',
+                                )
+    ) {
+      return $part_svc_link->dst_svc. ' is suggested with '.
+             $part_svc_link->src_svc
+        unless $pkg_svc{ $part_svc_link->dst_svcpart };
+    }
+
+  }
+
+  '';
+}
+
+=item part_svc_link OPTION => VALUE ...
+
+Returns the service dependencies (see L<FS::part_svc_link>) for the given
+search options, taking into account this package definition's agent.
+
+Available options are any field in part_svc_link.  Typically used options are
+src_svcpart and link_type.
+
+=cut
+
+sub part_svc_link {
+  my( $self, %opt ) = @_;
+
+  my $agentnum = $self->agentnum;
+
+  qsearch({ 'table'     => 'part_svc_link',
+            'hashref'   => \%opt,
+            'extra_sql' =>
+              $agentnum
+                ? "AND ( agentnum IS NULL OR agentnum = $agentnum )"
+                : 'AND agentnum IS NULL',
+         });
+}
+
 =item supersede OLD [, OPTION => VALUE ... ]
 
 Inserts this package as a successor to the package OLD.  All options are as
diff --git a/FS/FS/part_svc_link.pm b/FS/FS/part_svc_link.pm
index cf82a90..af70d8f 100644
--- a/FS/FS/part_svc_link.pm
+++ b/FS/FS/part_svc_link.pm
@@ -64,15 +64,14 @@ Link type:
 
 # XXX false laziness w/edit/part_svc_link.html
 
-=item part_svc_restrict
+=item part_pkg_restrict
 
 In package defintions, require the destination service definition when the
 source service definition is included
 
-=item part_svc_restrict_soft
+=item part_pkg_restrict_soft
 
-Soft order block: in package definitions, warn if the destination service
-definition is included without the source service definition
+Soft order block: in package definitions,  suggest the destination service definition when the source service definition is included
 
 =item cust_svc_provision_restrict
 
@@ -167,10 +166,10 @@ sub description {
   #  (and hooks each place we have manual checks for the various rules)
   # but this will do for now
 
-  $self->link_type eq 'part_svc_restrict'
+  $self->link_type eq 'part_pkg_restrict'
    and return "In package definitions, $dst is required when $src is included";
 
-  $self->link_type eq 'part_svc_restrict_soft'
+  $self->link_type eq 'part_pkg_restrict_soft'
    and return "In package definitions, $dst is suggested when $src is included";
 
   $self->link_type eq 'cust_svc_provision_restrict'
@@ -219,7 +218,6 @@ L<FS::part_svc>).
 
 =cut
 
-
 sub dst_part_svc {
   my $self = shift;
   qsearchs('part_svc', { svcpart=>$self->dst_svcpart } );
@@ -232,7 +230,7 @@ Returns the destination service definition name (part_svc.svc).
 =cut
 
 sub dst_svc {
-  shift->src_part_svc->svc;
+  shift->dst_part_svc->svc;
 }
 
 =back
diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html
index 5a7920b..76df820 100644
--- a/httemplate/edit/elements/edit.html
+++ b/httemplate/edit/elements/edit.html
@@ -132,6 +132,9 @@ Example:
 
     'html_init'   => '', #after the header/menubar
 
+    'form_init'   => '', #after html_init, error and the opening <FORM>, but
+                         #before any other form contents
+
     #string or coderef of additional HTML to add before </TABLE>
     'html_table_bottom' => '',
 
@@ -244,6 +247,14 @@ Example:
   <INPUT TYPE="hidden" NAME="svcdb" VALUE="<% $table %>">
   <INPUT TYPE="hidden" NAME="<% $pkey %>" VALUE="<% $clone ? '' : $object->$pkey() %>">
 
+  <% defined($opt{'form_init'}) 
+        ? ( ref($opt{'form_init'})
+              ? &{$opt{'form_init'}}()
+              : $opt{'form_init'}
+          )
+        : ''
+  %>
+
 %   unless ( $opt{'no_pkey_display'} ) {
 
       <FONT SIZE="+1"><B>
diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi
index 6629407..fbc19c3 100755
--- a/httemplate/edit/part_pkg.cgi
+++ b/httemplate/edit/part_pkg.cgi
@@ -559,6 +559,11 @@ my $error_callback = sub {
     'cgiparam'
   );
 
+  if ( $cgi->param('error') =~ / is suggested with / ) {
+    #yeah, detection is a shitty kludge, but we don't have exception objects
+    $opt->{form_init} = '<INPUT TYPE="checkbox" NAME="part_pkg_restrict_soft_override" VALUE="Y"> Override suggestion<BR><BR>';
+  }
+
 };
 
 my $new_hashref_callback = sub { { 'plan' => 'flat' }; };
diff --git a/httemplate/edit/part_svc_link.html b/httemplate/edit/part_svc_link.html
index 64a99d6..c8c385d 100644
--- a/httemplate/edit/part_svc_link.html
+++ b/httemplate/edit/part_svc_link.html
@@ -25,13 +25,13 @@ my @fields = (
     type        => 'select',
     #XXX false laziness w/part_svc_link POD documentation
     options     =>[ qw(
-      part_svc_restrict part_svc_restrict_soft
+      part_pkg_restrict part_pkg_restrict_soft
       cust_svc_provision_restrict cust_svc_unprovision_restrict
       cust_svc_unprovision_cascade cust_svc_suspend_cascade
     )],
     labels      => {
-      part_svc_restrict => 'In package defintions, prevent including the destination service definition unless the source service definition is also included',
-      part_svc_restrict_soft => 'Soft order block: in package definitions, warn if the destination service definition is included without the source service definition',
+      part_pkg_restrict => 'In package defintions, require the destination service definition when the source service definition is included',
+      part_pkg_restrict_soft => 'In package definitions, suggest the destination service definition when the source service definition is included',
       cust_svc_provision_restrict => 'Require the target service to be provisioned before the source service',
       cust_svc_unprovision_restrict => 'Require the target service to be unprovisioned before the source service',
       cust_svc_unprovision_cascade => 'Automatically unprovision the target service when the source service is unprovisioned',
diff --git a/httemplate/edit/process/part_pkg.cgi b/httemplate/edit/process/part_pkg.cgi
index 0343cc0..eda3f33 100755
--- a/httemplate/edit/process/part_pkg.cgi
+++ b/httemplate/edit/process/part_pkg.cgi
@@ -188,6 +188,9 @@ my $args_callback = sub {
     push @args, 'part_pkg_vendor' => \%part_pkg_vendor;
   }
 
+  push @args, 'part_pkg_restrict_soft_override' => 1
+    if $cgi->param('part_pkg_restrict_soft_override');
+
   #warn "args: ".join('/', @args). "\n";
 
   @args;

commit 7d34aacffa38c4cac09b54080487a66c264e4668
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Thu Apr 30 02:34:51 2015 -0700

    service dependencies: UI, RT#33685

diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index d9b9188..78779d7 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -401,6 +401,7 @@ if ( -e $addl_handler_use_file ) {
   use FS::legacy_cust_history;
   use FS::quotation_pkg_tax;
   use FS::cust_pkg_reason_fee;
+  use FS::part_svc_link;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 42122f7..114acb8 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -3618,6 +3618,31 @@ sub tables_hashref {
       'index' => [ ['disabled'] ],
     },
 
+    'part_svc_link' => {
+      'columns' => [
+        'svclinknum',  'serial',   '',      '', '', '',
+        #'linkname',    'varchar', 'NULL', $char_d, '', '',
+        'agentnum',    'int',     'NULL', '', '', '', 
+        'src_svcpart', 'int',      '',      '', '', '',
+        'dst_svcpart', 'int',      '',      '', '', '', 
+        'link_type',   'varchar',  '', $char_d, '', '',
+        'disabled',    'char', 'NULL',   1, '', '', 
+      ],
+      'primary_key'  => 'svclinknum',
+      'unique'       => [ ['agentnum','src_svcpart','dst_svcpart','link_type'] ],
+      'index'        => [ [ 'src_svcpart' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'src_svcpart' ],
+                            table      => 'part_svc',
+                            references => [ 'svcpart' ]
+                          },
+                          { columns    => [ 'dst_svcpart' ],
+                            table      => 'part_svc',
+                            references => [ 'svcpart' ]
+                          },
+                        ],
+    },
+
     #(this should be renamed to part_pop)
     'svc_acct_pop' => {
       'columns' => [
diff --git a/FS/FS/part_svc_link.pm b/FS/FS/part_svc_link.pm
new file mode 100644
index 0000000..cf82a90
--- /dev/null
+++ b/FS/FS/part_svc_link.pm
@@ -0,0 +1,249 @@
+package FS::part_svc_link;
+use base qw( FS::Record );
+
+use strict;
+use FS::Record qw( qsearchs ); # qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::part_svc_link - Object methods for part_svc_link records
+
+=head1 SYNOPSIS
+
+  use FS::part_svc_link;
+
+  $record = new FS::part_svc_link \%hash;
+  $record = new FS::part_svc_link { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_svc_link object represents an service definition dependency.
+FS::part_svc_link inherits from FS::Record.  The following fields are currently
+supported:
+
+=over 4
+
+=item svclinknum
+
+primary key
+
+=cut
+
+#=item linkname
+#
+#Dependency name
+
+=item agentnum
+
+Empty for global dependencies, or agentnum (see L<FS::agent>) for
+agent-specific dependencies
+
+=item src_svcpart
+
+Source service definition (see L<FS::part_svc>)
+
+=item dst_svcpart
+
+Destination service definition (see L<FS::part_svc>)
+
+=item link_type
+
+Link type:
+
+=over 4
+
+=cut
+
+# XXX false laziness w/edit/part_svc_link.html
+
+=item part_svc_restrict
+
+In package defintions, require the destination service definition when the
+source service definition is included
+
+=item part_svc_restrict_soft
+
+Soft order block: in package definitions, warn if the destination service
+definition is included without the source service definition
+
+=item cust_svc_provision_restrict
+
+Require the destination service to be provisioned before the source service
+
+=item cust_svc_unprovision_restrict
+
+Require the destination service to be unprovisioned before the source service
+
+=item cust_svc_unprovision_cascade
+
+Automatically unprovision the destination service when the source service is
+unprovisioned
+
+=item cust_svc_suspend_cascade
+
+Suspend the destination service before the source service
+
+=back
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'part_svc_link'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('svclinknum')
+    #|| $self->ut_textn('linkname')
+    || $self->ut_number('src_svcpart')
+    || $self->ut_number('dst_svcpart')
+    || $self->ut_text('link_type')
+    || $self->ut_enum('disabled', [ '', 'Y' ])
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=item description
+
+Returns an extended description of this dependency, including.  Exact wording
+depends on I<link_type>.
+
+=cut
+
+sub description {
+  my $self = shift;
+
+  my $src = $self->src_part_svc->svc;
+  my $dst = $self->dst_part_svc->svc;
+
+  #maybe sub-classes with overrides at some point
+  #  (and hooks each place we have manual checks for the various rules)
+  # but this will do for now
+
+  $self->link_type eq 'part_svc_restrict'
+   and return "In package definitions, $dst is required when $src is included";
+
+  $self->link_type eq 'part_svc_restrict_soft'
+   and return "In package definitions, $dst is suggested when $src is included";
+
+  $self->link_type eq 'cust_svc_provision_restrict'
+   and return "Require $dst provisioning before $src";
+
+  $self->link_type eq 'cust_svc_unprovision_restrict'
+   and return "Require $dst unprovisioning before $src";
+
+  $self->link_type eq 'cust_svc_unprovision_cascade'
+   and return "Automatically unprovision $dst when $src is unprovisioned";
+
+  $self->link_type eq 'cust_svc_suspend_cascade'
+   and return "Suspend $dst before $src";
+
+  warn "WARNING: unknown part_svc_link.link_type ". $self->link_type. "\n";
+  return "$src (unknown link_type ". $self->link_type. ") $dst";
+
+}
+
+=item src_part_svc 
+
+Returns the source service definition, as an FS::part_svc object (see
+L<FS::part_svc>).
+
+=cut
+
+sub src_part_svc {
+  my $self = shift;
+  qsearchs('part_svc', { svcpart=>$self->src_svcpart } );
+}
+
+=item src_svc
+
+Returns the source service definition name (part_svc.svc).
+
+=cut
+
+sub src_svc {
+  shift->src_part_svc->svc;
+}
+
+=item dst_part_svc
+
+Returns the destination service definition, as an FS::part_svc object (see
+L<FS::part_svc>).
+
+=cut
+
+
+sub dst_part_svc {
+  my $self = shift;
+  qsearchs('part_svc', { svcpart=>$self->dst_svcpart } );
+}
+
+=item dst_svc
+
+Returns the destination service definition name (part_svc.svc).
+
+=cut
+
+sub dst_svc {
+  shift->src_part_svc->svc;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::part_svc>, L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 575184c..422f69c 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -794,7 +794,7 @@ t/cust_bill_pkg_fee.t
 FS/part_fee_msgcat.pm
 t/part_fee_msgcat.t
 FS/part_fee_usage.pm
-FS/part_fee_usage.t
+t/part_fee_usage.t
 FS/sched_item.pm
 t/sched_item.t
 FS/sched_avail.pm
@@ -841,7 +841,8 @@ t/legacy_cust_history.t
 FS/quotation_pkg_tax.pm
 t/quotation_pkg_tax.t
 FS/h_svc_circuit.pm
-FS/h_svc_circuit.t
 FS/FeeOrigin_Mixin.pm
 FS/cust_pkg_reason_fee.pm
 t/cust_pkg_reason_fee.t
+FS/part_svc_link.pm
+t/part_svc_link.t
diff --git a/FS/t/part_svc_link.t b/FS/t/part_svc_link.t
new file mode 100644
index 0000000..3cac9ef
--- /dev/null
+++ b/FS/t/part_svc_link.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_svc_link;
+$loaded=1;
+print "ok 1\n";
diff --git a/httemplate/browse/part_svc_link.html b/httemplate/browse/part_svc_link.html
new file mode 100644
index 0000000..d31acc6
--- /dev/null
+++ b/httemplate/browse/part_svc_link.html
@@ -0,0 +1,32 @@
+<& elements/browse.html,
+     'title'              => 'Service dependencies',
+     'name_singular'      => 'dependency',
+     'menubar'            => [ 'Add a new service dependency' =>
+                                 $p.'edit/part_svc_link.html',
+                             ],
+     'query'              => { 'table'    => 'part_svc_link',
+                               'order_by' => 'ORDER BY src_svcpart',
+                             },
+     'count_query'        => 'SELECT COUNT(*) FROM part_svc_link',
+     'header'             => [ 'Source', 'Dependency', ],
+     'fields'             => [ 'src_svc', 'description', ],
+     'sort_fields'        => [],
+     'links'              => [ $svc_link, $link, ],
+     'disableable'        => 1,
+     'disabled_statuspos' => 1,
+     'agent_virt'         => 1,
+     'agent_null'         => 1,
+     'agent_null_right'   => 'Configuration',
+     #agent_null_right_link'
+     'agent_pos'          => 0,
+&>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $svc_link = [ "${p}edit/part_svc.cgi?", 'src_svcpart' ];
+
+my $link = [ "${p}edit/part_svc_link.html?", 'svclinknum' ];
+
+</%init>
diff --git a/httemplate/edit/elements/export_svc.html b/httemplate/edit/elements/export_svc.html
index 5962ae7..1735148 100644
--- a/httemplate/edit/elements/export_svc.html
+++ b/httemplate/edit/elements/export_svc.html
@@ -39,8 +39,8 @@ function toggle_selectrole() {
   }
 </&>
 </script>
-<& /elements/table.html &>
-  <TR><TH COLSPAN=<% $columns %>>Exports</TH></TR>
+<FONT CLASS="fsinnerbox-title">Exports</FONT>
+<TABLE CLASS="fsinnerbox">
   <TR>
 % # exports
 % foreach my $part_export (@part_export) {
@@ -81,4 +81,4 @@ function toggle_selectrole() {
 %   }
 % }
   </TR>
-</TABLE><BR><BR>
+</TABLE><BR>
diff --git a/httemplate/edit/elements/part_svc_column.html b/httemplate/edit/elements/part_svc_column.html
index a6ccaf8..23a6deb 100644
--- a/httemplate/edit/elements/part_svc_column.html
+++ b/httemplate/edit/elements/part_svc_column.html
@@ -63,15 +63,14 @@ my %communigate_fields = (
 );
 </%once>
 <INPUT TYPE="hidden" NAME="svcdb" VALUE="<% $svcdb %>">
-<BR><BR>
+<BR>
 %# include export selection
 <& export_svc.html,
   part_svc => $part_svc,
   svcdb => $svcdb
 &>
-For the selected table, you can give fields default or fixed (unchangeable)
-values, or select an inventory class to manually or automatically fill in 
-that field.
+
+<FONT CLASS="fsinnerbox-title">Fields</FONT>
 <& /elements/table-grid.html, cellpadding => 4 &>
   <TR>
     <TH BGCOLOR="#cccccc">Field</TH>
diff --git a/httemplate/edit/part_svc.cgi b/httemplate/edit/part_svc.cgi
index 7a47f15..a07fc60 100755
--- a/httemplate/edit/part_svc.cgi
+++ b/httemplate/edit/part_svc.cgi
@@ -187,10 +187,9 @@ window.onload = function() {
 
 <BR>
 
-<BR>
-Table <% $widget->html %>
+<FONT SIZE="+1"><B>Table</B></FONT> <% $widget->html %>
 
-<% include('/elements/footer.html') %>
+<& /elements/footer.html &>
 
 <%init>
 
diff --git a/httemplate/edit/part_svc_link.html b/httemplate/edit/part_svc_link.html
new file mode 100644
index 0000000..64a99d6
--- /dev/null
+++ b/httemplate/edit/part_svc_link.html
@@ -0,0 +1,51 @@
+<& elements/edit.html,
+  'table'         => 'part_svc_link',
+  'name_singular' => 'dependency',
+  'labels'        => \%labels,
+  'fields'        => \@fields,
+&>
+<%init>
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my @fields = (
+  { field       => 'agentnum',
+    type        => 'select-agent',
+    empty_label => '(global)',
+  },
+  { field       => 'src_svcpart',
+    type        => 'select-part_svc',
+    empty_label => 'Select service definition',
+  },
+  { field       => 'dst_svcpart',
+    type        => 'select-part_svc',
+    empty_label => 'Select service definition',
+  },
+  { field       => 'link_type',
+    type        => 'select',
+    #XXX false laziness w/part_svc_link POD documentation
+    options     =>[ qw(
+      part_svc_restrict part_svc_restrict_soft
+      cust_svc_provision_restrict cust_svc_unprovision_restrict
+      cust_svc_unprovision_cascade cust_svc_suspend_cascade
+    )],
+    labels      => {
+      part_svc_restrict => 'In package defintions, prevent including the destination service definition unless the source service definition is also included',
+      part_svc_restrict_soft => 'Soft order block: in package definitions, warn if the destination service definition is included without the source service definition',
+      cust_svc_provision_restrict => 'Require the target service to be provisioned before the source service',
+      cust_svc_unprovision_restrict => 'Require the target service to be unprovisioned before the source service',
+      cust_svc_unprovision_cascade => 'Automatically unprovision the target service when the source service is unprovisioned',
+      cust_svc_suspend_cascade => 'Suspend the target service before the source service',
+    },
+  },
+  { field => 'disabled', type => 'checkbox', value => 'Y' }
+);
+my %labels = (
+  'svclinknum ' => '',
+  'agentnum'    => 'Agent',
+  'src_svcpart' => 'Source service',
+  'dst_svcpart' => 'Destination service',
+  'link_type'   => 'Dependency type',
+  'disabled'    => 'Disabled'
+);
+</%init>
diff --git a/httemplate/edit/process/part_svc_link.html b/httemplate/edit/process/part_svc_link.html
new file mode 100644
index 0000000..ffe979d
--- /dev/null
+++ b/httemplate/edit/process/part_svc_link.html
@@ -0,0 +1,5 @@
+<& elements/process.html, table=>'part_svc_link', viewall_dir=>'browse' &>
+<%init>
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+</%init>
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index 0aefcd7..e152cb6 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -577,6 +577,7 @@ tie my %config_export_svc, 'Tie::IxHash', ();
 if ( $curuser->access_right('Configuration') ) {
   $config_export_svc{'Service definitions'} = [ $fsurl.'browse/part_svc.cgi', 'Services are items you offer to your customers' ];
   $config_export_svc{'Service classes'} = [ $fsurl.'browse/part_svc_class.html', 'Services classes are user-defined, informational types for services' ];
+  $config_export_svc{'Service dependencies'} = [ $fsurl.'browse/part_svc_link.html', 'Services depencies define rules between service definitions' ];
   $config_export_svc{'Provisioning exports'} = [ $fsurl.'browse/part_export.cgi', 'Provisioning services to external machines, databases and APIs' ];
 }
 $config_export_svc{'Dialup'}  = [ \%config_dialup, ''    ]

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Mason.pm                                     |    1 +
 FS/FS/Schema.pm                                    |   25 ++
 FS/FS/cust_pkg.pm                                  |   49 ++--
 FS/FS/cust_svc.pm                                  |  136 +++++++++-
 FS/FS/part_pkg.pm                                  |   78 ++++++
 FS/FS/part_pkg_vendor.pm                           |    4 +-
 FS/FS/part_svc_link.pm                             |  270 ++++++++++++++++++++
 FS/FS/svc_Common.pm                                |    3 +-
 FS/MANIFEST                                        |    5 +-
 FS/t/{AccessRight.t => part_svc_link.t}            |    2 +-
 httemplate/browse/part_svc_link.html               |   32 +++
 httemplate/edit/elements/edit.html                 |   11 +
 httemplate/edit/elements/export_svc.html           |    6 +-
 httemplate/edit/elements/part_svc_column.html      |    7 +-
 httemplate/edit/part_pkg.cgi                       |    5 +
 httemplate/edit/part_svc.cgi                       |    5 +-
 httemplate/edit/part_svc_link.html                 |   51 ++++
 httemplate/edit/process/part_pkg.cgi               |    3 +
 .../edit/process/{rate.cgi => part_svc_link.html}  |    6 +-
 httemplate/elements/menu.html                      |    1 +
 20 files changed, 655 insertions(+), 45 deletions(-)
 create mode 100644 FS/FS/part_svc_link.pm
 copy FS/t/{AccessRight.t => part_svc_link.t} (81%)
 create mode 100644 httemplate/browse/part_svc_link.html
 create mode 100644 httemplate/edit/part_svc_link.html
 copy httemplate/edit/process/{rate.cgi => part_svc_link.html} (54%)
 mode change 100755 => 100644




More information about the freeside-commits mailing list