Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorIvan Kohler <ivan@freeside.biz>
Wed, 26 Feb 2014 21:15:13 +0000 (13:15 -0800)
committerIvan Kohler <ivan@freeside.biz>
Wed, 26 Feb 2014 21:15:13 +0000 (13:15 -0800)
FS/FS/Schema.pm
FS/FS/cust_bill_pkg_fee.pm
FS/FS/cust_event_fee.pm
FS/FS/cust_main/Billing.pm
FS/FS/part_event/Action/Mixin/fee.pm
FS/FS/part_event/Action/cust_bill_fee.pm
FS/FS/part_event/Action/cust_fee.pm
FS/FS/part_fee.pm
httemplate/edit/part_fee.html

index bf516b2..795b97f 100644 (file)
@@ -943,6 +943,7 @@ sub tables_hashref {
         'eventnum',       'int', '', '', '', '',
         'billpkgnum',     'int', 'NULL', '', '', '',
         'feepart',        'int', '', '', '', '',
+        'nextbill',      'char', 'NULL',  1, '', '',
       ],
       'primary_key'  => 'eventfeenum', # I'd rather just use eventnum
       'unique' => [ [ 'billpkgnum' ], [ 'eventnum' ] ], # one-to-one link
index 8ea73c9..b9adfaf 100644 (file)
@@ -26,8 +26,8 @@ FS::cust_bill_pkg_fee - Object methods for cust_bill_pkg_fee records
 =head1 DESCRIPTION
 
 An FS::cust_bill_pkg_fee object records the origin of a fee.  
-.  FS::cust_bill_pkg_fee inherits from
-FS::Record.  The following fields are currently supported:
+FS::cust_bill_pkg_fee inherits from FS::Record.  The following fields 
+are currently supported:
 
 =over 4
 
