package FS::commission_schedule; use base qw( FS::o2m_Common FS::Record ); use strict; use FS::Record qw( qsearch qsearchs ); use FS::commission_rate; use Tie::IxHash; tie our %basis_options, 'Tie::IxHash', ( setuprecur => 'Total sales', setup => 'One-time and setup charges', recur => 'Recurring charges', setup_cost => 'Setup costs', recur_cost => 'Recurring costs', setup_margin => 'Setup charges minus costs', recur_margin_permonth => 'Monthly recurring charges minus costs', ); =head1 NAME FS::commission_schedule - Object methods for commission_schedule records =head1 SYNOPSIS use FS::commission_schedule; $record = new FS::commission_schedule \%hash; $record = new FS::commission_schedule { 'column' => 'value' }; $error = $record->insert; $error = $new_record->replace($old_record); $error = $record->delete; $error = $record->check; =head1 DESCRIPTION An FS::commission_schedule object represents a bundle of one or more commission rates for invoices. FS::commission_schedule inherits from FS::Record. The following fields are currently supported: =over 4 =item schedulenum - primary key =item schedulename - descriptive name =item reasonnum - the credit reason (L) that will be assigned to these commission credits =item basis - for percentage credits, which component of the invoice charges the percentage will be calculated on: - setuprecur (total charges) - setup - recur - setup_cost - recur_cost - setup_margin (setup - setup_cost) - recur_margin_permonth ((recur - recur_cost) / freq) =back =head1 METHODS =over 4 =item new HASHREF Creates a new commission schedule. To add the object to the database, see L<"insert">. =cut sub table { 'commission_schedule'; } =item insert Adds this record to the database. If there is an error, returns the error, otherwise returns false. =item delete Delete this record from the database. =cut sub delete { my $self = shift; # don't allow the schedule to be removed if it's still linked to events if ($self->part_event) { return 'This schedule is still in use.'; # UI should be smarter } $self->process_o2m( 'table' => 'commission_rate', 'params' => [], ) || $self->delete; } =item replace OLD_RECORD Replaces the OLD_RECORD with this one in the database. If there is an error, returns the error, otherwise returns false. =item check Checks all fields to make sure this is a valid record. If there is an error, returns the error, otherwise returns false. Called by the insert and replace methods. =cut sub check { my $self = shift; my $error = $self->ut_numbern('schedulenum') || $self->ut_text('schedulename') || $self->ut_number('reasonnum') || $self->ut_enum('basis', [ keys %basis_options ]) ; return $error if $error; $self->SUPER::check; } =item part_event Returns a list of billing events (L objects) that pay commission on this schedule. =cut sub part_event { my $self = shift; map { $_->part_event } qsearch('part_event_option', { optionname => 'schedulenum', optionvalue => $self->schedulenum, } ); } =item calc_credit INVOICE Takes an L object and calculates credit on this schedule. Returns the amount to credit. If there's no rate defined for this invoice, returns nothing. =cut # Some false laziness w/ FS::part_event::Action::Mixin::credit_bill. # this is a little different in that we calculate the credit on the whole # invoice. sub calc_credit { my $self = shift; my $cust_bill = shift; die "cust_bill record required" if !$cust_bill or !$cust_bill->custnum; # count invoices before or including this one my $cycle = FS::cust_bill->count('custnum = ? AND _date <= ?', $cust_bill->custnum, $cust_bill->_date ); my $rate = qsearchs('commission_rate', { schedulenum => $self->schedulenum, cycle => $cycle, }); # we might do something with a rate that applies "after the end of the # schedule" (cycle = 0 or something) so that this can do commissions with # no end date. add that here if there's a need. return unless $rate; my $amount; if ( $rate->percent ) { my $what = $self->basis; my $cost = ($what =~ /_cost/ ? 1 : 0); my $margin = ($what =~ /_margin/ ? 1 : 0); my %part_pkg_cache; foreach my $cust_bill_pkg ( $cust_bill->cust_bill_pkg ) { my $charge = 0; next if !$cust_bill_pkg->pkgnum; # exclude taxes and fees my $cust_pkg = $cust_bill_pkg->cust_pkg; if ( $margin or $cost ) { # look up package costs only if we need them my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart; my $part_pkg = $part_pkg_cache{$pkgpart} ||= FS::part_pkg->by_key($pkgpart); if ( $cost ) { $charge = $part_pkg->get($what); } else { # $margin $charge = $part_pkg->$what($cust_pkg); } $charge = ($charge || 0) * ($cust_pkg->quantity || 1); } else { 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'); } } $amount += ($charge * $rate->percent / 100); } } # if $rate->percent if ( $rate->amount ) { $amount += $rate->amount; } $amount = sprintf('%.2f', $amount + 0.005); return $amount; } =back =head1 SEE ALSO L, L, L =cut 1;