Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorIvan Kohler <ivan@freeside.biz>
Mon, 27 Apr 2015 09:59:21 +0000 (02:59 -0700)
committerIvan Kohler <ivan@freeside.biz>
Mon, 27 Apr 2015 09:59:21 +0000 (02:59 -0700)
24 files changed:
FS/FS/ClientAPI/MasonComponent.pm
FS/FS/ClientAPI/MyAccount.pm
FS/FS/ClientAPI/MyAccount/quotation.pm [new file with mode: 0644]
FS/FS/ClientAPI_XMLRPC.pm
FS/FS/Conf.pm
FS/FS/Schema.pm
FS/FS/part_pkg/flat.pm
FS/FS/part_svc.pm
FS/FS/part_svc_column.pm
FS/FS/quotation.pm
FS/FS/svc_Common.pm
FS/FS/svc_acct.pm
FS/FS/svc_domain.pm
fs_selfservice/FS-SelfService/SelfService.pm
httemplate/browse/part_svc.cgi
httemplate/edit/elements/part_svc_column.html
httemplate/edit/part_svc.cgi
httemplate/misc/payment.cgi
ng_selfservice/images/cross.png [new file with mode: 0644]
ng_selfservice/quotation.php [new file with mode: 0644]
ng_selfservice/quotation_add_pkg.php [new file with mode: 0644]
ng_selfservice/quotation_order.php [new file with mode: 0644]
ng_selfservice/quotation_print.php [new file with mode: 0644]
ng_selfservice/quotation_remove_pkg.php [new file with mode: 0644]

index 695b4ca..b6f8aa4 100644 (file)
@@ -27,6 +27,7 @@ my %allowed_comps = map { $_=>1 } qw(
 my %session_comps = map { $_=>1 } qw(
   /elements/location.html
   /elements/tr-amount_fee.html
+  /elements/select-part_pkg.html
   /edit/cust_main/first_pkg/select-part_pkg.html
 );
 
@@ -106,6 +107,26 @@ my %session_callbacks = (
 
   },
 
+  '/elements/select-part_pkg.html' => sub {
+    my( $custnum, $argsref ) = @_;
+    my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+      or return "unknown custnum $custnum";
+
+    my $pkgpart = $cust_main->agent->pkgpart_hashref;
+
+    #false laziness w/ edit/cust_main/first_pkg.html
+    my @first_svc = ( 'svc_acct', 'svc_phone' );
+
+    my @part_pkg =
+      grep { $pkgpart->{ $_->pkgpart } 
+                  || ( $_->agentnum && $_->agentnum == $cust_main->agentnum )
+           }
+      qsearch( 'part_pkg', { 'disabled' => '' }, '', 'ORDER BY pkg' ); # case?
+
+    push @$argsref, 'part_pkg' =>  \@part_pkg;
+    '';
+  },
+
 );
 
 my $outbuf;
index 93f817d..e2f8595 100644 (file)
@@ -49,6 +49,8 @@ use FS::contact;
 use FS::cust_contact;
 use FS::cust_location;
 
+use FS::ClientAPI::MyAccount::quotation; # just for code organization
+
 $DEBUG = 0;
 $me = '[FS::ClientAPI::MyAccount]';
 
