sales commission events on invoices, #25847
authorMark Wells <mark@freeside.biz>
Sun, 30 Nov 2014 00:36:15 +0000 (16:36 -0800)
committerMark Wells <mark@freeside.biz>
Sun, 30 Nov 2014 00:36:15 +0000 (16:36 -0800)
14 files changed:
FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm
FS/FS/part_event/Action/Mixin/credit_bill.pm [new file with mode: 0644]
FS/FS/part_event/Action/Mixin/credit_flat.pm [new file with mode: 0644]
FS/FS/part_event/Action/Mixin/credit_pkg.pm
FS/FS/part_event/Action/Mixin/credit_sales_pkg_class.pm
FS/FS/part_event/Action/bill_sales_credit.pm [new file with mode: 0644]
FS/FS/part_event/Action/bill_sales_credit_pkg_class.pm [new file with mode: 0644]
FS/FS/part_event/Action/pkg_agent_credit.pm
FS/FS/part_event/Action/pkg_agent_credit_pkg_class.pm
FS/FS/part_event/Action/pkg_employee_credit.pm
FS/FS/part_event/Action/pkg_referral_credit.pm
FS/FS/part_event/Action/pkg_sales_credit.pm
FS/FS/part_event/Action/pkg_sales_credit_pkg.pm
FS/FS/part_event/Action/pkg_sales_credit_pkg_class.pm

index cb61f1b..488132a 100644 (file)
@@ -1,21 +1,16 @@
 package FS::part_event::Action::Mixin::credit_agent_pkg_class;
-use base qw( FS::part_event::Action::Mixin::credit_pkg );
+
+# calculates a credit percentage on a specific package for use with 
+# credit_pkg or credit_bill, based on an agent's commission table
 
 use strict;
 use FS::Record qw(qsearchs);
 
