Merge branch 'master' of ssh://git.freeside.biz/home/git/freeside
authorChristopher Burger <burgerc@freeside.biz>
Tue, 13 Feb 2018 14:04:28 +0000 (09:04 -0500)
committerChristopher Burger <burgerc@freeside.biz>
Tue, 13 Feb 2018 14:04:28 +0000 (09:04 -0500)
26 files changed:
FS/FS/Schema.pm
FS/FS/Template_Mixin.pm
FS/FS/cdr.pm
FS/FS/cust_bill.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_bill_pkg_detail.pm
FS/FS/cust_pkg.pm
FS/FS/part_pkg.pm
FS/FS/part_pkg/prorate_Mixin.pm
FS/FS/quotation.pm
FS/bin/freeside-passwd [new file with mode: 0755]
bin/update-rates [new file with mode: 0644]
debian/control
debian/freeside-webui.postinst [new file with mode: 0644]
debian/freeside.postinst [new file with mode: 0644]
debian/postinst [deleted file]
debian/rules
httemplate/docs/about.html
httemplate/edit/access_user.html
httemplate/edit/process/access_user.html
httemplate/edit/process/change-cust_pkg.html
httemplate/elements/menu.html
httemplate/misc/change_pkg.cgi
httemplate/misc/process/void-cust_bill.html
httemplate/misc/void-cust_bill.html
httemplate/view/prospect_main.html

