Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorIvan Kohler <ivan@freeside.biz>
Mon, 2 Feb 2015 09:34:59 +0000 (01:34 -0800)
committerIvan Kohler <ivan@freeside.biz>
Mon, 2 Feb 2015 09:34:59 +0000 (01:34 -0800)
17 files changed:
FS/FS/Schema.pm
FS/FS/TemplateItem_Mixin.pm
FS/FS/Template_Mixin.pm
FS/FS/Upgrade.pm
FS/FS/cust_bill.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_bill_pkg_discount.pm
FS/FS/cust_pkg.pm
FS/FS/quotation.pm
FS/FS/quotation_pkg.pm
FS/FS/quotation_pkg_discount.pm
FS/FS/svc_hardware.pm
httemplate/edit/process/quick-charge.cgi
httemplate/edit/process/quick-cust_pkg.cgi
httemplate/edit/quick-charge.html
httemplate/view/cust_main/payment_history/voided_invoice.html
httemplate/view/cust_main/payment_history/voided_payment.html

index b7611c1..d5ed1b7 100644 (file)
@@ -1895,6 +1895,8 @@ sub tables_hashref {
         'contract_end',    @date_type,             '', '',
         'quantity',             'int', 'NULL', '', '', '',
         'waive_setup',         'char', 'NULL',  1, '', '', 
+        'unitsetup',     @money_typen,             '', '',
+        'unitrecur',     @money_typen,             '', '',
       ],
       'primary_key'  => 'quotationpkgnum',
       'unique'       => [],
@@ -1917,6 +1919,8 @@ sub tables_hashref {
         'quotationpkgdiscountnum', 'serial', '', '', '', '',
         'quotationpkgnum',            'int', '', '', '', '', 
         'discountnum',                'int', '', '', '', '',
+        'setup_amount',        @money_typen,         '', '',
+        'recur_amount',        @money_typen,         '', '',
         #'end_date',              @date_type,         '', '',
       ],
       'primary_key'  => 'quotationpkgdiscountnum',
index 6ae3364..27b8f1b 100644 (file)
@@ -367,15 +367,17 @@ sub cust_bill_pkg_detail {
 
 }
 
-=item cust_bill_pkg_discount 
+=item pkg_discount 
 
-Returns the list of associated cust_bill_pkg_discount objects.
+Returns the list of associated cust_bill_pkg_discount or 
+quotation_pkg_discount objects.
 
 =cut
 
