X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fpart_pkg%2Fprorate_Mixin.pm;h=0f40576be5d6488e5bdbf7197ec7a298f5910bd7;hp=2adf2f16a9853c5f69b0522072fae4191e8cf1bc;hb=8f3fc38083332407e7ea1ef6c01ea547947c98e7;hpb=ced6be92d868addbed9ff93b39bbd6a1f634bcb7 diff --git a/FS/FS/part_pkg/prorate_Mixin.pm b/FS/FS/part_pkg/prorate_Mixin.pm index 2adf2f16a..0f40576be 100644 --- a/FS/FS/part_pkg/prorate_Mixin.pm +++ b/FS/FS/part_pkg/prorate_Mixin.pm @@ -1,11 +1,40 @@ package FS::part_pkg::prorate_Mixin; use strict; -use vars qw(@ISA %info); -use Time::Local qw(timelocal); - -@ISA = qw(FS::part_pkg); -%info = ( 'disabled' => 1 ); +use vars qw( %info ); +use Time::Local qw( timelocal ); +use Date::Format qw( time2str ); + +%info = ( + 'disabled' => 1, + # define all fields that are referenced in this code + 'fields' => { + 'add_full_period' => { + 'name' => 'When prorating first month, also bill for one full '. + 'period after that', + 'type' => 'checkbox', + }, + 'prorate_round_day' => { + 'name' => 'When prorating, round to the nearest full day', + 'type' => 'checkbox', + }, + 'prorate_defer_bill' => { + 'name' => 'When prorating, defer the first bill until the '. + 'billing day', + 'type' => 'checkbox', + }, + 'prorate_verbose' => { + 'name' => 'Show prorate details on the invoice', + 'type' => 'checkbox', + }, + }, + 'fieldorder' => [ qw(prorate_defer_bill prorate_round_day + add_full_period prorate_verbose) ], +); + +sub fieldorder { + @{ $info{'fieldorder'} } +} =head1 NAME @@ -28,69 +57,175 @@ sub calc_recur { =head METHODS -=item calc_prorate +=item calc_prorate CUST_PKG SDATE DETAILS PARAM CUTOFF_DAY -Takes all the arguments of calc_recur, and calculates a prorated charge -in one of two ways: +Takes all the arguments of calc_recur. Calculates a prorated charge from +the $sdate to the cutoff day for this package definition, and sets the $sdate +and $param->{months} accordingly. base_recur() will be called to determine +the base price per billing cycle. -- If 'sync_bill_date' is set: Charge for a number of days to synchronize - this package to the customer's next bill date. If this is their only - package (or they're already synchronized), that will take them through - one billing cycle. -- If 'cutoff_day' is set: Prorate this package so that its next bill date - falls on that day of the month. +Options: +- add_full_period: Bill for the time up to the prorate day plus one full +billing period after that. +- prorate_round_day: Round the current time to the nearest full day, +instead of using the exact time. +- prorate_defer_bill: Don't bill the prorate interval until the prorate +day arrives. +- prorate_verbose: Generate details to explain the prorate calculations. =cut sub calc_prorate { - my $self = shift; - my ($cust_pkg, $sdate, $details, $param) = @_; - - my $charge = $self->option('recur_fee') || 0; - my $cutoff_day; - if( $self->option('sync_bill_date',1) ) { - my $next_bill = $cust_pkg->cust_main->next_bill_date; - if( defined($next_bill) and $next_bill != $$sdate ) { - $cutoff_day = (localtime($next_bill))[3]; - } - else { - # don't prorate, assume a full month - $param->{'months'} = $self->freq; - } + my ($self, $cust_pkg, $sdate, $details, $param, $cutoff_day) = @_; + die "no cutoff_day" unless $cutoff_day; + die "can't prorate non-monthly package\n" if $self->freq =~ /\D/; + + my $money_char = FS::Conf->new->config('money_char') || '$'; + + my $charge = $self->base_recur($cust_pkg, $sdate) || 0; + + my $add_period = $self->option('add_full_period',1); + + my $mnow = $$sdate; + + # if this is the first bill but the bill date has been set + # (by prorate_defer_bill), calculate from the setup date, + # append the setup fee to @$details, and make sure to bill for + # a full period after the bill date. + if ( $self->option('prorate_defer_bill',1) + && ! $cust_pkg->getfield('last_bill') + && $cust_pkg->setup + ) + { + #warn "[calc_prorate] #".$cust_pkg->pkgnum.": running deferred setup\n"; + $param->{'setup_fee'} = $self->calc_setup($cust_pkg, $$sdate, $details); + $mnow = $cust_pkg->setup; + $add_period = 1; } - else { # no sync, use cutoff_day or day 1 - $cutoff_day = $self->option('cutoff_day') || 1; + + my ($mend, $mstart); + ($mnow, $mend, $mstart) = $self->_endpoints($mnow, $cutoff_day); + + # next bill date will be figured as $$sdate + one period + $$sdate = $mstart; + + my $permonth = $charge / $self->freq; + my $months = ( ( $self->freq - 1 ) + ($mend-$mnow) / ($mend-$mstart) ); + + if ( $self->option('prorate_verbose',1) + and $months > 0 and $months < $self->freq ) { + push @$details, + 'Prorated (' . time2str('%b %d', $mnow) . + ' - ' . time2str('%b %d', $mend) . '): ' . $money_char . + sprintf('%.2f', $permonth * $months + 0.00000001 ); } - if($cutoff_day) { - # only works for freq >= 1 month; probably can't be fixed - my $mnow = $$sdate; - my ($sec, $min, $hour, $mday, $mon, $year) = (localtime($mnow))[0..5]; - my $mend; - my $mstart; - if ( $mday >= $cutoff_day ) { - $mend = - timelocal(0,0,0,$cutoff_day,$mon == 11 ? 0 : $mon + 1,$year+($mon==11)); - $mstart = - timelocal(0,0,0,$cutoff_day,$mon,$year); + # add a full period if currently billing for a partial period + # or periods up to freq_override if billing for an override interval + if ( ($param->{'freq_override'} || 0) > 1 ) { + $months += $param->{'freq_override'} - 1; + } + elsif ( $add_period && $months < $self->freq) { + + if ( $self->option('prorate_verbose',1) ) { + # calculate the prorated and add'l period charges + push @$details, + 'First full month: ' . $money_char . + sprintf('%.2f', $permonth); + } + + $months += $self->freq; + $$sdate = $self->add_freq($mstart); + } + + $param->{'months'} = $months; + #so 1.005 rounds to 1.01 + $charge = sprintf('%.2f', $permonth * $months + 0.00000001 ); + + return $charge; +} + +=item prorate_setup CUST_PKG SDATE + +Set up the package. This only has an effect if prorate_defer_bill is +set, in which case it postpones the next bill to the cutoff day. + +=cut + +sub prorate_setup { + my $self = shift; + my ($cust_pkg, $sdate) = @_; + my $cutoff_day = $self->cutoff_day($cust_pkg); + if ( ! $cust_pkg->bill + and $self->option('prorate_defer_bill',1) + and $cutoff_day + ) { + my ($mnow, $mend, $mstart) = $self->_endpoints($sdate, $cutoff_day); + # If today is the cutoff day, set the next bill and setup both to + # midnight today, so that the customer will be billed normally for a + # month starting today. + if ( $mnow - $mstart < 86400 ) { + $cust_pkg->setup($mstart); + $cust_pkg->bill($mstart); } else { - $mend = - timelocal(0,0,0,$cutoff_day,$mon,$year); - $mstart = - timelocal(0,0,0,$cutoff_day,$mon == 0 ? 11 : $mon - 1,$year-($mon==11)); + $cust_pkg->bill($mend); + } + return 1; + } + return 0; +} + +=item _endpoints TIME CUTOFF_DAY + +Given a current time and a day of the month to prorate to, return three +times: the start of the prorate interval (usually the current time), the +end of the prorate interval (i.e. the cutoff date), and the time one month +before the end of the prorate interval. + +=cut + +sub _endpoints { + my ($self, $mnow, $cutoff_day) = @_; + + # only works for freq >= 1 month; probably can't be fixed + my ($sec, $min, $hour, $mday, $mon, $year) = (localtime($mnow))[0..5]; + if( $self->option('prorate_round_day',1) ) { + # If the time is 12:00-23:59, move to the next day by adding 18 + # hours to $mnow. Because of DST this can end up from 05:00 to 18:59 + # but it's always within the next day. + $mnow += 64800 if $hour >= 12; + # Get the new day, month, and year. + ($mday,$mon,$year) = (localtime($mnow))[3..5]; + # Then set $mnow to midnight on that day. + $mnow = timelocal(0,0,0,$mday,$mon,$year); + } + my $mend; + my $mstart; + # if cutoff day > 28, force it to the 1st of next month + if ( $cutoff_day > 28 ) { + $cutoff_day = 1; + # and if we are currently after the 28th, roll the current day + # forward to that day + if ( $mday > 28 ) { + $mday = 1; + #set $mnow = $mend so the amount billed will be zero + $mnow = timelocal(0,0,0,1,$mon == 11 ? 0 : $mon + 1,$year+($mon==11)); } - - $$sdate = $mstart; - - my $permonth = $self->option('recur_fee', 1) / $self->freq; - my $months = ( ( $self->freq - 1 ) + ($mend-$mnow) / ($mend-$mstart) ); - - $param->{'months'} = $months; - $charge = sprintf('%.2f', $permonth * $months); } - my $discount = $self->calc_discount(@_); - return ($charge - $discount); + if ( $mday >= $cutoff_day ) { + $mend = + timelocal(0,0,0,$cutoff_day,$mon == 11 ? 0 : $mon + 1,$year+($mon==11)); + $mstart = + timelocal(0,0,0,$cutoff_day,$mon,$year); + } + else { + $mend = + timelocal(0,0,0,$cutoff_day,$mon,$year); + $mstart = + timelocal(0,0,0,$cutoff_day,$mon == 0 ? 11 : $mon - 1,$year-($mon==0)); + } + return ($mnow, $mend, $mstart); } 1;