@@ -70,8 +70,8 @@ sub check {
   my $error = 
     $self->ut_numbern('billpkgfeenum')
     || $self->ut_number('billpkgnum')
-    || $self->ut_foreign_key('origin_invnum', 'cust_bill', 'invnum')
-    || $self->ut_foreign_keyn('origin_billpkgnum', 'cust_bill_pkg', 'billpkgnum')
+    || $self->ut_foreign_key('base_invnum', 'cust_bill', 'invnum')
+    || $self->ut_foreign_keyn('base_billpkgnum', 'cust_bill_pkg', 'billpkgnum')
     || $self->ut_money('amount')
   ;
   return $error if $error;
index 78794fd..d924485 100644 (file)
@@ -45,6 +45,9 @@ time billing runs for the customer.
 
 =item feepart - key of the fee definition (L<FS::part_fee>).
 
+=item nextbill - 'Y' if the fee should be charged on the customer's next
+bill, rather than causing a bill to be produced immediately.
+
 =back
 
 =head1 METHODS
@@ -93,6 +96,7 @@ sub check {
     || $self->ut_foreign_key('eventnum', 'cust_event', 'eventnum')
     || $self->ut_foreign_keyn('billpkgnum', 'cust_bill_pkg', 'billpkgnum')
     || $self->ut_foreign_key('feepart', 'part_fee', 'feepart')
+    || $self->ut_flag('nextbill')
   ;
   return $error if $error;
 
index f4c30ce..a7e7d19 100644 (file)
@@ -533,8 +533,6 @@ sub bill {
 
     my @cust_bill_pkg = _omit_zero_value_bundles(@{ $cust_bill_pkg{$pass} });
 
-    next unless @cust_bill_pkg; #don't create an invoice w/o line items
-
     warn "$me billing pass $pass\n"
            #.Dumper(\@cust_bill_pkg)."\n"
       if $DEBUG > 2;
@@ -547,11 +545,24 @@ sub bill {
       hashref => { 'billpkgnum' => '' }
     );
     warn "$me found pending fee events:\n".Dumper(\@pending_event_fees)."\n"
-      if @pending_event_fees;
+      if @pending_event_fees and $DEBUG > 1;
+
+    # determine whether to generate an invoice
+    my $generate_bill = scalar(@cust_bill_pkg) > 0;
+
+    foreach my $event_fee (@pending_event_fees) {
+      $generate_bill = 1 unless $event_fee->nextbill;
+    }
+    
+    # don't create an invoice with no line items, or where the only line 
+    # items are fees that are supposed to be held until the next invoice
+    next if !$generate_bill;
 
+    # calculate fees...
     my @fee_items;
     foreach my $event_fee (@pending_event_fees) {
       my $object = $event_fee->cust_event->cust_X;
+      my $part_fee = $event_fee->part_fee;
       my $cust_bill;
       if ( $object->isa('FS::cust_main') ) {
         # Not the real cust_bill object that will be inserted--in particular
@@ -569,7 +580,6 @@ sub bill {
         # etc.)
         $cust_bill = $object;
       }
-      my $part_fee = $event_fee->part_fee;
       # if the fee def belongs to a different agent, don't charge the fee.
       # event conditions should prevent this, but just in case they don't,
       # skip the fee.
@@ -585,7 +595,10 @@ sub bill {
       # link this so that we can clear the marker on inserting the line item
       $fee_item->set('cust_event_fee', $event_fee);
       push @fee_items, $fee_item;
+
     }
+    
+    # add fees to the invoice
     foreach my $fee_item (@fee_items) {
 
       push @cust_bill_pkg, $fee_item;
index 8eb86fa..a49782d 100644 (file)
@@ -2,6 +2,7 @@ package FS::part_event::Action::Mixin::fee;
 
 use strict;
 use base qw( FS::part_event::Action );
+use FS::Record qw( qsearch );
 
 sub event_stage { 'pre-bill'; }
 
@@ -15,16 +16,34 @@ sub option_fields {
                     value_col => 'feepart',
                     disable_empty => 1,
                   },
-  );
+  ),
+
 }
 
 sub default_weight { 10; }
 
+sub hold_until_bill { 1 }
+
 sub do_action {
   my( $self, $cust_object, $cust_event ) = @_;
 
-  die "no fee definition selected for event '".$self->event."'\n"
-    unless $self->option('feepart');
+  my $feepart = $self->option('feepart')
+    or die "no fee definition selected for event '".$self->event."'\n";
+  my $tablenum = $cust_object->get($cust_object->primary_key);
+
+  # see if there's already a pending fee for this customer/invoice
+  my @existing = qsearch({
+      table     => 'cust_event_fee',
+      addl_from => 'JOIN cust_event USING (eventnum)',
+      hashref   => { feepart    => $feepart,
+                     billpkgnum => '' },
+      extra_sql => " AND tablenum = $tablenum",
+  });
+  if (scalar @existing > 0) {
+    warn $self->event." event, object $tablenum: already scheduled\n"
+      if $FS::part_fee::DEBUG;
+    return;
+  }
 
   # mark the event so that the fee will be charged
   # the logic for calculating the fee amount is in FS::part_fee
@@ -32,8 +51,9 @@ sub do_action {
   # FS::cust_bill_pkg
   my $cust_event_fee = FS::cust_event_fee->new({
       'eventnum'    => $cust_event->eventnum,
-      'feepart'     => $self->option('feepart'),
+      'feepart'     => $feepart,
       'billpkgnum'  => '',
+      'nextbill'    => $self->hold_until_bill ? 'Y' : '',
   });
 
   my $error = $cust_event_fee->insert;
index fc185e4..5d962b1 100644 (file)
@@ -9,4 +9,20 @@ sub eventtable_hashref {
     { 'cust_bill' => 1 };
 }
 
+sub option_fields {
+  (
+    __PACKAGE__->SUPER::option_fields,
+    'nextbill'  => { label    => 'Hold fee until the customer\'s next bill',
+                     type     => 'checkbox',
+                     value    => 'Y'
+                   },
+  )
+}
+
+# it makes sense for this to be optional for previous-invoice fees
+sub hold_until_bill {
+  my $self = shift;
+  $self->option('nextbill');
+}
+
 1;
index a6f1078..9373091 100644 (file)
@@ -9,6 +9,8 @@ sub eventtable_hashref {
     { 'cust_main' => 1 };
 }
 
+sub hold_until_bill { 1 }
+
 # Otherwise identical to cust_bill_fee.  We only have a separate event 
 # because it behaves differently as an invoice event than as a customer
 # event, and needs a different description.
index 9605d61..b0e5473 100644 (file)
@@ -5,7 +5,7 @@ use base qw( FS::o2m_Common FS::Record );
 use vars qw( $DEBUG );
 use FS::Record qw( qsearch qsearchs );
 
-$DEBUG = 1;
+$DEBUG = 0;
 
 =head1 NAME
 
@@ -127,6 +127,7 @@ sub check {
   my $self = shift;
 
   $self->set('amount', 0) unless $self->amount;
+  $self->set('percent', 0) unless $self->percent;
 
   my $error = 
     $self->ut_numbern('feepart')
@@ -140,8 +141,8 @@ sub check {
     || $self->ut_floatn('credit_weight')
     || $self->ut_agentnum_acl('agentnum',
                               [ 'Edit global package definitions' ])
-    || $self->ut_moneyn('amount')
-    || $self->ut_floatn('percent')
+    || $self->ut_money('amount')
+    || $self->ut_float('percent')
     || $self->ut_moneyn('minimum')
     || $self->ut_moneyn('maximum')
     || $self->ut_flag('limit_credit')
@@ -289,7 +290,7 @@ sub lineitem {
       $maximum = -1 * $balance;
     }
   }
-  if ( $maximum ne '' ) {
+  if ( $maximum ne '' and $amount > $maximum ) {
     warn "Applying maximum fee\n" if $DEBUG;
     $amount = $maximum;
   }
@@ -307,7 +308,7 @@ sub lineitem {
   });
 
   if ( $maximum and $self->taxable ) {
-    warn "Estimating taxes on fee.\n";
+    warn "Estimating taxes on fee.\n" if $DEBUG;
     # then we need to estimate tax to respect the maximum
     # XXX currently doesn't work with external (tax_rate) taxes
     # or batch taxes, obviously
@@ -326,6 +327,7 @@ sub lineitem {
     if ($total_rate > 0) {
       my $max_cents = $maximum * 100;
       my $charge_cents = sprintf('%0.f', $max_cents * 100/(100 + $total_rate));
+      # the actual maximum that we can charge...
       $maximum = sprintf('%.2f', $charge_cents / 100.00);
       $amount = $maximum if $amount > $maximum;
     }
index dada233..b1044c9 100644 (file)
@@ -20,6 +20,7 @@
     'minimum'       => 'Minimum fee',
     'maximum'       => 'Maximum fee',
     'limit_credit'  => 'Limit to customer credit balance',
+    'nextbill'      => 'Hold until the customer\'s next invoice',
     %locale_labels
   },
   'fields'        => \@fields,
@@ -86,6 +87,11 @@ my @fields = (
     value   => 'Y',
   },
 
+  { field   => 'nextbill',
+    type    => 'checkbox',
+    value   => 'Y',
+  },
+
   { field   => 'setuprecur',
     type    => 'select',
     options => [ 'setup', 'recur' ],