diff --git a/FS/FS/ClientAPI/MyAccount/quotation.pm b/FS/FS/ClientAPI/MyAccount/quotation.pm
new file mode 100644 (file)
index 0000000..787a099
--- /dev/null
@@ -0,0 +1,220 @@
+package FS::ClientAPI::MyAccount::quotation;
+
+use strict;
+use FS::Record qw(qsearch qsearchs);
+use FS::quotation;
+use FS::quotation_pkg;
+
+our $DEBUG = 1;
+
+sub _custoragent_session_custnum {
+  FS::ClientAPI::MyAccount::_custoragent_session_custnum(@_);
+}
+
+sub _quotation {
+  # the currently active quotation
+  my $session = shift;
+  my $quotation;
+  if ( my $quotationnum = $session->{'quotationnum'} ) {
+    $quotation = FS::quotation->by_key($quotationnum);
+  } 
+  if ( !$quotation ) {
+    # find the last quotation created through selfservice
+    $quotation = qsearchs( 'quotation', {
+        'custnum'   => $session->{'custnum'},
+        'usernum'   => $FS::CurrentUser::CurrentUser->usernum,
+        'disabled'  => '',
+    }); 
+    warn "found selfservice quotation #". $quotation->quotationnum."\n"
+      if $quotation and $DEBUG;
+  } 
+  if ( !$quotation ) {
+    $quotation = FS::quotation->new({
+        'custnum'   => $session->{'custnum'},
+        'usernum'   => $FS::CurrentUser::CurrentUser->usernum,
+        '_date'     => time,
+    }); 
+    $quotation->insert; # what to do on error? call the police?
+    warn "started new selfservice quotation #". $quotation->quotationnum."\n"
+      if $quotation and $DEBUG;
+  } 
+  $session->{'quotationnum'} = $quotation->quotationnum;
+  return $quotation;
+}
+
+=item quotation_info { session }
+
+Returns a hashref describing the current quotation, containing:
+
+- "sections", an arrayref containing one section for each billing frequency.
+  Each one will have:
+  - "description"
+  - "subtotal"
+  - "detail_items", an arrayref of detail items, each with:
+    - "pkgnum", the reference number (actually the quotationpkgnum field)
+    - "description", the package name (or tax name)
+    - "quantity"
+    - "amount"
+
+=cut
+
+sub quotation_info {
+  my $p = shift;
+
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+
+  my $quotation = _quotation($session);
+  return { 'error' => "No current quotation for this customer" } if !$quotation;
+  warn "quotation_info #".$quotation->quotationnum
+    if $DEBUG;
+
+  # code reuse ftw
+  my $null_escape = sub { @_ };
+  my ($sections) = $quotation->_items_sections(escape => $null_escape);
+  foreach my $section (@$sections) {
+    $section->{'detail_items'} =
+      [ $quotation->_items_pkg('section' => $section, escape_function => $null_escape) ]; 
+  }
+  return { 'error' => '', 'sections' => $sections }
+}
+
+=item quotation_print { session, 'format' }
+
+Renders the quotation. 'format' can be either 'html' or 'pdf'; the resulting
+hashref will contain 'document' => the HTML or PDF contents.
+
+=cut
+
+sub quotation_print {
+  my $p = shift;
+
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+
+  my $quotation = _quotation($session);
+  return { 'error' => "No current quotation for this customer" } if !$quotation;
+  warn "quotation_print #".$quotation->quotationnum
+    if $DEBUG;
+
+  my $format = $p->{'format'}
+   or return { 'error' => "No rendering format specified" };
+
+  my $document;
+  if ($format eq 'html') {
+    $document = $quotation->print_html;
+  } elsif ($format eq 'pdf') {
+    $document = $quotation->print_pdf;
+  }
+  warn "$format, ".length($document)." bytes\n"
+    if $DEBUG;
+  return { 'error' => '', 'document' => $document };
+}
+
+=item quotation_add_pkg { session, 'pkgpart', 'quantity', [ location opts ] }
+
+Adds a package to the user's current quotation. Session info and 'pkgpart' are
+required. 'quantity' defaults to 1.
+
+Location can be specified as 'locationnum' to use an existing location, or
+'address1', 'address2', 'city', 'state', 'zip', 'country' to create a new one,
+or it will default to the customer's service location.
+
+=cut
+
+sub quotation_add_pkg {
+  my $p = shift;
+
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+  
+  my $quotation = _quotation($session);
+  my $cust_main = $quotation->cust_main;
+
+  my $pkgpart = $p->{'pkgpart'};
+  my $allowed_pkgpart = $cust_main->agent->pkgpart_hashref;
+
+  my $part_pkg = FS::part_pkg->by_key($pkgpart);
+
+  if (!$part_pkg or !$allowed_pkgpart->{$pkgpart}) {
+    warn "disallowed quotation_pkg pkgpart $pkgpart\n"
+      if $DEBUG;
+    return { 'error' => "unknown package $pkgpart" };
+  }
+
+  warn "creating quotation_pkg with pkgpart $pkgpart\n"
+    if $DEBUG;
+  my $quotation_pkg = FS::quotation_pkg->new({
+    'quotationnum'  => $quotation->quotationnum,
+    'pkgpart'       => $p->{'pkgpart'},
+    'quantity'      => $p->{'quantity'} || 1,
+  });
+  if ( $p->{locationnum} > 0 ) {
+    $quotation_pkg->set('locationnum', $p->{locationnum});
+  } elsif ( $p->{address1} ) {
+    my $location = FS::cust_location->find_or_insert(
+      'custnum' => $cust_main->custnum,
+      map { $_ => $p->{$_} }
+        qw( address1 address2 city county state zip country )
+    );
+    $quotation_pkg->set('locationnum', $location->locationnum);
+  }
+
+  my $error = $quotation_pkg->insert
+           || $quotation->estimate;
+
+  { 'error'         => $error,
+    'quotationnum'  => $quotation->quotationnum };
+}
+=item quotation_remove_pkg { session, 'pkgnum' }
+
+Removes the package from the user's current quotation. 'pkgnum' is required.
+
+=cut
+
+sub quotation_remove_pkg {
+  my $p = shift;
+
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+  
+  my $quotation = _quotation($session);
+  my $quotationpkgnum = $p->{pkgnum};
+  my $quotation_pkg = FS::quotation_pkg->by_key($quotationpkgnum);
+  if (!$quotation_pkg
+      or $quotation_pkg->quotationnum != $quotation->quotationnum) {
+    return { 'error' => "unknown quotation item $quotationpkgnum" };
+  }
+  warn "removing quotation_pkg with pkgpart ".$quotation_pkg->pkgpart."\n"
+    if $DEBUG;
+
+  my $error = $quotation_pkg->delete
+           || $quotation->estimate;
+
+  { 'error'         => $error,
+    'quotationnum'  => $quotation->quotationnum };
+}
+
+=item quotation_order
+
+Convert the current quotation to a package order.
+
+=cut
+
+sub quotation_order {
+  my $p = shift;
+
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+  
+  my $quotation = _quotation($session);
+
+  my $error = $quotation->order;
+  $quotation->set('disabled' => 'Y');
+  $error ||= $quotation->replace;
+
+  return { 'error' => $error };
+}
+
+1;
index 952b199..5f1b38c 100644 (file)
@@ -52,6 +52,7 @@ our %typefix = (
   'login_info'         => \%typefix_skin_info,
   'invoice_logo'       => { 'logo' => 'base64', },
   'login_banner_image' => { 'image' => 'base64', },
+  'quotation_print'    => { 'document' => 'base64' },
 );
 
 sub AUTOLOAD {
@@ -186,6 +187,12 @@ sub ss2clientapi {
   'call_time'                 => 'PrepaidPhone/call_time',
   'call_time_nanpa'           => 'PrepaidPhone/call_time_nanpa',
   'phonenum_balance'          => 'PrepaidPhone/phonenum_balance',
+
+  'quotation_info'            => 'MyAccount/quotation/quotation_info',
+  'quotation_print'           => 'MyAccount/quotation/quotation_print',
+  'quotation_add_pkg'         => 'MyAccount/quotation/quotation_add_pkg',
+  'quotation_remove_pkg'      => 'MyAccount/quotation/quotation_remove_pkg',
+  'quotation_order'           => 'MyAccount/quotation/quotation_order',
   };
 }
 
