multi-currency, RT#21565
[freeside.git] / FS / FS / part_pkg / flat.pm
index f5a2100..9737a94 100644 (file)
@@ -1,19 +1,16 @@
 package FS::part_pkg::flat;
+use base qw( FS::part_pkg::prorate_Mixin
+             FS::part_pkg::discount_Mixin
+             FS::part_pkg
+           );
 
 use strict;
-use vars qw( @ISA %info
-             %usage_recharge_fields @usage_recharge_fieldorder
-           );
+use vars qw( %info %usage_recharge_fields @usage_recharge_fieldorder );
+use FS::Record qw( qsearch );
 use Tie::IxHash;
-use List::Util qw(min); # max);
-#use FS::Record qw(qsearch);
+use List::Util qw( min );
 use FS::UI::bytecount;
 use FS::Conf;
-use FS::part_pkg;
-
-@ISA = qw(FS::part_pkg 
-          FS::part_pkg::prorate_Mixin
-          FS::part_pkg::discount_Mixin);
 
 tie my %temporalities, 'Tie::IxHash',
   'upcoming'  => "Upcoming (future)",
@@ -28,7 +25,7 @@ tie my %contract_years, 'Tie::IxHash', (
 %info = (
   'name' => 'Flat rate (anniversary billing)',
   'shortname' => 'Anniversary',
-  'inherit_fields' => [ 'usage_Mixin', 'global_Mixin' ],
+  'inherit_fields' => [ 'prorate_Mixin', 'usage_Mixin', 'global_Mixin' ],
   'fields' => {
     #false laziness w/voip_cdr.pm
     'recur_temporality' => { 'name' => 'Charge recurring fee for period',
@@ -59,6 +56,13 @@ tie my %contract_years, 'Tie::IxHash', (
                                     'the customer\'s next bill date',
                           'type' => 'checkbox',
                         },
+    'prorate_round_day' => {
+                          'name' => 'When synchronizing, round the prorated '.
+                                    'period to the nearest full day',
+                          'type' => 'checkbox',
+                        },
+    'add_full_period' => { 'disabled' => 1 }, # doesn't make sense with sync?
+
     'suspend_bill' => { 'name' => 'Continue recurring billing while suspended',
                         'type' => 'checkbox',
                       },
@@ -67,7 +71,14 @@ tie my %contract_years, 'Tie::IxHash', (
                                     'unsuspending',
                           'type' => 'checkbox',
                         },
-
+    'bill_recur_on_cancel' => {
+                        'name' => 'Bill the last period on cancellation',
+                        'type' => 'checkbox',
+                        },
+    'bill_suspend_as_cancel' => {
+                        'name' => 'Bill immediately upon suspension', #desc?
+                        'type' => 'checkbox',
+                        },
     'externalid' => { 'name'   => 'Optional External ID',
                       'default' => '',
                     },
@@ -75,8 +86,11 @@ tie my %contract_years, 'Tie::IxHash', (
   'fieldorder' => [ qw( recur_temporality 
                         expire_months adjourn_months
                         contract_end_months
-                        start_1st sync_bill_date prorate_defer_bill
+                        start_1st
+                        sync_bill_date prorate_defer_bill prorate_round_day
                         suspend_bill unsuspend_adjust_bill
+                        bill_recur_on_cancel
+                        bill_suspend_as_cancel
                         externalid ),
                   ],
   'weight' => 10,
@@ -89,14 +103,14 @@ sub price_info {
     my $setup = $self->option('setup_fee') || 0;
     my $recur = $self->option('recur_fee', 1) || 0;
     my $str = '';
-    $str = $money_char . $setup . ' one-time' if $setup;
+    $str = $money_char . $setup . ($recur ? ' setup ' : ' one-time') if $setup;
     $str .= ', ' if ($setup && $recur);
-    $str .= $money_char . $recur . ' recurring ' if $recur;
+    $str .= $money_char. $recur. '/'. $self->freq_pretty if $recur;
     $str;
 }
 
 sub calc_setup {
-  my($self, $cust_pkg, $sdate, $details ) = @_;
+  my($self, $cust_pkg, $sdate, $details, $param ) = @_;
 
   return 0 if $self->prorate_setup($cust_pkg, $sdate);
 
@@ -108,11 +122,19 @@ sub calc_setup {
 
   my $quantity = $cust_pkg->quantity || 1;
 
-  my $charge = $quantity * $self->unit_setup($cust_pkg, $sdate, $details);
-  sprintf('%.2f', $charge);
+  my $charge = $quantity * $self->base_setup($cust_pkg, $sdate, $details);
+
+  my $discount = 0;
+  if ( $charge > 0 ) {
+      $param->{'setup_charge'} = $charge;
+      $discount = $self->calc_discount($cust_pkg, $sdate, $details, $param);
+      delete $param->{'setup_charge'};
+  }
+
+  sprintf('%.2f', $charge - $discount);
 }
 
-sub unit_setup {
+sub base_setup {
   my($self, $cust_pkg, $sdate, $details ) = @_;
 
   $self->option('setup_fee') || 0;
@@ -126,11 +148,12 @@ sub calc_recur {
   my $last_bill = $cust_pkg->get('last_bill'); #->last_bill falls back to setup
 
   return 0
-    if $self->option('recur_temporality', 1) eq 'preceding' && $last_bill == 0;
+    if $self->recur_temporality eq 'preceding' && !$last_bill;
 
   my $charge = $self->base_recur($cust_pkg, $sdate);
-  if ( my $cutoff_day = $self->cutoff_day($cust_pkg) ) {
-    $charge = $self->calc_prorate(@_);
+  # always treat cutoff_day as a list
+  if ( my @cutoff_day = $self->cutoff_day($cust_pkg) ) {
+    $charge = $self->calc_prorate(@_, @cutoff_day);
   }
   elsif ( $param->{freq_override} ) {
     # XXX not sure if this should be mutually exclusive with sync_bill_date.
@@ -139,6 +162,9 @@ sub calc_recur {
     $charge *= $param->{freq_override} if $param->{freq_override};
   }
 
+  my $quantity = $cust_pkg->quantity || 1;
+  $charge *= $quantity;
+
   my $discount = $self->calc_discount($cust_pkg, $sdate, $details, $param);
   return sprintf('%.2f', $charge - $discount);
 }
@@ -152,7 +178,7 @@ sub cutoff_day {
       return (localtime($next_bill))[3];
     }
   }
-  return 0;
+  return ();
 }
 
 sub base_recur {
@@ -168,6 +194,22 @@ sub base_recur_permonth {
   sprintf('%.2f', $self->base_recur($cust_pkg) / $self->freq );
 }
 
+sub calc_cancel {
+  my $self = shift;
+  my $conf = new FS::Conf;
+  if ( $self->recur_temporality eq 'preceding'
+       and $self->option('bill_recur_on_cancel', 1) ) {
+    # run another recurring cycle
+    return $self->calc_recur(@_);
+  }
+  elsif ( $conf->exists('bill_usage_on_cancel') # should be a package option?
+          and $self->can('calc_usage') ) {
+    # bill for outstanding usage
+    return $self->calc_usage(@_);
+  }
+  0;
+}
+
 sub calc_remain {
   my ($self, $cust_pkg, %options) = @_;
 
@@ -184,19 +226,28 @@ sub calc_remain {
               || ! $next_bill
               || $next_bill < $time;
 
-  my %sec = (
-    'h' =>    3600, # 60 * 60
-    'd' =>   86400, # 60 * 60 * 24
-    'w' =>  604800, # 60 * 60 * 24 * 7
-    'm' => 2629744, # 60 * 60 * 24 * 365.2422 / 12 
-  );
-
-  $self->freq =~ /^(\d+)([hdwm]?)$/
-    or die 'unparsable frequency: '. $self->freq;
-  my $freq_sec = $1 * $sec{$2||'m'};
-  return 0 unless $freq_sec;
-
-  sprintf("%.2f", $self->base_recur($cust_pkg, \$time) * ( $next_bill - $time ) / $freq_sec );
+  # Use actual charge for this period, not base_recur (for discounts).
+  # Use sdate < $time and edate >= $time because when billing on 
+  # cancellation, edate = $time.
+  my $credit = 0;
+  foreach my $item ( 
+    qsearch('cust_bill_pkg', { 
+      pkgnum => $cust_pkg->pkgnum,
+      sdate => {op => '<' , value => $time},
+      edate => {op => '>=', value => $time},
+      recur => {op => '>' , value => 0},
+    })
+  ) {
+    # hack to deal with the weird behavior of edate on package cancellation
+    my $edate = $item->edate;
+    if ( $self->recur_temporality eq 'preceding' ) {
+      $edate = $self->add_freq($item->sdate);
+    }
+    $credit += ($item->recur - $item->usage) * 
+               ($edate - $time) / ($edate - $item->sdate);
+  } 
+  sprintf('%.2f', $credit);
+  #sprintf("%.2f", $self->base_recur($cust_pkg, \$time) * ( $next_bill - $time ) / $freq_sec );
 
 }
 
@@ -206,10 +257,13 @@ sub is_free_options {
 
 sub is_prepaid { 0; } #no, we're postpaid
 
-#XXX discounts only on recurring fees for now (no setup/one-time or usage)
-sub can_discount {
+sub can_start_date { ! shift->option('start_1st', 1) }
+
+sub can_discount { 1; }
+
+sub recur_temporality {
   my $self = shift;
-  $self->freq =~ /^\d+$/ && $self->freq > 0;
+  $self->option('recur_temporality', 1);
 }
 
 sub usage_valuehash {