fix upgrade error, RT#77099, RT#76171
[freeside.git] / FS / FS / commission_schedule.pm
1 package FS::commission_schedule;
2 use base qw( FS::o2m_Common FS::Record );
3
4 use strict;
5 use FS::Record qw( qsearch qsearchs );
6 use FS::commission_rate;
7 use Tie::IxHash;
8
9 tie our %basis_options, 'Tie::IxHash', (
10   setuprecur    => 'Total sales',
11   setup         => 'One-time and setup charges',
12   recur         => 'Recurring charges',
13   setup_cost    => 'Setup costs',
14   recur_cost    => 'Recurring costs',
15   setup_margin  => 'Setup charges minus costs',
16   recur_margin_permonth => 'Monthly recurring charges minus costs',
17 );
18
19 =head1 NAME
20
21 FS::commission_schedule - Object methods for commission_schedule records
22
23 =head1 SYNOPSIS
24
25   use FS::commission_schedule;
26
27   $record = new FS::commission_schedule \%hash;
28   $record = new FS::commission_schedule { 'column' => 'value' };
29
30   $error = $record->insert;
31
32   $error = $new_record->replace($old_record);
33
34   $error = $record->delete;
35
36   $error = $record->check;
37
38 =head1 DESCRIPTION
39
40 An FS::commission_schedule object represents a bundle of one or more
41 commission rates for invoices. FS::commission_schedule inherits from
42 FS::Record.  The following fields are currently supported:
43
44 =over 4
45
46 =item schedulenum - primary key
47
48 =item schedulename - descriptive name
49
50 =item reasonnum - the credit reason (L<FS::reason>) that will be assigned
51 to these commission credits
52
53 =item basis - for percentage credits, which component of the invoice charges
54 the percentage will be calculated on:
55 - setuprecur (total charges)
56 - setup
57 - recur
58 - setup_cost
59 - recur_cost
60 - setup_margin (setup - setup_cost)
61 - recur_margin_permonth ((recur - recur_cost) / freq)
62
63 =back
64
65 =head1 METHODS
66
67 =over 4
68
69 =item new HASHREF
70
71 Creates a new commission schedule.  To add the object to the database, see
72 L<"insert">.
73
74 =cut
75
76 sub table { 'commission_schedule'; }
77
78 =item insert
79
80 Adds this record to the database.  If there is an error, returns the error,
81 otherwise returns false.
82
83 =item delete
84
85 Delete this record from the database.
86
87 =cut
88
89 sub delete {
90   my $self = shift;
91   # don't allow the schedule to be removed if it's still linked to events
92   if ($self->part_event) {
93     return 'This schedule is still in use.'; # UI should be smarter
94   }
95   $self->process_o2m(
96     'table'   => 'commission_rate',
97     'params'  => [],
98   ) || $self->delete;
99 }
100
101 =item replace OLD_RECORD
102
103 Replaces the OLD_RECORD with this one in the database.  If there is an error,
104 returns the error, otherwise returns false.
105
106 =item check
107
108 Checks all fields to make sure this is a valid record.  If there is
109 an error, returns the error, otherwise returns false.  Called by the insert
110 and replace methods.
111
112 =cut
113
114 sub check {
115   my $self = shift;
116
117   my $error = 
118     $self->ut_numbern('schedulenum')
119     || $self->ut_text('schedulename')
120     || $self->ut_number('reasonnum')
121     || $self->ut_enum('basis', [ keys %basis_options ])
122   ;
123   return $error if $error;
124
125   $self->SUPER::check;
126 }
127
128 =item part_event
129
130 Returns a list of billing events (L<FS::part_event> objects) that pay
131 commission on this schedule.
132
133 =cut
134
135 sub part_event {
136   my $self = shift;
137   map { $_->part_event }
138     qsearch('part_event_option', {
139       optionname  => 'schedulenum',
140       optionvalue => $self->schedulenum,
141     }
142   );
143 }
144
145 =item calc_credit INVOICE
146
147 Takes an L<FS::cust_bill> object and calculates credit on this schedule.
148 Returns the amount to credit. If there's no rate defined for this invoice,
149 returns nothing.
150
151 =cut
152
153 # Some false laziness w/ FS::part_event::Action::Mixin::credit_bill.
154 # this is a little different in that we calculate the credit on the whole
155 # invoice.
156
157 sub calc_credit {
158   my $self = shift;
159   my $cust_bill = shift;
160   die "cust_bill record required" if !$cust_bill or !$cust_bill->custnum;
161   # count invoices before or including this one
162   my $cycle = FS::cust_bill->count('custnum = ? AND _date <= ?',
163     $cust_bill->custnum,
164     $cust_bill->_date
165   );
166   my $rate = qsearchs('commission_rate', {
167     schedulenum => $self->schedulenum,
168     cycle       => $cycle,
169   });
170   # we might do something with a rate that applies "after the end of the
171   # schedule" (cycle = 0 or something) so that this can do commissions with
172   # no end date. add that here if there's a need.
173   return unless $rate;
174
175   my $amount;
176   if ( $rate->percent ) {
177     my $what = $self->basis;
178     my $cost = ($what =~ /_cost/ ? 1 : 0);
179     my $margin = ($what =~ /_margin/ ? 1 : 0);
180     my %part_pkg_cache;
181     foreach my $cust_bill_pkg ( $cust_bill->cust_bill_pkg ) {
182
183       my $charge = 0;
184       next if !$cust_bill_pkg->pkgnum; # exclude taxes and fees
185
186       my $cust_pkg = $cust_bill_pkg->cust_pkg;
187       if ( $margin or $cost ) {
188         # look up package costs only if we need them
189         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
190         my $part_pkg   = $part_pkg_cache{$pkgpart}
191                      ||= FS::part_pkg->by_key($pkgpart);
192
193         if ( $cost ) {
194           $charge = $part_pkg->get($what);
195         } else { # $margin
196           $charge = $part_pkg->$what($cust_pkg);
197         }
198
199         $charge = ($charge || 0) * ($cust_pkg->quantity || 1);
200
201       } else {
202
203         if ( $what eq 'setup' ) {
204           $charge = $cust_bill_pkg->get('setup');
205         } elsif ( $what eq 'recur' ) {
206           $charge = $cust_bill_pkg->get('recur');
207         } elsif ( $what eq 'setuprecur' ) {
208           $charge = $cust_bill_pkg->get('setup') +
209                     $cust_bill_pkg->get('recur');
210         }
211       }
212
213       $amount += ($charge * $rate->percent / 100);
214
215     }
216   } # if $rate->percent
217
218   if ( $rate->amount ) {
219     $amount += $rate->amount;
220   }
221
222   $amount = sprintf('%.2f', $amount + 0.005);
223   return $amount;
224 }
225
226 =back
227
228 =head1 SEE ALSO
229
230 L<FS::Record>, L<FS::part_event>, L<FS::commission_rate>
231
232 =cut
233
234 1;
235