-sub cust_bill_pkg_discount {
+sub pkg_discount {
   my $self = shift;
-  qsearch( $self->discount_table, { 'billpkgnum' => $self->billpkgnum } );
+  my $pkey = $self->primary_key;
+  qsearch( $self->discount_table, { $pkey => $self->get($pkey) } );
 }
 
 1;
index 9669ac2..e26592c 100644 (file)
@@ -691,11 +691,12 @@ sub print_generic {
   # (this is used in the summary & on the payment coupon)
   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
 
-  # info from customer's last invoice before this one, for some 
-  # summary formats
-  $invoice_data{'last_bill'} = {};
+  # flag telling this invoice to have a first-page summary
+  my $summarypage = '';
 
   if ( $self->custnum && $self->invnum ) {
+    # XXX should be an FS::cust_bill method to set the defaults, instead
+    # of checking the type here
 
     my $last_bill = $self->previous_bill;
     if ( $last_bill ) {
@@ -801,13 +802,16 @@ sub print_generic {
       $invoice_data{'previous_payments'} = [];
       $invoice_data{'previous_credits'} = [];
     }
-  } # if this is an invoice
 
-  my $summarypage = '';
-  if ( $conf->exists('invoice_usesummary', $agentnum) ) {
-    $summarypage = 1;
-  }
-  $invoice_data{'summarypage'} = $summarypage;
+    # info from customer's last invoice before this one, for some 
+    # summary formats
+    $invoice_data{'last_bill'} = {};
+  
+    if ( $conf->exists('invoice_usesummary', $agentnum) ) {
+      $invoice_data{'summarypage'} = $summarypage = 1;
+    }
+
+  } # if this is an invoice
 
   warn "$me substituting variables in notes, footer, smallfooter\n"
     if $DEBUG > 1;
@@ -3093,7 +3097,9 @@ sub _items_cust_bill_pkg {
             if $cust_bill_pkg->recur != 0
             || $discount_show_always
             || $cust_bill_pkg->recur_show_zero;
-          push @b, {
+          #push @b, {
+          # keep it consistent, please
+          $s = {
             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
             'description' => $description,
             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
@@ -3106,7 +3112,8 @@ sub _items_cust_bill_pkg {
           };
         }
         if ( $cust_bill_pkg->recur != 0 ) {
-          push @b, {
+          #push @b, {
+          $r = {
             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
             'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
@@ -3399,89 +3406,6 @@ sub _items_cust_bill_pkg {
 
         } # recurring or usage with recurring charge
 
-        # decide whether to show active discounts here
-        if (
-            # case 1: we are showing a single line for the package
-            ( !$type )
-            # case 2: we are showing a setup line for a package that has
-            # no base recurring fee
-            or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
-            # case 3: we are showing a recur line for a package that has 
-            # a base recurring fee
-            or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
-        ) {
-
-          # the line item hashref for the line that will show the original
-          # price
-          # (use the recur or single line for the package, unless we're 
-          # showing a setup line for a package with no recurring fee)
-          my $active_line = $r;
-          if ( $type eq 'S' ) {
-            $active_line = $s;
-          }
-
-          my @discounts = $cust_bill_pkg->cust_bill_pkg_discount;
-          # special case: if there are old "discount details" on this line 
-          # item, don't show discount line items
-          if ( FS::cust_bill_pkg_detail->count(
-              "detail LIKE 'Includes discount%' AND billpkgnum = " .
-              $cust_bill_pkg->billpkgnum
-             ) > 0 ) {
-             @discounts = ();
-          }
-          if ( @discounts ) {
-            warn "$me _items_cust_bill_pkg including discounts for ".
-              $cust_bill_pkg->billpkgnum."\n"
-              if $DEBUG;
-            my $discount_amount = sum( map {$_->amount} @discounts );
-            # if multiple discounts apply to the same package, how to display
-            # them? ext_description lines, apparently
-            #
-            # # discount amounts are negative
-            if ( $d and $cust_bill_pkg->hidden ) {
-              $d->{amount}      -= $discount_amount;
-            } else {
-              my @ext;
-              $d = {
-                _is_discount    => 1,
-                description     => $self->mt('Discount'),
-                amount          => -1 * $discount_amount,
-                ext_description => \@ext,
-              };
-              foreach my $cust_bill_pkg_discount (@discounts) {
-                my $discount = $cust_bill_pkg_discount->cust_pkg_discount->discount;
-                my $discount_desc = $discount->description_short;
-
-                if ($discount->months) {
-
-                  # calculate months remaining after this invoice
-                  my $used = FS::Record->scalar_sql(
-                    'SELECT SUM(months) FROM cust_bill_pkg_discount
-                      JOIN cust_bill_pkg USING (billpkgnum)
-                      JOIN cust_bill USING (invnum)
-                      WHERE pkgdiscountnum = ? AND _date <= ?',
-                    $cust_bill_pkg_discount->pkgdiscountnum,
-                    $self->_date
-                  );
-                  $used ||= 0;
-                  my $remaining = sprintf('%.2f', $discount->months - $used);
-                  # append "for X months (Y months remaining)"
-                  $discount_desc .= $self->mt(' for [quant,_1,month] ([quant,_2,month] remaining)',
-                    $cust_bill_pkg_discount->months,
-                    $remaining
-                  );
-                } # else it's not time-limited
-                push @ext, &{$escape_function}($discount_desc);
-              }
-            }
-
-            # update the active line (before the discount) to show the 
-            # original price (whether this is a hidden line or not)
-            $active_line->{amount} += $discount_amount;
-            
-          } # if there are any discounts
-        } # if this is an appropriate place to show discounts
-
       } else { # taxes and fees
 
         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
@@ -3496,6 +3420,56 @@ sub _items_cust_bill_pkg {
 
       } # if quotation / package line item / other line item
 
+      # decide whether to show active discounts here
+      if (
+          # case 1: we are showing a single line for the package
+          ( !$type )
+          # case 2: we are showing a setup line for a package that has
+          # no base recurring fee
+          or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
+          # case 3: we are showing a recur line for a package that has 
+          # a base recurring fee
+          or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
+      ) {
+
+        my $item_discount = $cust_bill_pkg->_item_discount;
+        if ( $item_discount ) {
+          # $item_discount->{amount} is negative
+
+          if ( $d and $cust_bill_pkg->hidden ) {
+            $d->{amount}      += $item_discount->{amount};
+          } else {
+            $d = $item_discount;
+            $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
+          }
+
+          # update the active line (before the discount) to show the 
+          # original price (whether this is a hidden line or not)
+          #
+          # quotation discounts keep track of setup and recur; invoice 
+          # discounts currently don't
+          if ( exists $item_discount->{setup_amount} ) {
+
+            $s->{amount} -= $item_discount->{setup_amount} if $s;
+            $r->{amount} -= $item_discount->{recur_amount} if $r;
+
+          } else {
+
+            # $active_line is the line item hashref for the line that will
+            # show the original price
+            # (use the recur or single line for the package, unless we're 
+            # showing a setup line for a package with no recurring fee)
+            my $active_line = $r;
+            if ( $type eq 'S' ) {
+              $active_line = $s;
+            }
+            $active_line->{amount} -= $item_discount->{amount};
+
+          }
+
+        } # if there are any discounts
+      } # if this is an appropriate place to show discounts
+
     } # foreach $display
 
     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
index f84e4e5..4719caa 100644 (file)
@@ -70,7 +70,7 @@ sub upgrade_config {
 
   upgrade_invoice_from($conf);
   foreach my $agent (@agents) {
-    upgrade_invoice_from($conf,$agent->agentnum);
+    upgrade_invoice_from($conf,$agent->agentnum,1);
   }
 
   my $DIST_CONF = '/usr/local/etc/freeside/default_conf/';#DIST_CONF in Makefile
@@ -175,10 +175,10 @@ sub upgrade_overlimit_groups {
 }
 
 sub upgrade_invoice_from {
-  my ($conf, $agentnum) = @_;
+  my ($conf, $agentnum, $agentonly) = @_;
   if (
-      (!$conf->config('invoice_from_name',$agentnum)) && 
-      ($conf->config('invoice_from',$agentnum) =~ /\<(.*)\>/)
+      (!$conf->exists('invoice_from_name',$agentnum,$agentonly)) && 
+      ($conf->config('invoice_from',$agentnum,$agentonly) =~ /\<(.*)\>/)
   ) {
     my $realemail = $1;
     $realemail =~ s/^\s*//; # remove leading spaces
index d2a6ded..888e88b 100644 (file)
@@ -1902,13 +1902,18 @@ sub print_csv {
     my $lineseq = 0;
     foreach my $item ( $self->_items_pkg ) {
 
+      my $description = $item->{'description'};
+      if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
+        $description .= ': ' . $item->{ext_description}[0];
+      }
+
       $csv->combine(
         '',                     #  1 | N/A-Leave Empty            CHAR   2
         '',                     #  2 | N/A-Leave Empty            CHAR  15
         $tracctnum,             #  3 | Account Number             CHAR  15
         $self->invnum,          #  4 | Invoice Number             CHAR  15
         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
-        $item->{'description'}, #  6 | Transaction Detail         CHAR 100
+        $description,           #  6 | Transaction Detail         CHAR 100
         $item->{'amount'},      #  7 | Amount                     NUM*   9
         '',                     #  8 | Line Format Control**      CHAR   2
         '',                     #  9 | Grouping Code              CHAR   2
index 56a666e..352ed6a 100644 (file)
@@ -668,6 +668,45 @@ sub units {
   $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
 }
 
+=item _item_discount
+
+If this item has any discounts, returns a hashref in the format used
+by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
+on an invoice. This will contain the keys 'description', 'amount', 
+'ext_description' (an arrayref of text lines describing the discounts),
+and '_is_discount' (a flag).
+
+The value for 'amount' will be negative, and will be scaled for the package
+quantity.
+
+=cut
+
+sub _item_discount {
+  my $self = shift;
+  my @pkg_discounts = $self->pkg_discount;
+  return if @pkg_discounts == 0;
+  # special case: if there are old "discount details" on this line item, don't
+  # show discount line items
+  if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
+    return;
+  } 
+  
+  my @ext;
+  my $d = {
+    _is_discount    => 1,
+    description     => $self->mt('Discount'),
+    amount          => 0,
+    ext_description => \@ext,
+    # maybe should show quantity/unit discount?
+  };
+  foreach my $pkg_discount (@pkg_discounts) {
+    push @ext, $pkg_discount->description;
+    $d->{amount} -= $pkg_discount->amount;
+  } 
+  $d->{amount} *= $self->quantity || 1;
+  
+  return $d;
+}
 
 =item set_display OPTION => VALUE ...
 
index 534a067..9e64d20 100644 (file)
@@ -126,6 +126,39 @@ Returns the associated line item (see L<FS::cust_bill_pkg>).
 
 Returns the associated customer discount (see L<FS::cust_pkg_discount>).
 
+=item description
+
+Returns a string describing the discount (for use on an invoice).
+
+=cut
+
+sub description {
+  my $self = shift;
+  my $discount = $self->cust_pkg_discount->discount;
+  my $desc = $discount->description_short;
+  $desc .= $self->mt(' each') if $self->cust_bill_pkg->quantity > 1;
+
+  if ($discount->months) {
+    # calculate months remaining on this cust_pkg_discount after this invoice
+    my $date = $self->cust_bill_pkg->cust_bill->_date;
+    my $used = FS::Record->scalar_sql(
+      'SELECT SUM(months) FROM cust_bill_pkg_discount
+      JOIN cust_bill_pkg USING (billpkgnum)
+      JOIN cust_bill USING (invnum)
+      WHERE pkgdiscountnum = ? AND _date <= ?',
+      $self->pkgdiscountnum,
+      $date
+    );
+    $used ||= 0;
+    my $remaining = sprintf('%.2f', $discount->months - $used);
+    $desc .= $self->mt(' for [quant,_1,month] ([quant,_2,month] remaining)',
+              $self->months,
+              $remaining
+             );
+  }
+  return $desc;
+}
+
 =back
 
 =head1 BUGS
index 0f0983b..3bd2107 100644 (file)
@@ -2456,6 +2456,13 @@ sub modify_charge {
 
   } # else simply ignore them; the UI shouldn't allow editing the fields
 
+  
+  if ( exists($opt{'taxclass'}) 
+          and $part_pkg->taxclass ne $opt{'taxclass'}) {
+    
+      $part_pkg->set('taxclass', $opt{'taxclass'});
+  }
+
   my $error;
   if ( $part_pkg->modified or $pkg_opt_modified ) {
     # can we safely modify the package def?
index 5c94150..38e7318 100644 (file)
@@ -631,6 +631,27 @@ sub search_sql_where {
 
 }
 
+=item _items_pkg
+
+Return line item hashes for each package on this quotation. Differs from the
+base L<FS::Template_Mixin> version in that it recalculates each quoted package
+first, and doesn't implement the "condensed" option.
+
+=cut
+
+sub _items_pkg {
+  my ($self, %options) = @_;
+  my @quotation_pkg = $self->quotation_pkg;
+  foreach (@quotation_pkg) {
+    my $error = $_->estimate;
+    die "error calculating estimate for pkgpart " . $_->pkgpart.": $error\n"
+      if $error;
+  }
+
+  # run it through the Template_Mixin engine
+  return $self->_items_cust_bill_pkg(\@quotation_pkg, %options);
+}
+
 =back
 
 =head1 BUGS
index 33c761e..3813fb2 100644 (file)
@@ -2,9 +2,10 @@ package FS::quotation_pkg;
 use base qw( FS::TemplateItem_Mixin FS::Record );
 
 use strict;
-use FS::Record qw( qsearchs ); #qsearch
+use FS::Record qw( qsearchs dbh ); #qsearch
 use FS::part_pkg;
 use FS::quotation_pkg_discount; #so its loaded when TemplateItem_Mixin needs it
+use List::Util qw(sum);
 
 =head1 NAME
 
@@ -39,19 +40,19 @@ primary key
 
 =item pkgpart
 
-pkgpart
+pkgpart (L<FS::part_pkg>) of the package
 
 =item locationnum
 
-locationnum
+locationnum (L<FS::cust_location>) where the package will be in service
 
 =item start_date
 
-start_date
+expected start date for the package, as a timestamp
 
 =item contract_end
 
-contract_end
+contract end date
 
 =item quantity
 
@@ -59,8 +60,15 @@ quantity
 
 =item waive_setup
 
-waive_setup
+'Y' to waive the setup fee
 
+=item unitsetup
+
+The amount per package that will be charged in setup/one-time fees.
+
+=item unitrecur
+
+The amount per package that will be charged per billing cycle.
 
 =back
 
@@ -93,10 +101,69 @@ sub discount_table        { 'quotation_pkg_discount'; }
 Adds this record to the database.  If there is an error, returns the error,
 otherwise returns false.
 
+=cut
+
+sub insert {
+  my ($self, %options) = @_;
+
+  my $dbh = dbh;
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+
+  my $error = $self->SUPER::insert;
+
+  if ( !$error and $self->discountnum ) {
+    $error = $self->insert_discount;
+    $error .= ' (setting discount)' if $error;
+  }
+
+  # update $self and any discounts with their amounts
+  if ( !$error ) {
+    $error = $self->estimate;
+    $error .= ' (calculating charges)' if $error;
+  }
+
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  } else {
+    $dbh->commit if $oldAutoCommit;
+    return '';
+  }
+}
+
 =item delete
 
 Delete this record from the database.
 
+=cut
+
+sub delete {
+  my $self = shift;
+
+  my $dbh = dbh;
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+
+  foreach ($self->quotation_pkg_discount) {
+    my $error = $_->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error . ' (deleting discount)';
+    }
+  }
+
+  my $error = $self->SUPER::delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  } else {
+    $dbh->commit if $oldAutoCommit;
+    return '';
+  }
+  
+}
+
 =item replace OLD_RECORD
 
 Replaces the OLD_RECORD with this one in the database.  If there is an error,
@@ -121,8 +188,11 @@ sub check {
     || $self->ut_numbern('start_date')
     || $self->ut_numbern('contract_end')
     || $self->ut_numbern('quantity')
+    || $self->ut_moneyn('unitsetup')
+    || $self->ut_moneyn('unitrecur')
     || $self->ut_enum('waive_setup', [ '', 'Y'] )
   ;
+
   return $error if $error;
 
   $self->SUPER::check;
@@ -140,48 +210,159 @@ sub desc {
   $self->part_pkg->pkg;
 }
 
-sub setup {
+=item estimate
+
+Update the quotation_pkg record with the estimated setup and recurring 
+charges for the package. Returns nothing on success, or an error message
+on failure.
+
+=cut
+
+sub estimate {
   my $self = shift;
-  return '0.00' if $self->waive_setup eq 'Y' || $self->{'_NO_SETUP_KLUDGE'};
   my $part_pkg = $self->part_pkg;
-  #my $setup = $part_pkg->can('base_setup') ? $part_pkg->base_setup
-  #                                         : $part_pkg->option('setup_fee');
-  my $setup = $part_pkg->option('setup_fee');
-  #XXX discounts
-  $setup *= $self->quantity if $self->quantity;
-  sprintf('%.2f', $setup);
+  my $quantity = $self->quantity || 1;
+  my ($unitsetup, $unitrecur);
+  # calculate base fees
+  if ( $self->waive_setup eq 'Y' || $self->{'_NO_SETUP_KLUDGE'} ) {
+    $unitsetup = '0.00';
+  } else {
+    $unitsetup = $part_pkg->base_setup;
+  }
+  if ( $self->{'_NO_RECUR_KLUDGE'} ) {
+    $unitrecur = '0.00';
+  } else {
+    $unitrecur = $part_pkg->base_recur;
+  }
+
+  #XXX add-on packages
+
+  $self->set('unitsetup', $unitsetup);
+  $self->set('unitrecur', $unitrecur);
+  my $error = $self->replace;
+  return $error if $error;
+
+  # semi-duplicates calc_discount
+  my $setup_discount = 0;
+  my $recur_discount = 0;
+
+  my %setup_discounts; # quotationpkgdiscountnum => amount
+  my %recur_discounts; # quotationpkgdiscountnum => amount
+
+  # XXX the order of applying discounts is ill-defined, which matters
+  # if there are percentage and amount discounts on the same package.
+  foreach my $pkg_discount ($self->quotation_pkg_discount) {
+
+    my $discount = $pkg_discount->discount;
+    my $this_setup_discount = 0;
+    my $this_recur_discount = 0;
+
+    if ( $discount->percent > 0 ) {
+
+      if ( $discount->setup ) {
+        $this_setup_discount = ($discount->percent * $unitsetup / 100);
+      }
+      $this_recur_discount = ($discount->percent * $unitrecur / 100);
+
+    } elsif ( $discount->amount > 0 ) {
+
+      my $discount_left = $discount->amount;
+      if ( $discount->setup ) {
+        if ( $discount_left > $unitsetup - $setup_discount ) {
+          # then discount the setup to zero
+          $discount_left -= $unitsetup - $setup_discount;
+          $this_setup_discount = $unitsetup - $setup_discount;
+        } else {
+          # not enough discount to fully cover the setup
+          $this_setup_discount = $discount_left;
+          $discount_left = 0;
+        }
+      }
+      # same logic for recur
+      if ( $discount_left > $unitrecur - $recur_discount ) {
+        $this_recur_discount = $unitrecur - $recur_discount;
+      } else {
+        $this_recur_discount = $discount_left;
+      }
+
+    }
+
+    # increment the total discountage
+    $setup_discount += $this_setup_discount;
+    $recur_discount += $this_recur_discount;
+    # and update the pkg_discount object
+    $pkg_discount->set('setup_amount', sprintf('%.2f', $setup_discount));
+    $pkg_discount->set('recur_amount', sprintf('%.2f', $recur_discount));
+    my $error = $pkg_discount->replace;
+    return $error if $error;
+  }
 
+  '';
 }
 
-sub recur {
+=item insert_discount
+
+Associates this package with a discount (see L<FS::cust_pkg_discount>,
+possibly inserting a new discount on the fly (see L<FS::discount>). Properties
+of the discount will be taken from this object.
+
+=cut
+
+sub insert_discount {
+  #my ($self, %options) = @_;
   my $self = shift;
-  return '0.00' if $self->{'_NO_RECUR_KLUDGE'};
-  my $part_pkg = $self->part_pkg;
-  my $recur = $part_pkg->can('base_recur') ? $part_pkg->base_recur($self)
-                                           : $part_pkg->option('recur_fee');
-  #XXX discounts
-  $recur *= $self->quantity if $self->quantity;
-  sprintf('%.2f', $recur);
+
+  my $cust_pkg_discount = FS::quotation_pkg_discount->new( {
+    'quotationpkgnum' => $self->quotationpkgnum,
+    'discountnum'     => $self->discountnum,
+    #for the create a new discount case
+    '_type'           => $self->discountnum__type,
+    'amount'      => $self->discountnum_amount,
+    'percent'     => $self->discountnum_percent,
+    'months'      => $self->discountnum_months,
+    'setup'       => $self->discountnum_setup,
+  } );
+
+  $cust_pkg_discount->insert;
 }
 
-sub unitsetup {
+sub _item_discount {
   my $self = shift;
-  return '0.00' if $self->waive_setup eq 'Y' || $self->{'_NO_SETUP_KLUDGE'};
-  my $part_pkg = $self->part_pkg;
-  my $setup = $part_pkg->option('setup_fee');
+  my @pkg_discounts = $self->pkg_discount;
+  return if @pkg_discounts == 0;
+  
+  my @ext;
+  my $d = {
+    _is_discount    => 1,
+    description     => $self->mt('Discount'),
+    setup_amount    => 0,
+    recur_amount    => 0,
+    amount          => 0,
+    ext_description => \@ext,
+    # maybe should show quantity/unit discount?
+  };
+  foreach my $pkg_discount (@pkg_discounts) {
+    push @ext, $pkg_discount->description;
+    $d->{setup_amount} -= $pkg_discount->setup_amount;
+    $d->{recur_amount} -= $pkg_discount->recur_amount;
+  } 
+  $d->{setup_amount} *= $self->quantity || 1;
+  $d->{recur_amount} *= $self->quantity || 1;
+  $d->{amount} = $d->{setup_amount} + $d->{recur_amount};
+  
+  return $d;
+}
 
-  #XXX discounts
-  sprintf('%.2f', $setup);
+sub setup {
+  my $self = shift;
+  ($self->unitsetup - sum(map { $_->setup_amount } $self->pkg_discount))
+    * ($self->quantity || 1);
 }
 
-sub unitrecur {
+sub recur {
   my $self = shift;
-  return '0.00' if $self->{'_NO_RECUR_KLUDGE'};
-  my $part_pkg = $self->part_pkg;
-  my $recur = $part_pkg->can('base_recur') ? $part_pkg->base_recur
-                                           : $part_pkg->option('recur_fee');
-  #XXX discounts
-  sprintf('%.2f', $recur);
+  ($self->unitrecur - sum(map { $_->recur_amount } $self->pkg_discount))
+    * ($self->quantity || 1);
 }
 
 =item part_pkg_currency_option OPTIONNAME
@@ -273,6 +454,8 @@ sub prospect_main {
 
 =head1 BUGS
 
+Doesn't support taxes, fees, or add-on packages.
+
 =head1 SEE ALSO
 
 L<FS::Record>, schema.html from the base documentation.
index 19930ac..633308c 100644 (file)
@@ -1,5 +1,6 @@
 package FS::quotation_pkg_discount;
 use base qw( FS::Record );
+use FS::Maketext 'mt'; # XXX not really correct
 
 use strict;
 
@@ -36,12 +37,21 @@ primary key
 
 =item quotationpkgnum
 
-quotationpkgnum
+quotationpkgnum of the L<FS::quotation_pkg> record that this discount is
+for.
 
 =item discountnum
 
-discountnum
+discountnum (L<FS::discount>)
 
+=item setup_amount
+
+Amount that will be discounted from setup fees, per package quantity.
+
+=item recur_amount
+
+Amount that will be discounted from recurring fees in the first billing
+cycle, per package quantity.
 
 =back
 
@@ -107,6 +117,8 @@ sub check {
     $self->ut_numbern('quotationpkgdiscountnum')
     || $self->ut_foreign_key('quotationpkgnum', 'quotation_pkg', 'quotationpkgnum' )
     || $self->ut_foreign_key('discountnum', 'discount', 'discountnum' )
+    || $self->ut_moneyn('setup_amount')
+    || $self->ut_moneyn('recur_amount')
   ;
   return $error if $error;
 
@@ -115,6 +127,39 @@ sub check {
 
 =back
 
+=item amount
+
+Returns the total amount of this discount (setup + recur), for compatibility
+with L<FS::cust_bill_pkg_discount>.
+
+=cut
+
+sub amount {
+  my $self = shift;
+  return $self->get('setup_amount') + $self->get('recur_amount');
+}
+
+=item description
+
+Returns a string describing the discount (for use on the quotation).
+
+=cut
+
+sub description {
+  my $self = shift;
+  my $discount = $self->discount;
+  my $desc = $discount->description_short;
+  # XXX localize to prospect language, once prospects get languages
+  $desc .= mt(' each') if $self->quotation_pkg->quantity > 1;
+
+  if ($discount->months) {
+    # unlike cust_bill_pkg_discount, there are no "months remaining"; it 
+    # hasn't started yet.
+    $desc .= mt(' (for [quant,_1,month])', $discount->months);
+  }
+  return $desc;
+}
+
 =head1 BUGS
 
 =head1 SEE ALSO
index dc9ac59..16a5ea9 100644 (file)
@@ -132,7 +132,17 @@ sub search_sql {
 
 sub label {
   my $self = shift;
-  $self->serial || $self->display_hw_addr;
+  my @label = ();
+  if (my $type = $self->hardware_type) {
+    push @label, 'Type:' . $type->description;
+  }
+  if (my $ser = $self->serial) {
+    push @label, 'Serial#' . $ser;
+  }
+  if (my $mac = $self->display_hw_addr) {
+    push @label, 'MAC:'. $mac;
+  }
+  return join(', ', @label);
 }
 
 =item insert
index c130a55..aa6010e 100644 (file)
@@ -67,6 +67,18 @@ if ( $param->{'pkgnum'} =~ /^(\d+)$/ ) { #modifying an existing one-time charge
   my $start_date = $cgi->param('start_date')
                      ? parse_datetime($cgi->param('start_date'))
                      : time;
+   
+  $param->{'tax_override'} =~ /^\s*([,\d]*)\s*$/
+    or $error .= "Illegal tax override " . $param->{"tax_override"} . "  ";
+  my $override = $1;
+  if ( $param->{'taxclass'} eq '(select)' ) {
+    $error .= "Must select a tax class.  "
+      unless ($conf->exists('enable_taxproducts') &&
+               ( $override || $param->{taxproductnum} )
+             );
+    $cgi->param('taxclass', '');
+  }
 
   $error = $cust_pkg->modify_charge(
       'pkg'               => scalar($cgi->param('pkg')),
@@ -75,6 +87,10 @@ if ( $param->{'pkgnum'} =~ /^(\d+)$/ ) { #modifying an existing one-time charge
       'adjust_commission' => ($cgi->param('adjust_commission') ? 1 : 0),
       'amount'            => $amount,
       'setup_cost'        => $setup_cost,
+      'setuptax'          => scalar($cgi->param('setuptax')),
+      'taxclass'          => scalar($cgi->param('taxclass')),
+      'taxproductnum'     => scalar($cgi->param('taxproductnum')),
+      'tax_override'      => $override,
       'quantity'          => $quantity,
       'start_date'        => $start_date,
   );
index f1d8c26..34f5d12 100644 (file)
@@ -159,7 +159,7 @@ if ( $quotationnum ) {
   $quotation_pkg->prospectnum($prospect_main->prospectnum) if $prospect_main;
 
   #XXX handle new location
-  $error = $quotation_pkg->insert;
+  $error = $quotation_pkg->insert || $quotation_pkg->estimate;
 
 } else {
 
index 1e1232d..83620a9 100644 (file)
@@ -171,6 +171,15 @@ function bill_now_changed (what) {
       &>
 %   }
 
+<TR>
+  <TD ALIGN="right"><% mt('Tax exempt') |h %> </TD>
+  <TD><INPUT TYPE="checkbox" NAME="setuptax" VALUE="Y" <% $cgi->param('setuptax') ? 'CHECKED' : '' %>></TD>
+</TR>
+
+<& /elements/tr-select-taxclass.html, 'curr_value' => $part_pkg->get('taxclass')  &>
+
+<& /elements/tr-select-taxproduct.html, 'label' => emt('Tax product'), 'onclick' => 'parent.taxproductmagic(this);', 'curr_value' => $part_pkg->get('taxproductnum')  &>
+
 % } else { # new one-time charge
 
     <TR>
index eb00e05..3d81e66 100644 (file)
@@ -6,7 +6,7 @@
 % }
 % my $reason = $cust_bill_void->reason;
 % if ($reason) {
-     for <% $reason %>
+     (<% $reason %>)
 % }
 <% mt("on [_1]", time2str($date_format, $cust_bill_void->void_date) ) |h %> 
 </I>
index daeaa31..5c43c91 100644 (file)
@@ -6,7 +6,7 @@
 % }
 % my $reason = $cust_pay_void->reason;
 % if ($reason) {
-     for <% $reason %>
+     (<% $reason %>)
 % }
 <% mt("on [_1]", time2str($date_format, $cust_pay_void->void_date) ) |h %> 
 </I>