index a37e5a6..c5c03ff 100644 (file)
@@ -2854,6 +2854,13 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'manual_process-single_invoice_amount',
+    'section'     => 'billing',
+    'description' => 'When entering manual credit card and ACH payments, amount will not autofill if the customer has more than one open invoice',
+    'type'        => 'checkbox',
+  },
+
+  {
     'key'         => 'manual_process-pkgpart',
     'section'     => 'billing',
     'description' => 'Package to add to each manual credit card and ACH payment entered by employees from the backend.  Enabling this option may be in violation of your merchant agreement(s), so please check it(/them) carefully before enabling this option.',
index 7f28e11..42122f7 100644 (file)
@@ -3595,6 +3595,7 @@ sub tables_hashref {
         'columnlabel', 'varchar', 'NULL', $char_d, '', '',
         'columnvalue', 'varchar', 'NULL',     512, '', '', 
         'columnflag',  'char',    'NULL',       1, '', '', 
+        'required',    'char',    'NULL',       1, '', '', 
       ],
       'primary_key'  => 'columnnum',
       'unique'       => [ [ 'svcpart', 'columnname' ] ],
index d9d4588..930966a 100644 (file)
@@ -179,6 +179,12 @@ sub cutoff_day {
   if ( $self->option('sync_bill_date',1) ) {
     my $next_bill = $cust_pkg->cust_main->next_bill_date;
     if ( defined($next_bill) ) {
+      # careful here. if the prorate calculation is going to round to 
+      # the nearest day, this needs to always return the same result
+      if ( $self->option('prorate_round_day', 1) ) {
+        my $hour = (localtime($next_bill))[2];
+        $next_bill += 64800 if $hour >= 12;
+      }
       return (localtime($next_bill))[3];
     }
   }
index f56878a..1da30cb 100644 (file)
@@ -95,8 +95,12 @@ the part_svc_column table appropriately (see L<FS::part_svc_column>).
 
 =item I<svcdb>__I<field> - Default or fixed value for I<field> in I<svcdb>.
 
+=item I<svcdb>__I<field>_label
+
 =item I<svcdb>__I<field>_flag - defines I<svcdb>__I<field> action: null or empty (no default), `D' for default, `F' for fixed (unchangeable), , `S' for selectable choice, `M' for manual selection from inventory, or `A' for automatic selection from inventory.  For virtual fields, can also be 'X' for excluded.
 
+=item I<svcdb>__I<field>_required - I<field> should always have a true value
+
 =back
 
 If you want to add part_svc_column records for fields that do not exist as
@@ -145,6 +149,7 @@ sub insert {
   foreach my $field (
     grep { $_ ne 'svcnum'
            && ( defined( $self->getfield($svcdb.'__'.$_.'_flag') )
+                || defined($self->getfield($svcdb.'__'.$_.'_required'))
                 || $self->getfield($svcdb.'__'.$_.'_label') !~ /^\s*$/ )
          } (fields($svcdb), @fields)
   ) {
@@ -156,6 +161,7 @@ sub insert {
 
     my $flag  = $self->getfield($svcdb.'__'.$field.'_flag');
     my $label = $self->getfield($svcdb.'__'.$field.'_label');
+    my $required = $self->getfield($svcdb.'__'.$field.'_required') ? 'Y' : '';
     if ( uc($flag) =~ /^([A-Z])$/ || $label !~ /^\s*$/ ) {
 
       if ( uc($flag) =~ /^([A-Z])$/ ) {
@@ -170,6 +176,8 @@ sub insert {
       $part_svc_column->setfield('columnlabel', $label)
         if $label !~ /^\s*$/;
 
+      $part_svc_column->setfield('required', $required);
+
       if ( $previous ) {
         $error = $part_svc_column->replace($previous);
       } else {
@@ -279,6 +287,7 @@ sub replace {
     foreach my $field (
       grep { $_ ne 'svcnum'
              && ( defined( $new->getfield($svcdb.'__'.$_.'_flag') )
+                  || defined($new->getfield($svcdb.'__'.$_.'_required'))
                   || $new->getfield($svcdb.'__'.$_.'_label') !~ /^\s*$/ )
            } (fields($svcdb),@fields)
     ) {
@@ -291,6 +300,7 @@ sub replace {
 
       my $flag  = $new->getfield($svcdb.'__'.$field.'_flag');
       my $label = $new->getfield($svcdb.'__'.$field.'_label');
+      my $required = $new->getfield($svcdb.'__'.$field.'_required') ? 'Y' : '';
  
       if ( uc($flag) =~ /^([A-Z])$/ || $label !~ /^\s*$/ ) {
 
@@ -309,6 +319,8 @@ sub replace {
         $part_svc_column->setfield('columnlabel', $label)
           if $label !~ /^\s*$/;
 
+        $part_svc_column->setfield('required', $required);
+
         if ( $previous ) {
           $error = $part_svc_column->replace($previous);
         } else {
@@ -699,6 +711,8 @@ some components specified by "select-.*.html", and a bunch more...
 
 =item select_allow_empty - Used with select_table, adds an empty option
 
+=item required - This field should always have a true value (do not use with type checkbox or disabled)
+
 =back
 
 =cut
@@ -773,7 +787,7 @@ sub process {
                          and ref($param->{ $f }) ) {
                       $param->{ $f } = join(',', @{ $param->{ $f } });
                    }
-                    ( $f, $f.'_flag', $f.'_label' );
+                    ( $f, $f.'_flag', $f.'_label', $f.'_required' );
                   }
                   @fields;
 
index 38ce1fa..75a2dfb 100644 (file)
@@ -45,6 +45,8 @@ fields are currently supported:
 
 =item columnflag - null or empty (no default), `D' for default, `F' for fixed (unchangeable), `S' for selectable choice, `M' for manual selection from inventory, `A' for automatic selection from inventory, or `H' for selection from a hardware class.  For virtual fields, can also be 'X' for excluded.
 
+=item required - column value expected to be true
+
 =back
 
 =head1 METHODS
@@ -91,6 +93,7 @@ sub check {
     || $self->ut_alpha('columnname')
     || $self->ut_textn('columnlabel')
     || $self->ut_anything('columnvalue')
+    || $self->ut_flag('required')
   ;
   return $error if $error;
 
index f2a9620..45f3522 100644 (file)
@@ -695,22 +695,24 @@ sub estimate {
       # discounts
       if ( $cust_bill_pkg->get('discounts') ) {
         my $discount = $cust_bill_pkg->get('discounts')->[0];
-        # discount records are generated as (setup, recur).
-        # well, not always, sometimes it's just (recur), but fixing this
-        # is horribly invasive.
-        my $qpd = $quotation_pkg_discount{$quotationpkgnum}
-              ||= qsearchs('quotation_pkg_discount', {
-                  'quotationpkgnum' => $quotationpkgnum
-                  });
-
-        if (!$qpd) { #can't happen
-          warn "$me simulated bill returned a discount but no discount is in effect.\n";
-        }
-        if ($discount and $qpd) {
-          if ( $i == 0 ) {
-            $qpd->set('setup_amount', $discount->amount);
-          } else {
-            $qpd->set('recur_amount', $discount->amount);
+        if ( $discount ) {
+          # discount records are generated as (setup, recur).
+          # well, not always, sometimes it's just (recur), but fixing this
+          # is horribly invasive.
+          my $qpd = $quotation_pkg_discount{$quotationpkgnum}
+                ||= qsearchs('quotation_pkg_discount', {
+                    'quotationpkgnum' => $quotationpkgnum
+                    });
+
+          if (!$qpd) { #can't happen
+            warn "$me simulated bill returned a discount but no discount is in effect.\n";
+          }
+          if ($discount and $qpd) {
+            if ( $i == 0 ) {
+              $qpd->set('setup_amount', $discount->amount);
+            } else {
+              $qpd->set('recur_amount', $discount->amount);
+            }
           }
         }
       } # end of discount stuff
index 8199ba1..b1f9d14 100644 (file)
@@ -152,13 +152,46 @@ sub cust_linked {
 
 Checks the validity of fields in this record.
 
-At present, this does nothing but call FS::Record::check (which, in turn, 
-does nothing but run virtual field checks).
+Only checks fields marked as required in table_info or 
+part_svc_column definition.  Should be invoked by service-specific
+check using SUPER.  Invokes FS::Record::check using SUPER.
 
 =cut
 
 sub check {
   my $self = shift;
+
+  ## Checking required fields
+
+  # get fields marked as required in table_info
+  my $required = {};
+  my $labels = {};
+  my $tinfo = $self->can('table_info') ? $self->table_info : {};
+  my $fields = $tinfo->{'fields'} || {};
+  foreach my $field (keys %$fields) {
+    if (ref($fields->{$field}) && $fields->{$field}->{'required'}) {
+      $required->{$field} = 1;
+      $labels->{$field} = $fields->{$field}->{'label'};
+    }
+  }
+  # add fields marked as required in database
+  foreach my $column (
+    qsearch('part_svc_column',{
+      'svcpart' => $self->svcpart,
+      'required' => 'Y'
+    })
+  ) {
+    $required->{$column->columnname} = 1;
+    $labels->{$column->columnname} = $column->columnlabel;
+  }
+  # do the actual checking
+  foreach my $field (keys %$required) {
+    unless ($self->$field) {
+      my $name = $labels->{$field} || $field;
+      return "Field $name is required\n"
+    }
+  }
+
   $self->SUPER::check;
 }
 
index 452f250..790ac34 100644 (file)
@@ -283,6 +283,7 @@ sub table_info {
                          disable_default => 1,
                          disable_fixed => 1,
                          disable_select => 1,
+                         required => 1,
                        },
         'password_selfchange' => { label => 'Password modification',
                                    type  => 'checkbox',
@@ -310,7 +311,9 @@ sub table_info {
                          type => 'text',
                          disable_inventory => 1,
                        },
-        '_password' => 'Password',
+        '_password' => { label => 'Password',
+                         required => 1
+                       },
         'gid'       => {
                          label    => 'GID',
                         def_info => 'when blank, defaults to UID',
@@ -333,6 +336,7 @@ sub table_info {
                          select_key   => 'svcnum',
                          select_label => 'domain',
                          disable_inventory => 1,
+                         required => 1,
                        },
         'pbxsvc'    => { label => 'PBX',
                          type  => 'select-svc_pbx.html',
index b01d673..78556cf 100644 (file)
@@ -134,7 +134,10 @@ sub table_info {
     'display_weight' => 20,
     'cancel_weight'  => 60,
     'fields' => {
-      'domain' => 'Domain',
+      'domain' => {
+                  label => 'Domain',
+                  required => 1,
+                },
       'parent_svcnum' => { 
                          label => 'Parent domain / Communigate administrator domain',
                          type  => 'select',
index 12d56bb..765d611 100644 (file)
@@ -115,6 +115,13 @@ $socket .= '.'.$tag if defined $tag && length($tag);
 
   'start_thirdparty'          => 'MyAccount/start_thirdparty',
   'finish_thirdparty'         => 'MyAccount/finish_thirdparty',
+
+  'quotation_info'            => 'MyAccount/quotation/quotation_info',
+  'quotation_print'           => 'MyAccount/quotation/quotation_print',
+  'quotation_add_pkg'         => 'MyAccount/quotation/quotation_add_pkg',
+  'quotation_remove_pkg'      => 'MyAccount/quotation/quotation_remove_pkg',
+  'quotation_order'           => 'MyAccount/quotation/quotation_order',
+
 );
 @EXPORT_OK = (
   keys(%autoload),
index 0d36853..ec5f321 100755 (executable)
@@ -61,6 +61,8 @@ function part_export_areyousure(href) {
 
     <TH COLSPAN=2 CLASS="grid" BGCOLOR="#cccccc">Modifier</TH>
 
+    <TH CLASS="grid" BGCOLOR="#cccccc" STYLE="font-size: smaller;">Required</TH>
+
   </TR>
 % my $conf = FS::Conf->new;
 % foreach my $part_svc ( @part_svc ) {
@@ -78,6 +80,9 @@ function part_export_areyousure(href) {
 %                $col->columnflag || ( $col->columnlabel !~ /^\S*$/
 %                                      && $col->columnlabel ne $def->{'label'}
 %                                    )
+%                                 || ( $col->required
+%                                      && !$def->{'required'}
+%                                    )
 %              )
 %            }
 %            @dfields ;
@@ -150,7 +155,7 @@ function part_export_areyousure(href) {
     </TD>
 
 %     unless ( @fields ) {
-%       for ( 1..4 ) {  
+%       for ( 1..5 ) {  
          <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"</TD>
 %       }
 %     }
@@ -170,7 +175,6 @@ function part_export_areyousure(href) {
      <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $field %></TD>
      <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $label %></TD>
      <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $flag{$flag} %></TD>
-
      <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
 % my $value = &$formatter($part_svc->part_svc_column($field)->columnvalue);
 % if ( $flag =~ /^[MAH]$/ ) { 
@@ -189,6 +193,11 @@ function part_export_areyousure(href) {
 % }
 
      </TD>
+     <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+% if ($part_svc_column->required) {
+       Yes
+% }
+     </TD>
 %     $n1="</TR><TR>";
 %     } #foreach $field
 %   if ( $part_svc->restrict_edit_password ) {
index 2bb4f5e..a6ccaf8 100644 (file)
@@ -77,6 +77,7 @@ that field.
     <TH BGCOLOR="#cccccc">Field</TH>
     <TH BGCOLOR="#cccccc">Label</TH>
     <TH BGCOLOR="#cccccc" COLSPAN=2>Modifier</TH>
+    <TH BGCOLOR="#cccccc">Required?</TH>
   </TR>
 % $part_svc->set('svcpart' => $opt{'clone'}) if $opt{'clone'}; # for now
 % my $i = 0;
@@ -210,9 +211,17 @@ that field.
       &>
 %   }
     </TD>
+    <TD>
+%   if (!$def->{'type'} || !(grep {$_ eq $def->{'type'}} ('checkbox','disabled'))) {
+      <INPUT ID="<% $name.'_required' %>" TYPE="checkbox" NAME="<% $svcdb %>__<% $field %>_required" VALUE="Y" 
+        <% ($part_svc_column->required || $def->{'required'}) ? 'CHECKED' : '' %> 
+        <% $def->{'required'} ? 'DISABLED' : '' %>
+       >
+%   }
+    </TD>
   </TR>
   <TR CLASS="row<%$i%>">
-    <TD COLSPAN=2 CLASS="def_info">
+    <TD COLSPAN=3 CLASS="def_info">
 %   if ( $def->{def_info} ) {
       (<% $def->{def_info} %>)
     </TD>
@@ -228,7 +237,7 @@ that field.
     <TD COLSPAN=3 ALIGN="right">
       <% emt('Require "Provision" access right to edit password') %>
     </TD>
-    <TD>
+    <TD COLSPAN=2>
       <INPUT TYPE="checkbox" NAME="restrict_edit_password" VALUE="Y" \
       <% $part_svc->restrict_edit_password ? 'CHECKED' : '' %>>
     </TD>
@@ -244,7 +253,7 @@ that field.
     <TD COLSPAN=3 ALIGN="right">
       <% emt('This service has an attached router') %>
     </TD>
-    <TD>
+    <TD COLSPAN=2>
       <INPUT TYPE="checkbox" NAME="has_router" VALUE="Y" \
       <% $part_svc->has_router ? 'CHECKED' : '' %>>
     </TD>
index 47b020c..7a47f15 100755 (executable)
@@ -101,6 +101,15 @@ function flag_changed(obj) {
       }
     }
   }
+  var required = document.getElementById(layer + '__' + field + '_required');
+  if (required && !required.disabledinit) {
+    if (newflag == "F") {
+      required.checked = false;
+      required.disabled = true;
+    } else {
+      required.disabled = false;
+    }
+  }
 }
 
 window.onload = function() {
@@ -111,6 +120,17 @@ window.onload = function() {
       obj.setAttribute('should_be_multiple', true);
     }
   }
+  var inputs = document.getElementsByTagName('INPUT');
+  for(i = 0; i < inputs.length; i++) {
+    var obj = inputs[i];
+    if (obj.type == 'checkbox') {
+      if ( obj.name.match(/_required$/) ) {
+        if ( obj.disabled ) {
+          obj.disabledinit = 1;
+        }
+      }
+    }
+  }
   for(i = 0; i < selects.length; i++) {
     var obj = selects[i];
     if ( obj.name.match(/_flag$/) ) {
index 90b03c7..b83ad71 100644 (file)
@@ -273,7 +273,9 @@ my @states = sort { $a cmp $b } keys %states;
 
 my $amount = '';
 if ( $balance > 0 ) {
-  $amount = $balance;
+  $amount = $balance
+    unless $conf->exists('manual_process-single_invoice_amount')
+      && ($cust_main->open_cust_bill != 1);
 }
 
 my $payunique = "webui-payment-". time. "-$$-". rand() * 2**32;
diff --git a/ng_selfservice/images/cross.png b/ng_selfservice/images/cross.png
new file mode 100644 (file)
index 0000000..1514d51
Binary files /dev/null and b/ng_selfservice/images/cross.png differ
diff --git a/ng_selfservice/quotation.php b/ng_selfservice/quotation.php
new file mode 100644 (file)
index 0000000..cf45543
--- /dev/null
@@ -0,0 +1,130 @@
+<STYLE>
+td.amount {
+    text-align: right;
+}
+td.amount:before {
+    content: "$";
+}
+tr.total * {
+    background-color: #ddf;
+    font-weight: bold;
+}
+table.section {
+    width: 100%;
+    border-collapse: collapse;
+}
+table.section td {
+    font-size: small;
+    padding: 1ex 1ex;
+}
+table.section th {
+    text-align: left;
+    padding: 1ex;
+}
+.row0 td {
+    background-color: #eee;
+}
+.row1 td {
+    background-color: #fff;
+}
+</STYLE>
+
+<? $title ='Plan a new service order'; include('elements/header.php'); ?>
+<? $current_menu = 'services_new.php'; include('elements/menu.php'); ?>
+<?
+
+$quotation = $freeside->quotation_info(array(
+  'session_id'  => $_COOKIE['session_id'],
+));
+
+$can_order = 0;
+
+if ( isset($quotation['sections']) and count($quotation['sections']) > 0 ) {
+  $can_order = 1;
+  # there are other ways this could be formatted, yes.
+  # if you want the HTML-formatted quotation, use quotation_print().
+  print(
+    '<INPUT STYLE="float: right" TYPE="button" onclick="window.location.href=\'quotation_print.php\'" value="Download a quotation" />'.
+    '<H3>Order summary</H3>'.
+    "\n"
+  );
+  foreach ( $quotation['sections'] as $section ) {
+    print(
+      '<TABLE CLASS="section">'.
+      '<TR>'.
+      '<TH COLSPAN=4>'.  htmlspecialchars($section['description']).'</TH>'.
+      '</TR>'.
+      "\n"
+    );
+    $row = 0;
+    foreach ( $section['detail_items'] as $detail ) {
+      print(
+        '<TR CLASS="row' . $row . '">'.
+        '<TD>'
+      );
+      if ( $detail['pkgnum'] ) {
+        print(
+          '<A HREF="quotation_remove_pkg.php?pkgnum=' .
+          $detail['pkgnum'] . '">'.
+          '<IMG SRC="images/cross.png" /></A>'
+        );
+      }
+      print(
+        '</TD>'.
+        '<TD>'. htmlspecialchars($detail['description']). '</TD>'.
+        '<TD CLASS="amount">'. $detail['amount']. '</TD>'.
+        '</TR>'. "\n"
+      );
+      $row = 1 - $row;
+    }
+    print(
+      '<TR CLASS="total">'.
+      '<TD></TD>'.
+      '<TD>Total</TD>'.
+      '<TD CLASS="amount">'. $section['subtotal']. '</TD>'.
+      '</TR>'.
+      '</TABLE>'.
+      "\n"
+    );
+  } # foreach $section
+}
+
+$pkgselect = $freeside->mason_comp( array(
+    'session_id' => $_COOKIE['session_id'],
+    'comp'       => '/elements/select-part_pkg.html',
+    'args'       => array( 'onchange'       , 'enable_order_pkg()',
+                           'empty_label'    , 'Select package',
+                           'form_name'      , 'AddPkgForm',
+                         ),
+));
+if ( isset($pkgselect['error']) && $pkgselect['error'] ) {
+  $error = $pkgselect['error'];
+  header('Location:index.php?error='. urlencode($pkgselect));
+  die();
+}
+
+?>
+<SCRIPT TYPE="text/javascript">
+function enable_order_pkg () {
+    document.AddPkgForm.submit.disabled =
+        (document.AddPkgForm.pkgpart.value == '');
+}
+</SCRIPT>
+
+<DIV STYLE="border-top: 1px solid; padding: 1ex">
+<? $error = $_REQUEST['error']; include('elements/error.php'); ?>
+
+<FORM NAME="AddPkgForm" ACTION="quotation_add_pkg.php" METHOD=POST>
+<? echo $pkgselect['output']; ?>
+<INPUT NAME="submit" TYPE="submit" VALUE="Add package" <? if ( ! isset($_REQUEST['pkgpart']) ) { echo 'DISABLED'; } ?>>
+</FORM>
+
+<? if ( $can_order ) { ?>
+<FORM NAME="OrderQuoteForm" ACTION="quotation_order.php" METHOD=POST>
+<INPUT TYPE="submit" VALUE="Confirm this order" <? if ( !$can_order ) { echo 'DISABLED'; } ?>>
+<? } ?>
+
+</DIV>
+
+<? include('elements/menu_footer.php'); ?>
+<? include('elements/footer.php'); ?>
diff --git a/ng_selfservice/quotation_add_pkg.php b/ng_selfservice/quotation_add_pkg.php
new file mode 100644 (file)
index 0000000..1e7e71f
--- /dev/null
@@ -0,0 +1,31 @@
+<? require('elements/session.php');
+
+$dest = 'quotation.php';
+
+if ( isset($_REQUEST['pkgpart']) ) {
+
+  $results = array();
+
+  $params = array( 'custnum', 'pkgpart' );
+
+  $matches = array();
+  if ( preg_match( '/^(\d+)$/', $_REQUEST['pkgpart'] ) ) {
+
+    $args = array(
+        'session_id' => $_COOKIE['session_id'],
+        'pkgpart'    => $_REQUEST['pkgpart'],
+    );
+
+    $results = $freeside->quotation_add_pkg($args);
+
+  }
+
+  if ( isset($results['error']) && $results['error'] ) {
+    $dest .= '?error=' . $results['error'] . ';pkgpart=' . $_REQUEST['pkgpart'];
+  }
+}
+
+header("Location:$dest");
+
+?>
+
diff --git a/ng_selfservice/quotation_order.php b/ng_selfservice/quotation_order.php
new file mode 100644 (file)
index 0000000..d35eacb
--- /dev/null
@@ -0,0 +1,15 @@
+<? require('elements/session.php');
+
+$dest = 'services.php';
+
+$args = array( 'session_id' => $_COOKIE['session_id'] );
+
+$results = $freeside->quotation_order($args);
+
+if ( isset($results['error']) && $results['error'] ) {
+    $dest = 'quotation.php?error=' . $results['error'];
+}
+
+header("Location:$dest");
+
+?>
diff --git a/ng_selfservice/quotation_print.php b/ng_selfservice/quotation_print.php
new file mode 100644 (file)
index 0000000..9676405
--- /dev/null
@@ -0,0 +1,17 @@
+<? require('elements/session.php');
+
+$args = array(
+    'session_id' => $_COOKIE['session_id'],
+    'format'     => 'pdf'
+);
+
+$results = $freeside->quotation_print($args);
+if ( isset($results['document']) ) {
+    header('Content-Type: application/pdf');
+    header('Content-Disposition: filename=quotation.pdf');
+    print($results['document']->scalar);
+} else {
+    header("Location: quotation.php?error=" . $results['error']);
+}
+
+?>
diff --git a/ng_selfservice/quotation_remove_pkg.php b/ng_selfservice/quotation_remove_pkg.php
new file mode 100644 (file)
index 0000000..07548c7
--- /dev/null
@@ -0,0 +1,31 @@
+<? require('elements/session.php');
+
+$dest = 'quotation.php';
+
+if ( isset($_REQUEST['pkgnum']) ) {
+
+  $results = array();
+
+  $params = array( 'custnum', 'pkgnum' );
+
+  $matches = array();
+  if ( preg_match( '/^(\d+)$/', $_REQUEST['pkgnum'] ) ) {
+
+    $args = array(
+        'session_id' => $_COOKIE['session_id'],
+        'pkgnum'     => $_REQUEST['pkgnum'],
+    );
+
+    $results = $freeside->quotation_remove_pkg($args);
+
+  }
+
+  if ( isset($results['error']) && $results['error'] ) {
+    $dest .= '?error=' . $results['error'];
+  }
+
+}
+
+header("Location:$dest");
+
+?>