-sub option_fields {
-  my $class = shift;
-  my %option_fields = $class->SUPER::option_fields;
-  delete $option_fields{'percent'};
-  %option_fields;
-}
-
 sub _calc_credit_percent {
-  my( $self, $cust_pkg ) = @_;
+  my( $self, $cust_pkg, $agent ) = @_;
 
   my $agent_pkg_class = qsearchs( 'agent_pkg_class', {
-    'agentnum' => $self->cust_main($cust_pkg)->agentnum,
+    'agentnum' => $agent->agentnum,
     'classnum' => $cust_pkg->part_pkg->classnum,
   });
 
diff --git a/FS/FS/part_event/Action/Mixin/credit_bill.pm b/FS/FS/part_event/Action/Mixin/credit_bill.pm
new file mode 100644 (file)
index 0000000..4930e35
--- /dev/null
@@ -0,0 +1,95 @@
+package FS::part_event::Action::Mixin::credit_bill;
+
+use strict;
+
+# credit_bill: calculates a credit amount that is some percentage of each 
+# line item of an invoice
+
+sub eventtable_hashref {
+  { 'cust_bill' => 1 };
+}
+
+sub option_fields {
+  my $class = shift;
+  my @fields = (
+    'reasonnum' => { 'label'        => 'Credit reason',
+                     'type'         => 'select-reason',
+                     'reason_class' => 'R',
+                   },
+    'percent'   => { 'label'   => 'Percent',
+                     'type'    => 'input-percentage',
+                     'default' => '100',
+                   },
+    'what' => {
+      'label'   => 'Of',
+      'type'    => 'select',
+      #add additional ways to specify in the package def
+      'options' => [qw( setuprecur setup recur setuprecur_margin setup_margin recur_margin )],
+      'labels'  => {
+        'setuprecur'        => 'Amount charged',
+        'setup'             => 'Setup fee',
+        'recur'             => 'Recurring fee',
+        'setuprecur_margin' => 'Amount charged minus total cost',
+        'setup_margin'      => 'Setup fee minus setup cost',
+        'recur_margin'      => 'Recurring fee minus recurring cost',
+      },
+    },
+  );
+  if ($class->can('_calc_credit_percent')) {
+    splice @fields, 2, 2; #remove the percentage option
+  }
+  @fields;
+    
+}
+
+our %part_pkg_cache;
+
+# arguments:
+# 1. the line item
+# 2. the recipient of the commission; may be FS::sales, FS::agent, 
+# FS::access_user, etc. Here we don't use it, but it will be passed through
+# to _calc_credit_percent.
+
+sub _calc_credit {
+  my $self = shift;
+  my $cust_bill_pkg = shift;
+
+  my $what = $self->option('what');
+  my $margin = 1 if $what =~ s/_margin$//;
+
+  my $pkgnum = $cust_bill_pkg->pkgnum;
+  my $cust_pkg = $cust_bill_pkg->cust_pkg;
+
+  my $percent;
+  if ( $self->can('_calc_credit_percent') ) {
+    $percent = $self->_calc_credit_percent($cust_pkg, @_);
+  } else {
+    $percent = $self->option('percent') || 0;
+  }
+
+  my $charge = 0;
+  if ( $what eq 'setup' ) {
+    $charge = $cust_bill_pkg->get('setup');
+  } elsif ( $what eq 'recur' ) {
+    $charge = $cust_bill_pkg->get('recur');
+  } elsif ( $what eq 'setuprecur' ) {
+    $charge = $cust_bill_pkg->get('setup') + $cust_bill_pkg->get('recur');
+  }
+  if ( $margin ) {
+    my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
+    my $part_pkg   = $part_pkg_cache{$pkgpart}
+                 ||= FS::part_pkg->by_key($pkgpart);
+    if ( $what eq 'setup' ) {
+      $charge -= $part_pkg->get('setup_cost');
+    } elsif ( $what eq 'recur' ) {
+      $charge -= $part_pkg->get('recur_cost');
+    } elsif ( $what eq 'setuprecur' ) {
+      $charge -= $part_pkg->get('setup_cost') + $part_pkg->get('recur_cost');
+    }
+  }
+
+  $charge = 0 if $charge < 0; # e.g. prorate
+  return ($percent * $charge / 100);
+}
+
+1;
diff --git a/FS/FS/part_event/Action/Mixin/credit_flat.pm b/FS/FS/part_event/Action/Mixin/credit_flat.pm
new file mode 100644 (file)
index 0000000..374cf5d
--- /dev/null
@@ -0,0 +1,25 @@
+package FS::part_event::Action::Mixin::credit_flat;
+
+# credit_flat: return a fixed amount for _calc_credit, specified in the 
+# options
+
+use strict;
+
+sub option_fields {
+  (
+    'reasonnum' => { 'label'        => 'Credit reason',
+                     'type'         => 'select-reason',
+                     'reason_class' => 'R',
+                   },
+    'amount'    => { 'label'        => 'Credit amount',
+                     'type'         => 'money',
+                   },
+  );
+}
+
+sub _calc_credit {
+  my $self = shift;
+  $self->option('amount');
+}
+
+1;
index e586f85..400ece9 100644 (file)
@@ -2,12 +2,19 @@ package FS::part_event::Action::Mixin::credit_pkg;
 
 use strict;
 
+# credit_pkg: calculates a credit amount that is some percentage of the 
+# package charge / cost / margin / some other amount of a package
+#
+# also provides an option field for the percentage, unless the action knows
+# how to calculate its own percentage somehow (has a _calc_credit_percent)
+
 sub eventtable_hashref {
   { 'cust_pkg' => 1 };
 }
 
 sub option_fields {
-  ( 
+  my $class = shift;
+  my @fields = (
     'reasonnum' => { 'label'        => 'Credit reason',
                      'type'         => 'select-reason',
                      'reason_class' => 'R',
@@ -36,12 +43,19 @@ sub option_fields {
       },
     },
   );
+  if ($class->can('_calc_credit_percent')) {
+    splice @fields, 2, 2; #remove the percentage option
+  }
+  @fields;
 }
 
-#my %no_cust_pkg = ( 'setup_cost' => 1 );
+# arguments:
+# 1. cust_pkg
+# 2. recipient of the credit (passed through to _calc_credit_percent)
 
 sub _calc_credit {
-  my( $self, $cust_pkg ) = @_;
+  my $self = shift;
+  my $cust_pkg = shift;
 
   my $cust_main = $self->cust_main($cust_pkg);
 
@@ -59,18 +73,17 @@ sub _calc_credit {
     }
   }
 
-  my $percent = $self->_calc_credit_percent($cust_pkg);
+  my $percent;
+  if ( $self->can('_calc_credit_percent') ) {
+    $percent = $self->_calc_credit_percent($cust_pkg, @_);
+  } else {
+    $percent = $self->option('percent') || 0;
+  }
 
-  #my @arg = $no_cust_pkg{$what} ? () : ($cust_pkg);
   my @arg = ($what eq 'setup_cost') ? () : ($cust_pkg);
 
   sprintf('%.2f', $part_pkg->$what(@arg) * $percent / 100 );
 
 }
 
-sub _calc_credit_percent {
-  my( $self, $cust_pkg ) = @_;
-  $self->option('percent');
-}
-
 1;
index 5c090ef..61302aa 100644 (file)
@@ -1,30 +1,16 @@
 package FS::part_event::Action::Mixin::credit_sales_pkg_class;
-use base qw( FS::part_event::Action::Mixin::credit_pkg );
 
 use strict;
 use FS::Record qw(qsearchs);
 use FS::sales_pkg_class;
 
-sub option_fields {
-  my $class = shift;
-  my %option_fields = $class->SUPER::option_fields;
-
-  delete $option_fields{'percent'};
-
-  %option_fields;
-}
-
 sub _calc_credit_percent {
-  my( $self, $cust_pkg ) = @_;
-
-  my $salesnum = $cust_pkg->salesnum;
-  $salesnum ||= $self->cust_main($cust_pkg)->salesnum
-    if $self->option('cust_main_sales');
+  my( $self, $cust_pkg, $sales ) = @_;
 
-  return 0 unless $salesnum;
+  die "sales record required" unless $sales;
 
   my $sales_pkg_class = qsearchs( 'sales_pkg_class', {
-    'salesnum' => $salesnum,
+    'salesnum' => $sales->salesnum,
     'classnum' => $cust_pkg->part_pkg->classnum,
   });
 
diff --git a/FS/FS/part_event/Action/bill_sales_credit.pm b/FS/FS/part_event/Action/bill_sales_credit.pm
new file mode 100644 (file)
index 0000000..3193a81
--- /dev/null
@@ -0,0 +1,91 @@
+package FS::part_event::Action::bill_sales_credit;
+
+# in this order:
+# - pkg_sales_credit invokes NEXT, then appends the 'cust_main_sales' param
+# - credit_bill contains the core _calc_credit logic, and also defines other
+# params
+
+use base qw( FS::part_event::Action::Mixin::pkg_sales_credit
+             FS::part_event::Action::Mixin::credit_bill
+             FS::part_event::Action );
+use FS::Record qw(qsearch qsearchs);
+use FS::Conf;
+use Date::Format qw(time2str);
+
+use strict;
+
+sub description { 'Credit the sales person based on the billed amount'; }
+
+sub eventtable_hashref {
+  { 'cust_bill' => 1 };
+}
+
+our $date_format;
+
+sub do_action {
+  my( $self, $cust_bill, $cust_event ) = @_;
+
+  $date_format ||= FS::Conf->new->config('date_format') || '%x';
+
+  my $cust_main = $self->cust_main($cust_bill);
+
+  my %salesnum_sales; # salesnum => FS::sales object
+  my %salesnum_amount; # salesnum => credit amount
+  my %pkgnum_pkg; # pkgnum => FS::cust_pkg
+  my %salesnum_pkgnums; # salesnum => [ pkgnum, ... ]
+
+  my @items = qsearch('cust_bill_pkg', { invnum => $cust_bill->invnum,
+                                         pkgnum => { op => '>', value => '0' }
+                                       });
+
+  foreach my $cust_bill_pkg (@items) {
+    my $pkgnum = $cust_bill_pkg->pkgnum;
+    my $cust_pkg = $pkgnum_pkg{$pkgnum} ||= $cust_bill_pkg->cust_pkg;
+
+    my $salesnum = $cust_pkg->salesnum;
+    $salesnum ||= $cust_main->salesnum
+      if $self->option('cust_main_sales');
+    my $sales = $salesnum_sales{$salesnum}
+            ||= FS::sales->by_key($salesnum);
+
+    next if !$sales; #no sales person, no credit
+
+    my $amount = $self->_calc_credit($cust_bill_pkg, $sales);
+
+    if ($amount > 0) {
+      $salesnum_amount{$salesnum} ||= 0;
+      $salesnum_amount{$salesnum} += $amount;
+      push @{ $salesnum_pkgnums{$salesnum} ||= [] }, $pkgnum;
+    }
+  }
+
+  foreach my $salesnum (keys %salesnum_amount) {
+    my $amount = sprintf('%.2f', $salesnum_amount{$salesnum});
+    next if $amount < 0.005;
+
+    my $sales = $salesnum_sales{$salesnum};
+
+    my $sales_cust_main = $sales->sales_cust_main;
+    die "No customer record for sales person ". $sales->salesperson
+      unless $sales->sales_custnum;
+
+    my $reasonnum = $self->option('reasonnum');
+
+    my $desc = 'from invoice #'. $cust_bill->display_invnum .
+               ' ('. time2str($date_format, $cust_bill->_date) . ')';
+               # could also show custnum and pkgnums here?
+    my $error = $sales_cust_main->credit(
+      $amount, 
+      \$reasonnum,
+      'eventnum'            => $cust_event->eventnum,
+      'addlinfo'            => $desc,
+      'commission_salesnum' => $sales->salesnum,
+    );
+    die "Error crediting customer ". $sales_cust_main->custnum.
+        " for sales commission: $error"
+      if $error;
+  } # foreach $salesnum
+
+}
+
+1;
diff --git a/FS/FS/part_event/Action/bill_sales_credit_pkg_class.pm b/FS/FS/part_event/Action/bill_sales_credit_pkg_class.pm
new file mode 100644 (file)
index 0000000..91442b9
--- /dev/null
@@ -0,0 +1,11 @@
+package FS::part_event::Action::bill_sales_credit_pkg_class;
+
+use base qw( FS::part_event::Action::Mixin::pkg_sales_credit
+             FS::part_event::Action::Mixin::credit_bill
+             FS::part_event::Action::Mixin::credit_sales_pkg_class
+             FS::part_event::Action::bill_sales_credit
+             );
+
+sub description { "Credit the sales person based on their commission percentage for the package's class"; }
+
+1;
index 494c40e..65f8c27 100644 (file)
@@ -1,7 +1,8 @@
 package FS::part_event::Action::pkg_agent_credit;
 
 use strict;
-use base qw( FS::part_event::Action::pkg_referral_credit );
+use base qw( FS::part_event::Action::Mixin::credit_flat
+             FS::part_event::Action );
 
 sub description { 'Credit the agent a specific amount'; }
 
@@ -18,7 +19,7 @@ sub do_action {
   my $agent_cust_main = $agent->agent_cust_main;
     #? or return "No customer record for agent ". $agent->agent;
 
-  my $amount = $self->_calc_credit($cust_pkg);
+  my $amount = $self->_calc_credit($cust_pkg, $agent);
   return '' unless $amount > 0;
 
   my $reasonnum = $self->option('reasonnum');
index 3dcf668..92c1556 100644 (file)
@@ -1,7 +1,8 @@
 package FS::part_event::Action::pkg_agent_credit_pkg_class;
 
 use strict;
-use base qw( FS::part_event::Action::Mixin::credit_agent_pkg_class
+use base qw( FS::part_event::Action::Mixin::credit_pkg
+             FS::part_event::Action::Mixin::credit_agent_pkg_class
              FS::part_event::Action::pkg_agent_credit );
 
 sub description { 'Credit the agent an amount based on their commission percentage for the referred package class'; }
index 64dd8b2..6cbe9bc 100644 (file)
@@ -1,7 +1,8 @@
 package FS::part_event::Action::pkg_employee_credit;
 
 use strict;
-use base qw( FS::part_event::Action::pkg_referral_credit );
+use base qw( FS::part_event::Action::Mixin::credit_flat
+             FS::part_event::Action );
 
 sub description { 'Credit the ordering employee a specific amount'; }
 
@@ -18,7 +19,7 @@ sub do_action {
   my $employee_cust_main = $employee->user_cust_main;
     #? or return "No customer record for employee ". $employee->username;
 
-  my $amount    = $self->_calc_credit($cust_pkg);
+  my $amount    = $self->_calc_credit($cust_pkg, $employee);
   return '' unless $amount > 0;
 
   my $reasonnum = $self->option('reasonnum');
index e7c92d6..9d7bbf8 100644 (file)
@@ -1,7 +1,8 @@
 package FS::part_event::Action::pkg_referral_credit;
 
 use strict;
-use base qw( FS::part_event::Action );
+use base qw( FS::part_event::Action::Mixin::credit_flat
+             FS::part_event::Action  );
 
 sub description { 'Credit the referring customer a specific amount'; }
 
@@ -9,19 +10,6 @@ sub eventtable_hashref {
   { 'cust_pkg' => 1 };
 }
 
-sub option_fields {
-  ( 
-    'reasonnum' => { 'label'        => 'Credit reason',
-                     'type'         => 'select-reason',
-                     'reason_class' => 'R',
-                   },
-    'amount'    => { 'label'        => 'Credit amount',
-                     'type'         => 'money',
-                   },
-  );
-
-}
-
 sub do_action {
   my( $self, $cust_pkg, $cust_event ) = @_;
 
@@ -35,7 +23,7 @@ sub do_action {
   return 'Referring customer is cancelled'
     if $referring_cust_main->status eq 'cancelled';
 
-  my $amount    = $self->_calc_credit($cust_pkg);
+  my $amount    = $self->_calc_credit($cust_pkg, $referring_cust_main);
   return '' unless $amount > 0;
 
   my $reasonnum = $self->option('reasonnum');
@@ -53,10 +41,4 @@ sub do_action {
 
 }
 
-sub _calc_credit {
-  my( $self, $cust_pkg ) = @_;
-
-  $self->option('amount');
-}
-
 1;
index e7551cd..3c569ca 100644 (file)
@@ -1,12 +1,15 @@
 package FS::part_event::Action::pkg_sales_credit;
-use base qw( FS::part_event::Action::Mixin::pkg_sales_credit
-             FS::part_event::Action::pkg_referral_credit );
+use base qw( FS::part_event::Action::Mixin::credit_flat
+             FS::part_event::Action );
 
 use strict;
 
 sub description { 'Credit the sales person a specific amount'; }
 
-#a little false laziness w/pkg_referral_credit
+sub eventtable_hashref {
+  { 'cust_pkg' => 1 };
+} 
+
 sub do_action {
   my( $self, $cust_pkg, $cust_event ) = @_;
 
@@ -24,7 +27,7 @@ sub do_action {
   my $sales_cust_main = $sales->sales_cust_main;
     #? or return "No customer record for sales person ". $sales->salesperson;
 
-  my $amount = $self->_calc_credit($cust_pkg);
+  my $amount = $self->_calc_credit($cust_pkg, $sales);
   return '' unless $amount > 0;
 
   my $reasonnum = $self->option('reasonnum');
index 9b13cd8..bd165f1 100644 (file)
@@ -1,4 +1,6 @@
 package FS::part_event::Action::pkg_sales_credit_pkg;
+
+# yes, they must be in this order
 use base qw( FS::part_event::Action::Mixin::pkg_sales_credit
              FS::part_event::Action::Mixin::credit_pkg
              FS::part_event::Action::pkg_sales_credit );
index c69c004..53ffc6c 100644 (file)
@@ -1,8 +1,10 @@
 package FS::part_event::Action::pkg_sales_credit_pkg_class;
 
 use base qw( FS::part_event::Action::Mixin::pkg_sales_credit
+             FS::part_event::Action::Mixin::credit_pkg
              FS::part_event::Action::Mixin::credit_sales_pkg_class
-             FS::part_event::Action::pkg_sales_credit );
+             FS::part_event::Action::pkg_sales_credit
+             );
 
 sub description { "Credit the package sales person an amount based on their commission percentage for the package's class"; }