[freeside-commits] branch master updated. f81c236b2f3b5fe8777b04ee78b793b301eef9fd

Mark Wells mark at 420.am
Sat Oct 15 21:07:45 PDT 2016


The branch, master has been updated
       via  f81c236b2f3b5fe8777b04ee78b793b301eef9fd (commit)
       via  605a9aa2bef153ebe5376ba7500e5a6b567a8b63 (commit)
       via  6a509099343ed155525c4304f1ad742cc6e4ce59 (commit)
       via  fbfd9445bb00c2a1c7f4386878edead9b50c482f (commit)
       via  8282a324857d658d17061e3f0867d8c7d71b098a (commit)
       via  c8d971625edc253e1fa011c08a96030c12dde3bb (commit)
       via  6eec422e339e7a7209cac18da71ba598ee18d7d2 (commit)
      from  d4114381c5d95e8acd0d0fc2bbc2b3528bde2ecf (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 f81c236b2f3b5fe8777b04ee78b793b301eef9fd
Author: Mark Wells <mark at freeside.biz>
Date:   Sat Oct 15 21:03:27 2016 -0700

    reconcile prorate-sync behavior with prorate rounding, #72928

diff --git a/FS/FS/part_pkg/flat.pm b/FS/FS/part_pkg/flat.pm
index 504def0..6fd9c7d 100644
--- a/FS/FS/part_pkg/flat.pm
+++ b/FS/FS/part_pkg/flat.pm
@@ -15,6 +15,7 @@ use Tie::IxHash;
 use List::Util qw( min );
 use FS::UI::bytecount;
 use FS::Conf;
+use Time::Local 'timelocal';
 
 #ask FS::UID to run this stuff for us later
 FS::UID->install_callback( sub {
@@ -182,13 +183,27 @@ sub cutoff_day {
   if ( $self->option('sync_bill_date',1) ) {
     my $next_bill = $cust_pkg->cust_main->next_bill_date;
     if ( $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];
+    } else {
+      # This is the customer's only active package and hasn't been billed
+      # yet, so set the cutoff day to either today or tomorrow, whichever
+      # would result in a full period after rounding.
+      my $setup = $cust_pkg->setup; # because it's "now"
+      my $rounding_mode = $self->option('prorate_round_day',1);
+      return () if !$setup or !$rounding_mode;
+      my ($sec, $min, $hour, $mday, $mon, $year) = localtime($setup);
+
+      if (   ( $rounding_mode == 1 and $hour >= 12 )
+          or ( $rounding_mode == 3 and ( $sec > 0 or $min > 0 or $hour > 0 ))
+      ) {
+        # then the prorate period will be rounded down to start from
+        # midnight tomorrow, so the cutoff day should be the current day +
+        # 1.
+        $setup = timelocal(59,59,23,$mday,$mon,$year) + 1;
+        $mday = (localtime($setup))[3];
+      }
+      # otherwise, it will be rounded up, so leave the cutoff day at today.
+      return $mday;
     }
   }
   return ();

commit 605a9aa2bef153ebe5376ba7500e5a6b567a8b63
Author: Mark Wells <mark at freeside.biz>
Date:   Sat Oct 15 21:03:17 2016 -0700

    improve testing of prorate-sync behavior, #72928, #42108, and #34622

diff --git a/FS/t/suite/05-prorate_sync_same_day.t b/FS/t/suite/05-prorate_sync_same_day.t
index 91a8efa..d08752e 100755
--- a/FS/t/suite/05-prorate_sync_same_day.t
+++ b/FS/t/suite/05-prorate_sync_same_day.t
@@ -5,10 +5,15 @@
 Tests the effect of ordering and activating two sync_bill_date packages on
 the same day. Ref RT#42108.
 
-Correct: If the packages have prorate_round_day = 1 (round nearest), or 3
-(round down) then the second package should be prorated one day short. If
-they have prorate_round_day = 2 (round up), they should be billed
-for the same amount. In both cases they should have the same next bill date.
+Formerly correct: If the packages have prorate_round_day = 1 (round
+nearest), or 3 (round down) then the second package should be prorated one
+day short. If they have prorate_round_day = 2 (round up), they should be
+billed for the same amount. In both cases they should have the same next
+bill date.
+
+Revised RT#72928: The second package should be prorated one day short only
+if the rounding mode is 1 (round nearest), as the nearest day is different
+for the two packages.
 
 =cut
 
@@ -81,7 +86,7 @@ foreach my $prorate_mode (1, 2, 3) {
   $error = $cust->bill_and_collect;
 
   # Check the amount billed.
-  if ( $prorate_mode == 1 or $prorate_mode == 3 ) {
+  if ( $prorate_mode == 1 ) {
     # it should be one day short, in March
     $recur = sprintf('%.2f', $recur * 30/31);
   }
diff --git a/FS/t/suite/10-prorate_sync_same_hour.t b/FS/t/suite/10-prorate_sync_same_hour.t
new file mode 100755
index 0000000..f1e3185
--- /dev/null
+++ b/FS/t/suite/10-prorate_sync_same_hour.t
@@ -0,0 +1,102 @@
+#!/usr/bin/perl
+
+=head2 DESCRIPTION
+
+Tests the effect of ordering and activating two sync_bill_date packages
+either both before or both after noon, less than an hour apart. Ref RT#42108
+and #72928.
+
+Correct: The packages should always end up with the same next bill date,
+and should be billed for a full period, except in the case where the first
+package starts at midnight and the rounding mode is "always round down".
+
+=cut
+
+use strict;
+use Test::More tests => 27;
+use FS::Test;
+use Date::Parse 'str2time';
+use Date::Format 'time2str';
+use Test::MockTime qw(set_fixed_time);
+use FS::cust_main;
+use FS::cust_pkg;
+use FS::Conf;
+my $FS= FS::Test->new;
+
+foreach my $prorate_mode (1, 2, 3) {
+  diag("prorate_round_day = $prorate_mode");
+  # Create a package def with the sync_bill_date option.
+  my $error;
+  my $old_part_pkg = $FS->qsearchs('part_pkg', { pkgpart => 5 });
+  my $part_pkg = $old_part_pkg->clone;
+  BAIL_OUT("existing pkgpart 5 is not a flat monthly package")
+    unless $part_pkg->freq eq '1' and $part_pkg->plan eq 'flat';
+  $error = $part_pkg->insert(
+    options => {  $old_part_pkg->options,
+                  'sync_bill_date' => 1,
+                  'prorate_round_day' => $prorate_mode, }
+  );
+
+  BAIL_OUT("can't configure package: $error") if $error;
+
+  my $pkgpart = $part_pkg->pkgpart;
+  # Create a clean customer with no other packages.
+  foreach my $hour (0, 8, 16) {
+    diag("$hour:00");
+    my $location = FS::cust_location->new({
+        address1  => '123 Example Street',
+        city      => 'Sacramento',
+        state     => 'CA',
+        country   => 'US',
+        zip       => '94901',
+    });
+    my $cust = FS::cust_main->new({
+        agentnum      => 1,
+        refnum        => 1,
+        last          => 'Customer',
+        first         => 'Sync bill date',
+        invoice_email => 'newcustomer at fake.freeside.biz',
+        payby         => 'BILL',
+        bill_location => $location,
+        ship_location => $location,
+    });
+    $error = $cust->insert;
+    BAIL_OUT("can't create test customer: $error") if $error;
+
+    my @pkgs;
+    # Create and bill the first package.
+    set_fixed_time(str2time("2016-03-10 $hour:00"));
+    $pkgs[0] = FS::cust_pkg->new({ pkgpart => $pkgpart });
+    $error = $cust->order_pkg({ 'cust_pkg' => $pkgs[0] });
+    BAIL_OUT("can't order package: $error") if $error;
+    $error = $cust->bill_and_collect;
+    # Check the amount billed.
+    my ($cust_bill_pkg) = $pkgs[0]->cust_bill_pkg;
+    my $recur = $part_pkg->base_recur;
+    ok( $cust_bill_pkg->recur == $recur, "first package recur is $recur" )
+      or diag("first package recur is ".$cust_bill_pkg->recur);
+
+    # Create and bill the second package.
+    set_fixed_time(str2time("2016-03-10 $hour:01"));
+    $pkgs[1] = FS::cust_pkg->new({ pkgpart => $pkgpart });
+    $error = $cust->order_pkg({ 'cust_pkg' => $pkgs[1] });
+    BAIL_OUT("can't order package: $error") if $error;
+    $error = $cust->bill_and_collect;
+
+    # Check the amount billed.
+    if ( $prorate_mode == 3 and $hour == 0 ) {
+      # special case: a start date of midnight won't be rounded down but any
+      # later start date will, so the second package will be one day short.
+      $recur = sprintf('%.2f', $recur * 30/31);
+    }
+    ($cust_bill_pkg) = $pkgs[1]->cust_bill_pkg;
+    ok( $cust_bill_pkg->recur == $recur, "second package recur is $recur" )
+      or diag("second package recur is ".$cust_bill_pkg->recur);
+
+    my @next_bill = map { time2str('%Y-%m-%d', $_->replace_old->get('bill')) } @pkgs;
+
+    ok( $next_bill[0] eq $next_bill[1],
+      "both packages will bill again on $next_bill[0]" )
+      or diag("first package bill date is $next_bill[0], second package is $next_bill[1]");
+  }
+}
diff --git a/FS/t/suite/11-prorate_sync_single_pkg.t b/FS/t/suite/11-prorate_sync_single_pkg.t
new file mode 100755
index 0000000..83308f5
--- /dev/null
+++ b/FS/t/suite/11-prorate_sync_single_pkg.t
@@ -0,0 +1,89 @@
+#!/usr/bin/perl
+
+=head2 DESCRIPTION
+
+Tests the effect of ordering a sync_bill_date package either before or
+after noon and billing it for two consecutive cycles, in all three prorate
+rounding modes (round nearest, round up, and round down). Ref RT#34622.
+
+Correct: It should be charged full price in both cycles regardless of
+the prorate rounding mode, as long as prorate rounding is enabled.
+
+=cut
+
+use strict;
+use Test::More tests => 18;
+use FS::Test;
+use Date::Parse 'str2time';
+use Date::Format 'time2str';
+use Test::MockTime qw(set_fixed_time);
+use FS::cust_main;
+use FS::cust_pkg;
+use FS::Conf;
+my $FS= FS::Test->new;
+
+foreach my $prorate_mode (1, 2, 3) {
+  diag("prorate_round_day = $prorate_mode");
+  # Create a package def with the sync_bill_date option.
+  my $error;
+  my $old_part_pkg = $FS->qsearchs('part_pkg', { pkgpart => 5 });
+  my $part_pkg = $old_part_pkg->clone;
+  BAIL_OUT("existing pkgpart 5 is not a flat monthly package")
+    unless $part_pkg->freq eq '1' and $part_pkg->plan eq 'flat';
+  $error = $part_pkg->insert(
+    options => {  $old_part_pkg->options,
+                  'sync_bill_date' => 1,
+                  'prorate_round_day' => $prorate_mode, }
+  );
+
+  BAIL_OUT("can't configure package: $error") if $error;
+
+  my $pkgpart = $part_pkg->pkgpart;
+  # Create a clean customer with no other packages.
+  foreach my $hour (0, 8, 16) {
+    diag("$hour:00");
+    my $location = FS::cust_location->new({
+        address1  => '123 Example Street',
+        city      => 'Sacramento',
+        state     => 'CA',
+        country   => 'US',
+        zip       => '94901',
+    });
+    my $cust = FS::cust_main->new({
+        agentnum      => 1,
+        refnum        => 1,
+        last          => 'Customer',
+        first         => 'Sync bill date',
+        invoice_email => 'newcustomer at fake.freeside.biz',
+        payby         => 'BILL',
+        bill_location => $location,
+        ship_location => $location,
+    });
+    $error = $cust->insert;
+    BAIL_OUT("can't create test customer: $error") if $error;
+
+    my $pkg;
+    # Create and bill the package.
+    set_fixed_time(str2time("2016-03-10 $hour:00"));
+    $pkg = FS::cust_pkg->new({ pkgpart => $pkgpart });
+    $error = $cust->order_pkg({ 'cust_pkg' => $pkg });
+    BAIL_OUT("can't order package: $error") if $error;
+    $error = $cust->bill_and_collect;
+    BAIL_OUT("can't bill package: $error") if $error;
+
+    # Bill it a second time.
+    $pkg = $pkg->replace_old;
+    set_fixed_time($pkg->bill);
+    $error = $cust->bill_and_collect;
+    BAIL_OUT("can't bill package: $error") if $error;
+
+    # Check the amount billed.
+    my $recur = $part_pkg->base_recur;
+    my @cust_bill = $cust->cust_bill;
+    ok( $cust_bill[0]->charged == $recur, "first bill is $recur" )
+      or diag("first bill is ".$cust_bill[0]->charged);
+    ok( $cust_bill[1]->charged == $recur, "second bill is $recur" )
+      or diag("second bill is ".$cust_bill[1]->charged);
+
+  }
+}

commit 6a509099343ed155525c4304f1ad742cc6e4ce59
Merge: fbfd944 d411438
Author: Mark Wells <mark at freeside.biz>
Date:   Sat Oct 15 21:06:04 2016 -0700

    Merge branch 'master' of git.freeside.biz:/home/git/freeside


commit fbfd9445bb00c2a1c7f4386878edead9b50c482f
Author: Mark Wells <mark at freeside.biz>
Date:   Fri Oct 14 12:04:45 2016 -0700

    and show the flag in view UI, #38191

diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html
index 7ee05a3..8d3925a 100644
--- a/httemplate/view/cust_main/billing.html
+++ b/httemplate/view/cust_main/billing.html
@@ -83,6 +83,7 @@ set_display_recurring(<% encode_json({'display_recurring' => [ $cust_main->displ
 <TR>
   <TH ALIGN="right"><% mt('Prorate day of month') |h %></TH>
   <TD><% $cust_main->prorate_day %>
+  <% $cust_main->force_prorate_day && ('<i>'.emt('(applies to all packages)').'</i>') %>
   </TD>
 </TR>
 % }

commit 8282a324857d658d17061e3f0867d8c7d71b098a
Author: Mark Wells <mark at freeside.biz>
Date:   Fri Oct 14 09:34:06 2016 -0700

    cursorize part_event::initialize so that it works on large data sets

diff --git a/FS/FS/part_event.pm b/FS/FS/part_event.pm
index 58e0127..1c23899 100644
--- a/FS/FS/part_event.pm
+++ b/FS/FS/part_event.pm
@@ -6,6 +6,7 @@ use vars qw( $DEBUG );
 use Carp qw(confess);
 use FS::Record qw( dbh qsearch qsearchs );
 use FS::Conf;
+use FS::Cursor;
 use FS::part_event_option;
 use FS::part_event_condition;
 use FS::cust_event;
@@ -251,10 +252,28 @@ but can be useful when configuring events.
 
 =cut
 
-sub targets {
+sub targets { # may want to cursor this also
   my $self = shift;
   my %opt = @_;
-  my $time = $opt{'time'} || time;
+  my $time = $opt{'time'} ||= time;
+  
+  my $query = $self->_target_query(%opt);
+  my @objects = qsearch($query);
+  my @tested_objects;
+  foreach my $object ( @objects ) {
+    my $cust_event = $self->new_cust_event($object, 'time' => $time);
+    next unless $cust_event->test_conditions;
+
+    $object->set('cust_event', $cust_event);
+    push @tested_objects, $object;
+  }
+  @tested_objects;
+}
+
+sub _target_query {
+  my $self = shift;
+  my %opt = @_;
+  my $time = $opt{'time'};
 
   my $eventpart = $self->eventpart;
   $eventpart =~ /^\d+$/ or die "bad eventpart $eventpart";
@@ -285,23 +304,15 @@ sub targets {
   # and don't enforce disabled because we want to be able to see targets 
   # for a disabled event
 
-  my @objects = qsearch({
+  {
       table     => $eventtable,
       hashref   => {},
       addl_from => $join,
       extra_sql => "WHERE $where",
-  });
-  my @tested_objects;
-  foreach my $object ( @objects ) {
-    my $cust_event = $self->new_cust_event($object, 'time' => $time);
-    next unless $cust_event->test_conditions;
-
-    $object->set('cust_event', $cust_event);
-    push @tested_objects, $object;
-  }
-  @tested_objects;
+  };
 }
 
+
 =item initialize PARAMS
 
 Identify all objects eligible for this event and create L<FS::cust_event>
@@ -323,26 +334,26 @@ sub initialize {
   my $self = shift;
   my $error;
 
-  my $oldAutoCommit = $FS::UID::AutoCommit;
-  local $FS::UID::AutoCommit = 0;
-  my $dbh = dbh;
+  my $time = time;
+
+  local $FS::UID::AutoCommit = 1;
+  my $cursor = FS::Cursor->new( $self->_target_query('time' => $time) );
+  while (my $object = $cursor->fetch) {
+
+    my $cust_event = $self->new_cust_event($object, 'time' => $time);
+    next unless $cust_event->test_conditions;
 
-  my @objects = $self->targets;
-  foreach my $object ( @objects ) {
-    my $cust_event = $object->get('cust_event');
     $cust_event->status('initial');
     $error = $cust_event->insert;
-    last if $error;
+    die $error if $error;
   }
-  if ( !$error and $self->disabled ) {
+
+  # on successful completion only, re-enable the event
+  if ( $self->disabled ) {
     $self->disabled('');
     $error = $self->replace;
+    die $error if $error;
   }
-  if ( $error ) {
-    $dbh->rollback;
-    return $error;
-  }
-  $dbh->commit if $oldAutoCommit;
   return;
 }
 

commit c8d971625edc253e1fa011c08a96030c12dde3bb
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Oct 13 20:31:59 2016 -0700

    script to manually initialize events, #72949

diff --git a/bin/initialize-event b/bin/initialize-event
new file mode 100755
index 0000000..f186e19
--- /dev/null
+++ b/bin/initialize-event
@@ -0,0 +1,68 @@
+#!/usr/bin/perl
+
+use FS::Misc::Getopt;
+use FS::part_event;
+use FS::cust_event;
+use FS::Record 'dbdef';
+use FS::Cursor;
+
+getopts('e:x');
+
+my $eventpart = $opt{e};
+my $part_event = FS::part_event->by_key($opt{e})
+  or die "usage: initialize-event -e <eventpart> <username>\n";
+
+
+my $eventtable = $part_event->eventtable;
+my $pkey = dbdef->table($eventtable)->primary_key;
+my $from = " LEFT JOIN (SELECT DISTINCT tablenum AS $pkey FROM cust_event
+                   WHERE eventpart = $eventpart) AS done USING ($pkey)",
+my $where = " WHERE done.$pkey IS NULL";
+
+my $count = FS::Record->scalar_sql("SELECT COUNT(*) FROM $eventtable $from $where");
+print "Event ".$part_event->event."\n".
+      "Will initialize on $count $eventtable records.\n";
+if (!$opt{x}) {
+  print "Run with -x to make changes.\n";
+  exit;
+}
+
+
+print "Disabling event.\n";
+$part_event->disabled('Y');
+my $error = $part_event->replace;
+die $error if $error;
+my $cursor = FS::Cursor->new({
+  table => $eventtable,
+  addl_from => $from,
+  extra_sql => $where,
+});
+my $user = $FS::CurrentUser::CurrentUser->username;
+my $statustext = "Manually by $user";
+while (my $record = $cursor->fetch) {
+  my $cust_event = FS::cust_event->new({
+    status      => 'initial',
+    eventpart   => $eventpart,
+    tablenum    => $record->get($pkey),
+    _date       => $^T,
+    statustext  => $statustext,
+  });
+  $error = $cust_event->insert;
+  if ($error) {
+    print "$eventtable #".$record->get($pkey).": $error\n" if $error;
+  } else {
+    $count--;
+  }
+}
+print "$count unprocessed records.";
+if ($count == 0) {
+  print "Re-enabling event.\n";
+  $part_event->disabled('');
+  $error = $part_event->replace;
+  die $error if $error;
+} else {
+  print "Event is still disabled.\n";
+}
+
+print "Finished.\n";
+

commit 6eec422e339e7a7209cac18da71ba598ee18d7d2
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Oct 13 15:35:26 2016 -0700

    per-customer option to force anniversary packages to prorate, #38191

diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index a1615b7..f8b82f4 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1641,6 +1641,7 @@ sub tables_hashref {
         'accountcode_cdr', 'char', 'NULL', 1, '', '',
         'billday',   'int', 'NULL', '', '', '',
         'prorate_day',   'int', 'NULL', '', '', '',
+        'force_prorate_day', 'char', 'NULL', 1, '', '',
         'edit_subject', 'char', 'NULL', 1, '', '',
         'locale', 'varchar', 'NULL', 16, '', '', 
         'calling_list_exempt', 'char', 'NULL', 1, '', '',
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 11d7763..9c8e374 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -1775,6 +1775,7 @@ sub check {
     || $self->ut_floatn('credit_limit')
     || $self->ut_numbern('billday')
     || $self->ut_numbern('prorate_day')
+    || $self->ut_flag('force_prorate_day')
     || $self->ut_flag('edit_subject')
     || $self->ut_flag('calling_list_exempt')
     || $self->ut_flag('invoice_noemail')
diff --git a/FS/FS/part_pkg/flat.pm b/FS/FS/part_pkg/flat.pm
index 97d4363..504def0 100644
--- a/FS/FS/part_pkg/flat.pm
+++ b/FS/FS/part_pkg/flat.pm
@@ -173,6 +173,12 @@ sub calc_recur {
 sub cutoff_day {
   my $self = shift;
   my $cust_pkg = shift;
+  my $cust_main = $cust_pkg->cust_main;
+  # force it to act like a prorate package, is what this means
+  # because we made a distinction once between prorate and flat packages
+  if ( $cust_main->force_prorate_day  and $cust_main->prorate_day ) {
+     return ( $cust_main->prorate_day );
+  }
   if ( $self->option('sync_bill_date',1) ) {
     my $next_bill = $cust_pkg->cust_main->next_bill_date;
     if ( $next_bill ) {
diff --git a/FS/FS/part_pkg/prorate_calendar.pm b/FS/FS/part_pkg/prorate_calendar.pm
index c50cae0..a8ed8f9 100644
--- a/FS/FS/part_pkg/prorate_calendar.pm
+++ b/FS/FS/part_pkg/prorate_calendar.pm
@@ -72,7 +72,11 @@ sub check {
 sub cutoff_day {
   my( $self, $cust_pkg ) = @_;
   my @periods = @{ $freq_cutoff_days{$self->freq} };
-  my @cutoffs = ($self->option('cutoff_day') || 1); # Jan 1 = 1
+  my $prorate_day = $cust_pkg->cust_main->prorate_day
+                    || $self->option('cutoff_day')
+                    || 1;
+
+  my @cutoffs = ($prorate_day);
   pop @periods; # we don't care about the last one
   foreach (@periods) {
     push @cutoffs, $cutoffs[-1] + $_;
diff --git a/FS/FS/part_pkg/recur_Common.pm b/FS/FS/part_pkg/recur_Common.pm
index b73c62c..4ed83a4 100644
--- a/FS/FS/part_pkg/recur_Common.pm
+++ b/FS/FS/part_pkg/recur_Common.pm
@@ -41,13 +41,14 @@ sub cutoff_day {
   # prorate/subscription only; we don't support sync_bill_date here
   my( $self, $cust_pkg ) = @_;
   my $recur_method = $self->option('recur_method',1) || 'anniversary';
-  return () unless $recur_method eq 'prorate'
-                || $recur_method eq 'subscription';
+  my $cust_main = $cust_pkg->cust_main;
 
-  #false laziness w/prorate.pm::cutoff_day
-  my $prorate_day = $cust_pkg->cust_main->prorate_day;
-  $prorate_day ? ( $prorate_day )
-               : split(/\s*,\s*/, $self->option('cutoff_day', 1) || '1');
+  if ( $cust_main->force_prorate_day and $cust_main->prorate_day ) {
+     return ( $cust_main->prorate_day );
+  } elsif ($recur_method eq 'prorate' || $recur_method eq 'subscription') {
+
+    return split(/\s*,\s*/, $self->option('cutoff_day', 1) || '1');
+  }
 }
 
 sub calc_recur_Common {
diff --git a/FS/FS/part_pkg/subscription.pm b/FS/FS/part_pkg/subscription.pm
index 0dfe049..bf644d4 100644
--- a/FS/FS/part_pkg/subscription.pm
+++ b/FS/FS/part_pkg/subscription.pm
@@ -88,6 +88,11 @@ use FS::part_pkg::flat;
 sub calc_recur {
   my($self, $cust_pkg, $sdate, $details, $param ) = @_;
   my $cutoff_day = $self->option('cutoff_day', 1) || 1;
+  my $cust_main = $cust_pkg->cust_main;
+  if ( $cust_main->force_prorate_day  and $cust_main->prorate_day ) {
+     $cutoff_day = $cust_main->prorate_day;
+  }
+
   my $mnow = $$sdate;
   my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($mnow) )[0,1,2,3,4,5];
 
diff --git a/httemplate/edit/cust_main/billing.html b/httemplate/edit/cust_main/billing.html
index 50262e8..649c4c9 100644
--- a/httemplate/edit/cust_main/billing.html
+++ b/httemplate/edit/cust_main/billing.html
@@ -124,6 +124,12 @@
         <SELECT NAME="prorate_day">
           <% prorate_day_options($cust_main->prorate_day) %>
         </SELECT>
+        <& /elements/checkbox.html,
+          field       => 'force_prorate_day',
+          value       => 'Y',
+          curr_value  => $cust_main->force_prorate_day
+        &>
+        <label><% emt('Force all packages to this day') %></label>
       </TD>
     </TR>
 
diff --git a/httemplate/elements/freeside.css b/httemplate/elements/freeside.css
index 8545ee5..c98fdcb 100644
--- a/httemplate/elements/freeside.css
+++ b/httemplate/elements/freeside.css
@@ -232,7 +232,8 @@ div.fstabcontainer {
   border-radius: .25em;
 }
 
-.fsinnerbox th {
+.fsinnerbox th,
+.fsinnerbox label {
   font-weight:normal;
   font-size:80%;
   vertical-align: top;

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

Summary of changes:
 FS/FS/Schema.pm                         |    1 +
 FS/FS/cust_main.pm                      |    1 +
 FS/FS/part_event.pm                     |   63 +++++++++++--------
 FS/FS/part_pkg/flat.pm                  |   33 ++++++++--
 FS/FS/part_pkg/prorate_calendar.pm      |    6 +-
 FS/FS/part_pkg/recur_Common.pm          |   13 ++--
 FS/FS/part_pkg/subscription.pm          |    5 ++
 FS/t/suite/05-prorate_sync_same_day.t   |   15 +++--
 FS/t/suite/10-prorate_sync_same_hour.t  |  102 +++++++++++++++++++++++++++++++
 FS/t/suite/11-prorate_sync_single_pkg.t |   89 +++++++++++++++++++++++++++
 bin/initialize-event                    |   68 +++++++++++++++++++++
 httemplate/edit/cust_main/billing.html  |    6 ++
 httemplate/elements/freeside.css        |    3 +-
 httemplate/view/cust_main/billing.html  |    1 +
 14 files changed, 361 insertions(+), 45 deletions(-)
 create mode 100755 FS/t/suite/10-prorate_sync_same_hour.t
 create mode 100755 FS/t/suite/11-prorate_sync_single_pkg.t
 create mode 100755 bin/initialize-event




More information about the freeside-commits mailing list