From ea16cbe4a50fa79872a4f08225863f39b7d28c06 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 29 Nov 2014 16:36:15 -0800 Subject: [PATCH] sales commission events on invoices, #25847 --- .../Action/Mixin/credit_agent_pkg_class.pm | 15 ++-- FS/FS/part_event/Action/Mixin/credit_bill.pm | 95 ++++++++++++++++++++++ FS/FS/part_event/Action/Mixin/credit_flat.pm | 25 ++++++ FS/FS/part_event/Action/Mixin/credit_pkg.pm | 33 +++++--- .../Action/Mixin/credit_sales_pkg_class.pm | 20 +---- FS/FS/part_event/Action/bill_sales_credit.pm | 91 +++++++++++++++++++++ .../Action/bill_sales_credit_pkg_class.pm | 11 +++ FS/FS/part_event/Action/pkg_agent_credit.pm | 5 +- .../Action/pkg_agent_credit_pkg_class.pm | 3 +- FS/FS/part_event/Action/pkg_employee_credit.pm | 5 +- FS/FS/part_event/Action/pkg_referral_credit.pm | 24 +----- FS/FS/part_event/Action/pkg_sales_credit.pm | 11 ++- FS/FS/part_event/Action/pkg_sales_credit_pkg.pm | 2 + .../Action/pkg_sales_credit_pkg_class.pm | 4 +- 14 files changed, 276 insertions(+), 68 deletions(-) create mode 100644 FS/FS/part_event/Action/Mixin/credit_bill.pm create mode 100644 FS/FS/part_event/Action/Mixin/credit_flat.pm create mode 100644 FS/FS/part_event/Action/bill_sales_credit.pm create mode 100644 FS/FS/part_event/Action/bill_sales_credit_pkg_class.pm diff --git a/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm b/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm index cb61f1b77..488132a36 100644 --- a/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm +++ b/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm @@ -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 index 000000000..4930e35fb --- /dev/null +++ b/FS/FS/part_event/Action/Mixin/credit_bill.pm @@ -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 index 000000000..374cf5d6b --- /dev/null +++ b/FS/FS/part_event/Action/Mixin/credit_flat.pm @@ -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; diff --git a/FS/FS/part_event/Action/Mixin/credit_pkg.pm b/FS/FS/part_event/Action/Mixin/credit_pkg.pm index e586f8532..400ece97b 100644 --- a/FS/FS/part_event/Action/Mixin/credit_pkg.pm +++ b/FS/FS/part_event/Action/Mixin/credit_pkg.pm @@ -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; diff --git a/FS/FS/part_event/Action/Mixin/credit_sales_pkg_class.pm b/FS/FS/part_event/Action/Mixin/credit_sales_pkg_class.pm index 5c090ef54..61302aa27 100644 --- a/FS/FS/part_event/Action/Mixin/credit_sales_pkg_class.pm +++ b/FS/FS/part_event/Action/Mixin/credit_sales_pkg_class.pm @@ -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 index 000000000..3193a81ef --- /dev/null +++ b/FS/FS/part_event/Action/bill_sales_credit.pm @@ -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 index 000000000..91442b9e4 --- /dev/null +++ b/FS/FS/part_event/Action/bill_sales_credit_pkg_class.pm @@ -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; diff --git a/FS/FS/part_event/Action/pkg_agent_credit.pm b/FS/FS/part_event/Action/pkg_agent_credit.pm index 494c40e3f..65f8c27d6 100644 --- a/FS/FS/part_event/Action/pkg_agent_credit.pm +++ b/FS/FS/part_event/Action/pkg_agent_credit.pm @@ -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'); diff --git a/FS/FS/part_event/Action/pkg_agent_credit_pkg_class.pm b/FS/FS/part_event/Action/pkg_agent_credit_pkg_class.pm index 3dcf668f9..92c155627 100644 --- a/FS/FS/part_event/Action/pkg_agent_credit_pkg_class.pm +++ b/FS/FS/part_event/Action/pkg_agent_credit_pkg_class.pm @@ -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'; } diff --git a/FS/FS/part_event/Action/pkg_employee_credit.pm b/FS/FS/part_event/Action/pkg_employee_credit.pm index 64dd8b2c5..6cbe9bc4e 100644 --- a/FS/FS/part_event/Action/pkg_employee_credit.pm +++ b/FS/FS/part_event/Action/pkg_employee_credit.pm @@ -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'); diff --git a/FS/FS/part_event/Action/pkg_referral_credit.pm b/FS/FS/part_event/Action/pkg_referral_credit.pm index e7c92d650..9d7bbf8b3 100644 --- a/FS/FS/part_event/Action/pkg_referral_credit.pm +++ b/FS/FS/part_event/Action/pkg_referral_credit.pm @@ -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; diff --git a/FS/FS/part_event/Action/pkg_sales_credit.pm b/FS/FS/part_event/Action/pkg_sales_credit.pm index e7551cda9..3c569cada 100644 --- a/FS/FS/part_event/Action/pkg_sales_credit.pm +++ b/FS/FS/part_event/Action/pkg_sales_credit.pm @@ -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'); diff --git a/FS/FS/part_event/Action/pkg_sales_credit_pkg.pm b/FS/FS/part_event/Action/pkg_sales_credit_pkg.pm index 9b13cd872..bd165f1c8 100644 --- a/FS/FS/part_event/Action/pkg_sales_credit_pkg.pm +++ b/FS/FS/part_event/Action/pkg_sales_credit_pkg.pm @@ -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 ); diff --git a/FS/FS/part_event/Action/pkg_sales_credit_pkg_class.pm b/FS/FS/part_event/Action/pkg_sales_credit_pkg_class.pm index c69c004ba..53ffc6cff 100644 --- a/FS/FS/part_event/Action/pkg_sales_credit_pkg_class.pm +++ b/FS/FS/part_event/Action/pkg_sales_credit_pkg_class.pm @@ -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"; } -- 2.11.0