index 65eadad..edecb7f 100644 (file)
@@ -5661,9 +5661,6 @@ sub tables_hashref {
         'sessionnum',       'int',    'NULL',      '', '', '',
         'subscriber',   'varchar',    'NULL', $char_d, '', '',
 
-        #old
-        'cdrbatch',     'varchar',    'NULL',     255, '', '',
-        #new
         'cdrbatchnum',      'int',    'NULL',      '', '', '',
 
         # FK to cust_bill_pkg_detail; having a value here absolutely means
index b9f3e92..88fd4e8 100644 (file)
@@ -657,10 +657,11 @@ sub print_generic {
   $invoice_data{'cid'} = $params{'cid'}
     if $params{'cid'};
 
-  if ( $cust_main->country eq $countrydefault ) {
-    $invoice_data{'country'} = '';
-  } else {
+  if ( $cust_main->bill_locationnum
+       && $cust_main->bill_location->country ne $countrydefault ) {
     $invoice_data{'country'} = &$escape_function($cust_main->bill_country_full);
+  } else {
+    $invoice_data{'country'} = '';
   }
 
   my @address = ();
index 331ac0f..3de0224 100644 (file)
@@ -170,7 +170,7 @@ following fields are currently supported:
 
 =item freesiderewritestatus - NULL, done, skipped
 
-=item cdrbatch
+=item cdrbatchnum
 
 =item detailnum - Link to invoice detail (L<FS::cust_bill_pkg_detail>)
 
@@ -240,7 +240,6 @@ sub table_info {
         'svcnum'                => 'Freeside service',
         'freesidestatus'        => 'Freeside status',
         'freesiderewritestatus' => 'Freeside rewrite status',
-        'cdrbatch'              => 'Legacy batch',
         'cdrbatchnum'           => 'Batch',
         'detailnum'             => 'Freeside invoice detail line',
     },
@@ -1659,7 +1658,12 @@ foreach my $INC ( @INC ) {
 
 tie my %import_formats, 'Tie::IxHash',
   map  { $_ => $cdr_info{$_}->{'name'} }
-  sort { $cdr_info{$a}->{'weight'} <=> $cdr_info{$b}->{'weight'} }
+  
+  #this is not doing anything useful anymore
+  #sort { $cdr_info{$a}->{'weight'} <=> $cdr_info{$b}->{'weight'} }
+  #so just sort alpha
+  sort { lc($cdr_info{$a}->{'name'}) cmp lc($cdr_info{$b}->{'name'}) }
+
   grep { exists($cdr_info{$_}->{'import_fields'}) }
   keys %cdr_info;
 
@@ -1868,41 +1872,6 @@ sub process_batch_import {
 #    @columns = map { s/^ +//; $_; } @columns;
 #  }
 
-# _ upgrade_data
-#
-# Used by FS::Upgrade to migrate to a new database.
-
-sub _upgrade_data {
-  my ($class, %opts) = @_;
-
-  warn "$me upgrading $class\n" if $DEBUG;
-
-  my $sth = dbh->prepare(
-    'SELECT DISTINCT(cdrbatch) FROM cdr WHERE cdrbatch IS NOT NULL'
-  ) or die dbh->errstr;
-
-  $sth->execute or die $sth->errstr;
-
-  my %cdrbatchnum = ();
-  while (my $row = $sth->fetchrow_arrayref) {
-
-    my $cdr_batch = qsearchs( 'cdr_batch', { 'cdrbatch' => $row->[0] } );
-    unless ( $cdr_batch ) {
-      $cdr_batch = new FS::cdr_batch { 'cdrbatch' => $row->[0] };
-      my $error = $cdr_batch->insert;
-      die $error if $error;
-    }
-
-    $cdrbatchnum{$row->[0]} = $cdr_batch->cdrbatchnum;
-  }
-
-  $sth = dbh->prepare('UPDATE cdr SET cdrbatch = NULL, cdrbatchnum = ? WHERE cdrbatch IS NOT NULL AND cdrbatch = ?') or die dbh->errstr;
-
-  foreach my $cdrbatch (keys %cdrbatchnum) {
-    $sth->execute($cdrbatchnum{$cdrbatch}, $cdrbatch) or die $sth->errstr;
-  }
-
-}
 
 =item ip_addr_sql FIELD RANGE
 
index 8b94dcc..bd1b8bb 100644 (file)
@@ -215,7 +215,7 @@ sub insert {
 
 }
 
-=item void [ REASON ]
+=item void [ REASON [ , REPROCESS_CDRS ] ]
 
 Voids this invoice: deletes the invoice and adds a record of the voided invoice
 to the FS::cust_bill_void table (and related tables starting from
@@ -226,6 +226,7 @@ FS::cust_bill_pkg_void).
 sub void {
   my $self = shift;
   my $reason = scalar(@_) ? shift : '';
+  my $reprocess_cdrs = scalar(@_) ? shift : '';
 
   unless (ref($reason) || !$reason) {
     $reason = FS::reason->new_or_existing(
@@ -257,7 +258,7 @@ sub void {
   }
 
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
-    my $error = $cust_bill_pkg->void($reason);
+    my $error = $cust_bill_pkg->void($reason, $reprocess_cdrs);
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
@@ -3541,6 +3542,23 @@ sub _items_aging_balances {
   return map{ sprintf('%.2f',$_) } @aging_balances;
 }
 
+=item has_call_details
+
+Returns true if this invoice has call details.
+
+=cut
+
+sub has_call_details {
+  my $self = shift;
+  $self->scalar_sql("
+    SELECT 1 FROM cust_bill_pkg_detail
+             LEFT JOIN cust_bill_pkg USING (billpkgnum)
+      WHERE cust_bill_pkg_detail.format = 'C'
+        AND cust_bill_pkg.invnum = ?
+      LIMIT 1
+  ", $self->invnum);
+}
+
 =item call_details [ OPTION => VALUE ... ]
 
 Returns an array of CSV strings representing the call details for this invoice
index e44a847..77dce24 100644 (file)
@@ -324,7 +324,7 @@ sub insert {
 
 }
 
-=item void [ REASON ]
+=item void [ REASON [ , REPROCESS_CDRS ] ]
 
 Voids this line item: deletes the line item and adds a record of the voided
 line item to the FS::cust_bill_pkg_void table (and related tables).
@@ -334,6 +334,7 @@ line item to the FS::cust_bill_pkg_void table (and related tables).
 sub void {
   my $self = shift;
   my $reason = scalar(@_) ? shift : '';
+  my $reprocess_cdrs = scalar(@_) ? shift : '';
 
   unless (ref($reason) || !$reason) {
     $reason = FS::reason->new_or_existing(
@@ -373,6 +374,9 @@ sub void {
     cust_tax_exempt_pkg
     cust_bill_pkg_fee
   )) {
+    my %delete_args = ();
+    $delete_args{'reprocess_cdrs'} = $reprocess_cdrs
+      if $table eq 'cust_bill_pkg_detail';
 
     foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
 
@@ -380,7 +384,7 @@ sub void {
       my $void = $vclass->new( {
         map { $_ => $linked->get($_) } $linked->fields
       });
-      my $error = $void->insert || $linked->delete;
+      my $error = $void->insert || $linked->delete(%delete_args);
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
         return $error;
@@ -863,7 +867,7 @@ sub _item_discount {
   # show introductory rate as a pseudo-discount
   if (!$d) { # this will conflict with showing real discounts
     my $part_pkg = $self->part_pkg;
-    if ( $part_pkg and $part_pkg->option('show_as_discount') ) {
+    if ( $part_pkg and $part_pkg->option('show_as_discount',1) ) {
       my $cust_pkg = $self->cust_pkg;
       my $intro_end = $part_pkg->intro_end($cust_pkg);
       my $_date = $self->cust_bill->_date;
index dd118c1..19b15f7 100644 (file)
@@ -106,21 +106,37 @@ sub insert {
   '';
 }
 
-=item delete
+=item delete [ ARG => VALUE ... ]
 
 Delete this record from the database.
 
+If the "reprocess_cdrs" argument is set to true, resets the status of any
+related CDRs (and deletes their associated cdr_termination records, if any).
+
 =cut
 
 sub delete {
-  my $self = shift;
+  my( $self, %args ) = @_;
+
   my $error = $self->SUPER::delete;
   return $error if $error;
+
   foreach my $cdr (qsearch('cdr', { detailnum => $self->detailnum })) {
+
     $cdr->set('detailnum', '');
+    $cdr->set('freesidestatus', '') if $args{'reprocess_cdrs'};
     $error = $cdr->replace;
     return "error unlinking CDR #" . $cdr->acctid . ": $error" if $error;
+
+    #well, technically this could have been on other invoices / termination
+    # partners... separate flag?
+    $self->scalar_sql( 'DELETE FROM cdr_termination WHERE acctid = ?',
+                       $cdr->acctid )
+      if $args{'reprocess_cdrs'};
+
   }
+
+  '';
 }
 
 =item replace OLD_RECORD
index 7d68323..f11beec 100644 (file)
@@ -6,7 +6,7 @@ use base qw( FS::cust_pkg::Search FS::cust_pkg::API
            );
 
 use strict;
-use Carp qw(cluck);
+use Carp qw(cluck croak);
 use Scalar::Util qw( blessed );
 use List::Util qw(min max sum);
 use Tie::IxHash;
@@ -2372,8 +2372,20 @@ sub change {
     $same_pkgpart = 0;
   }
 
-  if ($opt->{'waive_setup'}) { $self->set('waive_setup', $opt->{'waive_setup'}) }
-  else { $self->set('waive_setup', ''); }
+  # Discounts:
+  #   When a new discount level is specified in $opt:
+  #     If new discountnum matches old discountnum, months_used/end_date are
+  #       carried over as the discount is applied to the new cust_pkg
+  #
+  #   Legacy behavior:
+  #     Unless discount-related fields have been set within $opt, change()
+  #     sets no discounts on the changed packages unless the new pkgpart is the
+  #     same as the old pkgpart.  In that case, discounts from the old cust_pkg
+  #     are copied onto the new cust_pkg
+
+  # Read discount fields from $opt
+  my %new_discount = $self->_parse_new_discounts($opt);
+  $self->set(waive_setup => $opt->{waive_setup} ? $opt->{waive_setup} : '');
 
   # Before going any further here: if the package is still in the pre-setup
   # state, it's safe to modify it in place. No need to charge/credit for 
@@ -2429,6 +2441,22 @@ sub change {
 
     } # done transferring services
 
+    # Set waive_setup as directed
+    if ( !$error && exists $opt->{waive_setup} ) {
+      $self->set(waive_setup => $opt->{waive_setup});
+      $error = $self->replace;
+    }
+
+    # Set discounts if explicitly specified in $opt
+    if ( !$error && %new_discount ) {
+      $error = $self->change_discount(%new_discount);
+    }
+
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+
     $dbh->commit if $oldAutoCommit;
     return $self;
 
@@ -2618,8 +2646,18 @@ sub change {
     }
   }
 
-  # transfer discounts, if we're not changing pkgpart
-  if ( $same_pkgpart ) {
+  if (%new_discount && !$error) {
+
+    # If discounts were explicitly specified in $opt
+    $error = $cust_pkg->change_discount(%new_discount);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "applying discounts: $error";
+    }
+
+  } elsif ( $same_pkgpart ) {
+
+    # transfer discounts, if we're not changing pkgpart
     foreach my $old_discount ($self->cust_pkg_discount_active) {
       # don't remove the old discount, we may still need to bill that package.
       my $new_discount = new FS::cust_pkg_discount {
@@ -2836,6 +2874,20 @@ sub change_later {
     $opt->{'locationnum'} = $opt->{'cust_location'}->locationnum;
   }
 
+  # Discounts:
+  #   Applies discounts to the newly created future_change package
+  #
+  #   If a new discount is the same as the old discount, carry over the
+  #     old discount's months_used/end_date fields too
+  #
+  #   Legacy behavior:
+  #     Legacy behavior was to create the next package with no discount.
+  #     This behavior is preserved.  Without the discount fields in $opt,
+  #     the new package will be created with no discounts.
+
+  # parse discount information from $opt
+  my %new_discount = $self->_parse_new_discounts($opt);
+
   if ( $self->change_to_pkgnum ) {
     my $change_to = FS::cust_pkg->by_key($self->change_to_pkgnum);
     my $new_pkgpart = $opt->{'pkgpart'}
@@ -2873,6 +2925,16 @@ sub change_later {
       $change_to->set('start_date', $date);
       $error = $self->replace || $change_to->replace;
     }
+
+    if ( !$error && exists $opt->{waive_setup} ) {
+      $change_to->set(waive_setup => $opt->{waive_setup} );
+      $error = $change_to->insert();
+    }
+
+    if ( !$error && %new_discount ) {
+      $error = $change_to->change_discount(%new_discount);
+    }
+
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
@@ -2906,11 +2968,17 @@ sub change_later {
   } );
   $error = $new->insert('change' => 1, 
                         'allow_pkgpart' => ($new_pkgpart ? 0 : 1));
+
+  if ( !$error && %new_discount ) {
+    $error = $new->change_discount(%new_discount);
+  }
+
   if ( !$error ) {
     $self->set('change_to_pkgnum', $new->pkgnum);
     $self->set('expire', $date);
     $error = $self->replace;
   }
+
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
   } else {
@@ -2920,6 +2988,66 @@ sub change_later {
   $error;
 }
 
+# Helper method reads $opt hashref from change() and change_later()
+# Returns a hash of %new_discount suitable for passing to change_discount()
+sub _parse_new_discounts {
+  my ($self, $opt) = @_;
+
+  croak "Bad parameter list" unless ref $opt;
+
+  my %old_discount =
+    map { $_->setuprecur => $_ }
+    qsearch('cust_pkg_discount', {
+      pkgnum   => $self->pkgnum,
+      disabled => '',
+    });
+
+  my %new_discount;
+  for my $type(qw|setup recur|) {
+
+    if (exists $opt->{"${type}_discountnum"}) {
+      $new_discount{$type} = {
+        discountnum => $opt->{"${type}_discountnum"},
+        amount      => $opt->{"${type}_discountnum_amount"},
+        percent     => $opt->{"${type}_discountnum_percent"},
+      };
+    }
+
+    # Specified discountnum same as old discountnum, carry over addl fields
+    if (
+      exists $opt->{"${type}_discountnum"}
+      && exists $old_discount{$type}
+      && $opt->{"${type}_discountnum"} eq $old_discount{$type}->discountnum
+    ){
+      $new_discount{$type}->{months}   = $old_discount{$type}->months;
+      $new_discount{$type}->{end_date} = $old_discount{$type}->end_date;
+    }
+
+    # No new discount specified, carryover old discount
+    #   If we wanted to abandon legacy behavior, and always carry old discounts
+    #   uncomment this:
+
+    # if (!exists $new_discount{$type} && $old_discount{$type}) {
+    #   $new_discount{$type} = {
+    #     discountnum => $old_discount{$type}->discountnum,
+    #     amount      => $old_discount{$type}->amount,
+    #     percent     => $old_discount{$type}->percent,
+    #     months      => $old_discount{$type}->months,
+    #     end_date    => $old_discount{$type}->end_date,
+    #   };
+    # }
+  }
+
+  if ($DEBUG) {
+    warn "_parse_new_discounts(), pkgnum: ".$self->pkgnum." \n";
+    warn "Determine \%old_discount, \%new_discount: \n";
+    warn Dumper(\%old_discount);
+    warn Dumper(\%new_discount);
+  }
+
+  %new_discount;
+}
+
 =item abort_change
 
 Cancels a future package change scheduled by C<change_later>.
@@ -4731,6 +4859,149 @@ sub insert_discount {
   '';
 }
 
+=item change_discount %opt
+
+Method checks if the given values represent a change in either setup or
+discount level.  If so, the existing discounts are revoked, the new
+discounts are recorded.
+
+Usage:
+
+$error = change_discount(
+  setup => {
+
+    # -1: Indicates a "custom discount"
+    #  0: Indicates to remove any discount
+    # >0: discountnum to apply
+    discountnum => [-1, 0, discountnum],
+
+    # When discountnum is "-1" to indicate custom discount, include
+    # the additional fields:
+    amount      => AMOUNT_DISCOUNT
+    percent     => PERCENTAGE_DISCOUNT
+    months      => -1,
+  },
+
+  recur => {...}
+);
+
+
+=cut
+
+sub change_discount {
+  my ($self, %opt) = @_;
+  return "change_discount() called with bad \%opt"
+    unless %opt;
+
+  for (keys %opt) {
+    return "change_discount() called with unknown bad key $_"
+      unless $_ eq 'setup' || $_ eq 'recur';
+  }
+
+  my @old_discount =
+    qsearch('cust_pkg_discount',{
+      pkgnum   => $self->pkgnum,
+      disabled => '',
+    });
+
+  if ($DEBUG) {
+    warn "change_discount() pkgnum: ".$self->pkgnum." \n";
+    warn "change_discount() \%opt: \n";
+    warn Dumper(\%opt);
+  }
+
+  my @to_be_disabled;
+
+  for my $type (qw|setup recur|) {
+    next unless ref $opt{$type};
+    my %change = %{$opt{$type}};
+
+    return "change_discount() called with bad \$opt($type)"
+      unless $change{discountnum} =~ /^-?\d+$/;
+
+    if ($change{discountnum} eq 0) {
+      # Removing old discount
+
+      delete $opt{$type};
+      push @to_be_disabled, grep {$_->setuprecur eq $type} @old_discount;
+    } else {
+
+      if (
+        grep {
+          $_->discountnum   eq $change{discountnum}
+          && $_->setuprecur eq $type
+        } @old_discount
+      ){
+        # Duplicate, disregard this entry
+        delete $opt{$type};
+        next;
+      } else {
+        # Mark any discounts we're replacing
+        push @to_be_disabled, grep{ $_->setuprecur eq $type} @old_discount;
+      }
+
+    }
+  }
+
+
+  # If we still have changes queued, pass them to insert_discount()
+  # by setting values into object fields
+  for my $type (keys %opt) {
+    $self->set("${type}_discountnum", $opt{$type}->{discountnum});
+
+    if ($opt{$type}->{discountnum} eq '-1') {
+      $self->set("${type}_discountnum_${_}", $opt{$type}->{$_})
+        for qw(amount percent months);
+    }
+
+  }
+
+  if ($DEBUG) {
+    warn "change_discount() \% opt before insert \n";
+    warn Dumper \%opt;
+    warn "\@to_be_disabled \n";
+    warn Dumper \@to_be_disabled;
+  }
+
+  # Roll these updates into a transaction
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error;
+
+  # The "waive setup fee" flag has traditionally been handled by setting
+  # $cust_pkg->waive_setup_fee = Y.  This has been appropriately, and separately
+  # handled, and it operates on a differetnt table than cust_pkg_discount,
+  # so the "-2 for waive setup fee" option is not being reimplemented
+  # here.  Perhaps this may change later.
+  #
+  # When a setup discount is entered, we still need unset waive_setup
+  if ( $opt{setup} && $opt{setup} > -2 && $self->waive_setup ) {
+    $self->set(waive_setup => '');
+    $error = $self->replace();
+  }
+
+  # Create new discounts
+  $error ||= $self->insert_discount();
+
+  # Disabling old discounts
+  for my $tbd (@to_be_disabled) {
+    unless ($error) {
+      $tbd->set(disabled => 'Y');
+      $error = $tbd->replace();
+    }
+  }
+
+  if ($error) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit if $oldAutoCommit;
+  return undef;
+}
+
 =item set_usage USAGE_VALUE_HASHREF 
 
 USAGE_VALUE_HASHREF is a hashref of svc_acct usage columns and the amounts
@@ -5700,4 +5971,3 @@ L<FS::pkg_svc>, schema.html from the base documentation
 =cut
 
 1;
-
index 956cf79..da53715 100644 (file)
@@ -232,6 +232,19 @@ sub insert {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  if ( length($self->classnum) && $self->classnum !~ /^(\d+)$/ ) {
+    my $pkg_class = qsearchs('pkg_class', { 'classname' => $self->classnum } )
+                 || new FS::pkg_class { classname => $self->classnum };
+    unless ( $pkg_class->classnum ) {
+      my $error = $pkg_class->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+    $self->classnum( $pkg_class->classnum );
+  }
+
   warn "  inserting part_pkg record" if $DEBUG;
   my $error = $self->SUPER::insert( $options{options} );
   if ( $error ) {
index 1a7b1ef..9e97cc5 100644 (file)
@@ -205,7 +205,7 @@ sub prorate_setup {
         # For some reason (probably user override), the bill date has been set even
         # though the package isn't billing yet. Start billing as though that was the
         # start date.
-        $sdate = $cust_pkg->bill;
+        $$sdate = $cust_pkg->bill;
         $cust_pkg->setup($cust_pkg->bill);
       }
       # Now figure the start and end of the period that contains the start date.
index 6b0c914..a3f0612 100644 (file)
@@ -716,7 +716,10 @@ sub estimate {
     my $cust_main;
     if ( $cust_or_prospect->isa('FS::prospect_main') ) {
       $cust_main = $cust_or_prospect->convert_cust_main;
-      die "$cust_main (simulating customer signup)\n" unless ref $cust_main;
+      unless ( ref($cust_main) ) {
+        $temp_dbh->rollback;
+        die "$cust_main (simulating customer signup)\n";
+      }
       $fake_self->set('prospectnum', '');
       $fake_self->set('custnum', $cust_main->custnum);
     } else {
@@ -726,7 +729,10 @@ sub estimate {
     # order packages
     local($FS::cust_pkg::disable_start_on_hold) = 1;
     $error = $fake_self->order(\%pkgnum_of);
-    die "$error (simulating package order)\n" if $error;
+    if ( $error ) {
+      $temp_dbh->rollback;
+      die "$error (simulating package order)\n";
+    }
 
     my @new_pkgs = map { FS::cust_pkg->by_key($_) } values(%pkgnum_of);
 
@@ -739,7 +745,10 @@ sub estimate {
       'no_usage_reset'  => 1,
     );
     $error = $cust_main->bill(%bill_opt);
-    die "$error (simulating initial billing)\n" if $error;
+    if ( $error ) {
+      $temp_dbh->rollback;
+      die "$error (simulating initial billing)\n" if $error;
+    }
 
     # pick dates for future bills
     my %next_bill_pkgs;
@@ -755,7 +764,10 @@ sub estimate {
       $bill_opt{'return_bill'} = $return_bill[$i] = [];
       $bill_opt{'pkg_list'} = $next_bill_pkgs{$next_bill};
       $error = $cust_main->bill(%bill_opt);
-      die "$error (simulating recurring billing cycle $i)\n" if $error;
+      if ( $error ) {
+        $temp_dbh->rollback;
+        die "$error (simulating recurring billing cycle $i)\n";
+      }
       $i++;
     }
 
diff --git a/FS/bin/freeside-passwd b/FS/bin/freeside-passwd
new file mode 100755 (executable)
index 0000000..dbd566e
--- /dev/null
@@ -0,0 +1,19 @@
+#!/usr/bin/perl -w
+
+use strict;
+
+my $user = shift or die &usage;
+my $password = shift or die &usage;
+
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw( qsearchs );
+use FS::access_user;
+
+adminsuidsetup $user;
+
+my $access_user = qsearchs('access_user', {'username'=>$user})
+  or die "unknown username $user\n";
+my $error = $access_user->change_password($password);
+die $error if $error;
+
+1;
diff --git a/bin/update-rates b/bin/update-rates
new file mode 100644 (file)
index 0000000..b16fc7f
--- /dev/null
@@ -0,0 +1,42 @@
+#!/usr/bin/perl
+
+use FS::UID 'adminsuidsetup';
+use FS::Record qw(dbh qsearch qsearchs);
+use FS::tax_class;
+use FS::tax_rate;
+use strict;
+
+adminsuidsetup('ivan');
+$FS::UID::AutoCommit = 0;
+my @location = ( geocode => { op => 'like', value => '24%' } );
+
+# convert TELECOMM RELAY SYSTEMS SURCHARGE:CENTREX LINES
+# to      TELECOMM RELAY SYSTEMS SURCHARGE:TELECOMMUNICATIONS
+my $old_taxclassnum = qsearchs('tax_class', { 'taxclass' => '09:35' })->taxclassnum;
+my $new_taxclassnum = qsearchs('tax_class', { 'taxclass' => '09:00' })->taxclassnum;
+my $error;
+
+my @bindings = qsearch('part_pkg_taxrate', {
+  'taxclassnum' => $old_taxclassnum,
+  @location
+});
+print "remapping ".scalar(@bindings)." tax rate bindings.\n";
+foreach my $part_pkg_taxrate (@bindings) {
+  $part_pkg_taxrate->set('taxclassnum', $new_taxclassnum);
+  $error = $part_pkg_taxrate->replace;
+  die $part_pkg_taxrate->pkgtaxratenum .": $error" if $error;
+}
+
+# change the fee to 0.05.
+my @tax_rates = qsearch('tax_rate', {
+  taxclassnum => $new_taxclassnum,
+  @location
+});
+print "changing rate on ".scalar(@tax_rates)." tax rate definitions.\n";
+foreach my $tax_rate (@tax_rates) {
+  $tax_rate->set('fee', 0.05);
+  my $error = $tax_rate->replace;
+  die $tax_rate->taxnum . ": $error\n" if $error;
+}
+
+dbh->commit;
index fc3bae1..2391f73 100644 (file)
@@ -175,7 +175,7 @@ Description: Self-service portal html/cgi filesfor Freeside billing and trouble
 
 Package: freeside-ng-selfservice
 Architecture: all
-Depends: libapache2-mod-php5,php5-xmlrpc,apache2
+Depends: libapache2-mod-php|libapache2-mod-php5,php5-xmlrpc,apache2
 Recommends:
 Description: Next Generation Self-service portal for Freeside billing and trouble ticketing
  Freeside is a web-based billing and trouble ticketing application.
diff --git a/debian/freeside-webui.postinst b/debian/freeside-webui.postinst
new file mode 100644 (file)
index 0000000..8dd2baa
--- /dev/null
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+chown -R freeside /usr/local/etc/freeside
+rm -fr  /usr/local/etc/freeside/masondata/*
+
+#XXX systemd equivalent (start apache after postgres)
+/sbin/insserv -d
+
+exit 0
+
diff --git a/debian/freeside.postinst b/debian/freeside.postinst
new file mode 100644 (file)
index 0000000..3e7dc25
--- /dev/null
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+#probably not needed with systemd
+/usr/sbin/update-rc.d freeside defaults 23 01
+
+chown -R freeside /usr/local/etc/freeside
+rm -fr  /usr/local/etc/freeside/masondata/*
+
+exit 0
+
diff --git a/debian/postinst b/debian/postinst
deleted file mode 100644 (file)
index 09f9dae..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/sh
-
-chown -R freeside /usr/local/etc/freeside
-/usr/sbin/update-rc.d freeside defaults 23 01
-/sbin/insserv -d
-rm -fr  /usr/local/etc/freeside/masondata/*
-
-exit 0
-
index 1eb312b..041c895 100755 (executable)
@@ -228,8 +228,9 @@ install-stamp: build-stamp
 
        # Torrus 
        
-       install -d ${TORRUS_CONF}
-       install -o root -m 755 htetc/freeside-torrus.conf $(TORRUS_CONF)/
+       #in freeside-webui package
+       #install -d ${TORRUS_CONF}
+       #install -o root -m 755 htetc/freeside-torrus.conf $(TORRUS_CONF)/
 
        ( cd torrus; \
        torrus_user=freeside var_user=freeside var_group=freeside ./configure; \
index bc3c1b3..29a9fe0 100644 (file)
@@ -28,7 +28,7 @@
 % } else {
   <FONT SIZE="-1">
 % }
-&copy; 2017 Freeside Internet Services, Inc.<BR>
+&copy; 2018 Freeside Internet Services, Inc.<BR>
 All rights reserved.<BR>
 Licensed under the terms of the<BR>
 GNU <b>Affero</b> General Public License.<BR>
@@ -56,7 +56,7 @@ GNU <b>Affero</b> General Public License.<BR>
 
 % unless ( $agentnum ) {
   <CENTER>
-  <FONT SIZE="-3">"I've heard it too many times to ignore it / Its's something that I'm supposed to be" - K. Frog</FONT>
+  <FONT SIZE="-3">"Then, when all seemed to be lost, a small glimmer of light appeared in the distance.  Could it be...  back from the great beyond..." - D. 3030</FONT>
   </CENTER>
 % }
 
index 9d25853..f40575c 100644 (file)
                    sub {
                      my $access_user = shift;
 
-                     '<BR>Employee Groups<BR>'.
-                     ntable("#cccccc",2).
+                     '<BR>'.
+                     '<FONT CLASS="fsinnerbox-title">Employee Groups</FONT>'.
+                     '<BR>'.
+                     '<TABLE CLASS="fsinnerbox">'.
                      '<TR><TD>'.
                      include( '/elements/checkboxes-table.html',
                                 'source_obj'   => $access_user,
@@ -49,6 +51,7 @@
                    },
                  'onsubmit'  => 'check_user_custnum_search',
                  'html_foot' => $check_user_custnum_search,
+                 'html_table_class' => 'fsinnerbox',
            )
 %>
 <%init>
index 54d2b03..fcd210f 100644 (file)
@@ -11,7 +11,7 @@
                                        'target_table' => 'access_group',
                                      },
                  'precheck_callback'        => \&precheck_callback,
-                 'post_new_object_callback' => \&post_new_object_callback,
+                 #'post_new_object_callback' => \&post_new_object_callback,
                  'noerror_callback'         => \&noerror_callback,
              )
 %>
@@ -38,20 +38,24 @@ sub precheck_callback {
   return '';
 }
 
-sub post_new_object_callback {
+#sub post_new_object_callback {
+#  my( $cgi, $access_user ) = @_;
+#
+#  if ( length($cgi->param('_password')) ) {
+#    my $password = scalar($cgi->param('_password'));
+#    my $error = $access_user->is_password_allowed($password);
+#    #XXX and then bubble the error back up to the UI
+#  }
+#}
+
+sub noerror_callback {
   my( $cgi, $access_user ) = @_;
 
   if ( length($cgi->param('_password')) ) {
     my $password = scalar($cgi->param('_password'));
-    my $error = $access_user->is_password_allowed($password)
-             || $access_user->change_password($password);
+    $access_user->change_password($password);
   }
 
-}
-
-sub noerror_callback {
-  my( $cgi, $access_user ) = @_;
-
   #handle installer checkbox
   my @sched_item = $access_user->sched_item;
   my $sched_item = $sched_item[0];
index 7fcc1da..02b01f8 100644 (file)
@@ -40,19 +40,54 @@ if ( $cgi->param('locationnum') == -1 ) {
   $change{'cust_location'} = $cust_location;
 }
 
+my $error;
+
+# Discounts:
+# setup_discountnum and change_discountnum may contain one of the following:
+# - "-1" Represents the 'other' option.  Results in a new entry to the
+#        discount table.
+# - "-2" Represents the "waive setup fee" option. Sets cust_pkg.waive_setup = Y
+# - int  Represents the id for a discount row: discount.discountnum
+# my %discount;
+# $change{waive_setup} = '';
+# for my $type (qw|setup recur|) {
+#   my $dnum = $cgi->param("${type}_discountnum");
+
+#   if ($dnum eq '-2' && $type eq 'setup') {
+#     $change{waive_setup} = 'Y';
+#   } elsif ($val =~ /^-?\d+$/) {
+#     $discount{$type} = {discountnum => $dnum};
+#     if ($dnum eq '-1') {
+#       $discount{$type}->{amount}  = $cgi->param("${type}_discountnum_amount");
+#       $discount{$type}->{percent} = $cgi->param("${type}_discountnum_percent");
+#     }
+#   } else {
+#     # Shouldn't happen without funny business
+#     $error = "Bad value ${type}_discountnum ($val)";
+#   }
+# }
+
+
 $change{waive_setup} = '';
+for my $type (qw|setup_discountnum recur_discountnum|) {
+  my $dnum = $cgi->param($type);
 
-if ( $cgi->param('setup_discountnum') =~ /^(-?\d+)$/ ) { 
-  if ( $1 == -2 ) {
+  if ($dnum eq '-2' && $type eq 'setup_discountnum') {
+    # Waive Discount
     $change{waive_setup} = 'Y';
+  } elsif ($dnum =~ /^-?\d+$/) {
+    # Set discountnum
+    $change{$type} = $dnum;
+    $change{"${type}_amount"}  = $cgi->param("${type}_amount");
+    $change{"${type}_percent"} = $cgi->param("${type}_percent");
+  } elsif ($dnum eq '') {
+    # Set discount as no discount
+    $change{"${type}"} = 0;
   } else {
-    $change{setup_discountnum} = $1;
-    $change{setup_discountnum_amount} = $cgi->param('setup_discountnum_amount');
-    $change{setup_discountnum_percent} = $cgi->param('setup_discountnum_percent');
+    $error = "Bad value ${type}_discountnum ($dnum)";
   }
 }
 
-my $error;
 my $now = time;
 if (defined($cgi->param('contract_end'))) {
   $change{'contract_end'} = parse_datetime($cgi->param('contract_end'));
index 9b8b2cd..eb065b6 100644 (file)
@@ -518,8 +518,9 @@ tie my %tools_importing, 'Tie::IxHash',
   'Customers'            => [ $fsurl.'misc/cust_main-import.cgi', '' ],
   'Package definitions'  => [ $fsurl.'misc/part_pkg-import.html', '' ],
   'Customer packages'    => [ $fsurl.'misc/cust_pkg-import.html', '' ],
+#  'Customer broadband services' => [ $fsurl.'misc/svc_broadband-import.html', '' ],
   'Customer notes'       => [ $fsurl.'misc/cust_main_note-import.html', '' ],
-  'Customer Contacts'    => [ $fsurl.'misc/contact-import.cgi', '' ],
+  'Customer contacts'    => [ $fsurl.'misc/contact-import.cgi', '' ],
   'One-time charges'     => [ $fsurl.'misc/cust_main-import_charges.cgi', '' ],
   'Payments'             => [ $fsurl.'misc/cust_pay-import.cgi', '' ],
   'Credits'              => [ $fsurl.'misc/cust_credit-import.html', '' ],
index 243da93..2470ee1 100755 (executable)
 % if ( $discount_cust_pkg || $waive_setup_fee ) {
   <FONT CLASS="fsinnerbox-title"><% mt('Discounting') |h %></FONT>
   <TABLE CLASS="fsinnerbox">
-    <& /elements/tr-select-pkg-discount.html, disable_recur => 1, &>
+    <& /elements/tr-select-pkg-discount.html,
+      curr_value_setup    => $discount{setup},
+      curr_value_recur    => $discount{recur},
+      disable_setup       => 0,
+      disable_recur       => 0,
+      disable_waive_setup => 0
+    &>
   </TABLE><BR>
 
 % }
@@ -168,4 +174,14 @@ if ( $cust_pkg->change_to_pkgnum ) {
   }
   $title = "Edit Scheduled Package Change";
 }
+
+# Get current values of discounts for selectboxes
+my %discount = (setup => undef, recur => undef);
+$discount{$_->setuprecur} = $_->discountnum
+  for qsearch('cust_pkg_discount', {
+    pkgnum   => $cust_pkg->pkgnum,
+    disabled => '',
+  });
+$discount{setup} = '-2' if $cust_pkg->waive_setup;
+
 </%init>
index 32a2fc5..49dda18 100755 (executable)
@@ -19,8 +19,8 @@ my $invnum = $1;
 
 my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
 
-my $custnum = $cust_bill->custnum;
-
-my $error = $cust_bill->void( scalar($cgi->param('reason')) );
+my $error = $cust_bill->void( scalar($cgi->param('reason')),
+                              scalar($cgi->param('reprocess_cdrs')),
+                            );
 
 </%init>
index e4e4705..e5fbdc6 100755 (executable)
              'cgi'            => $cgi
 &>
 
+% if ( $cust_bill->has_call_details ) {
+  <& /elements/tr-checkbox.html,
+       label => 'Reprocess CDRs',
+       field => 'reprocess_cdrs',
+       value => '1',
+  &>
+% }
+
 </TABLE>
 
 <BR>
index 2fde797..f4dd414 100644 (file)
@@ -108,8 +108,7 @@ my $prospect_main = qsearchs( {
 });
 die "Prospect not found!" unless $prospect_main;
 
-my $title = encode_entities($prospect_main->name);
-$title = mt("Prospect"). ": $title";
+my $title = mt("Prospect"). ': '. $prospect_main->name;
 $title .= ' ('.mt('DISABLED').')'
   if $prospect_main->disabled;