1 package FS::part_pkg::agent_invoice;
2 use base qw(FS::part_pkg::recur_Common);
5 use FS::Record qw( qsearch );
8 use FS::cust_bill_pkg_detail;
9 use Date::Format 'time2str';
14 our $me = '[FS::part_pkg::agent_invoice]';
16 tie my %itemize_options, 'Tie::IxHash',
17 'cust_bill' => 'one line per invoice',
18 'cust_main' => 'one line per customer',
19 'agent' => 'one line per agent',
22 # use detail_formats for this?
23 my %itemize_header = (
24 'cust_bill' => '"Inv #","Customer","Date","Charge"',
25 'cust_main' => '"Cust #","Customer","Charge"',
30 'name' => 'Wholesale bulk billing based on actual invoice amounts, for master customers of an agent.',
31 'shortname' => 'Wholesale billing for agent (invoice amounts)',
32 'inherit_fields' => [qw( prorate_Mixin global_Mixin) ],
34 'cutoff_day' => { 'name' => 'Billing Day (1 - 28) for prorating or '.
39 'recur_method' => { 'name' => 'Recurring fee method',
41 #'options' => \%recur_method,
43 'select_options' => \%FS::part_pkg::recur_Common::recur_method,
45 'multiplier' => { 'name' => 'Percentage of billed amount to charge' },
46 'itemize' => { 'name' => 'Display on the wholesale invoice',
48 'select_options' => \%itemize_options,
51 'fieldorder' => [ qw( recur_method cutoff_day multiplier itemize ),
52 FS::part_pkg::prorate_Mixin::fieldorder,
59 #some false laziness-ish w/ the other agent plan
62 my($cust_pkg, $sdate, $details, $param ) = @_;
64 my $csv = Text::CSV->new({ binary => 1 });
66 my $itemize = $self->option('itemize') || 'cust_bill';
67 my $last_bill = $cust_pkg->last_bill;
69 my $conf = new FS::Conf;
70 # my $money_char = $conf->config('money_char') || '$';
72 warn "$me billing for agent packages from ". time2str('%x', $last_bill).
73 " to ". time2str('%x', $$sdate). "\n"
76 # only invoices dated after $last_bill, but before or on $$sdate, will be
77 # included in this wholesale bundle.
78 # $last_bill is the last date the wholesale package was billed, unless
79 # it has never been billed before, in which case it's the current time.
80 # $$sdate is the date of the invoice we are now generating. It is one of:
81 # - the bill date we are now billing, if there is one.
82 # - or the wholesale package's setup date, if there is one
83 # - or the current time
84 # It will usually not be _after_ the current time. This can still happen
85 # if this package's bill date is later in the current day than right now,
86 # and next-bill-ignore-time is on.
87 my $date_range = " AND _date <= $$sdate";
89 $date_range .= " AND _date > $last_bill";
92 my $percent = $self->option('multiplier') || 100;
94 my $charged_cents = 0;
95 my $wholesale_cents = 0;
98 my @agents = qsearch('agent', { 'agent_custnum' => $cust_pkg->custnum } );
100 # The existing "agent" plan (based on package defined charges/costs) has to
101 # ensure that all the agent's customers are billed before calculating its
102 # fee, so that it can tell which packages were charged within the period.
103 # _This_ plan has to do it because it actually uses the invoices. Either way
104 # this behavior is not ideal, especially when using freeside-daily in
105 # multiprocess mode, since it can lead to billing all of the agent's
106 # customers within the master customer's billing job. If this becomes a
107 # problem, one option is to use "freeside-daily -a" to bill the agent's
108 # customers _first_, and master customers later.
109 foreach my $agent (@agents) {
111 warn "$me billing for agent ". $agent->agent. "\n"
114 # cursor this if memory usage becomes a problem
115 my @cust_main = qsearch('cust_main', { 'agentnum' => $agent->agentnum } );
117 foreach my $cust_main (@cust_main) {
119 warn "$me billing agent charges for ". $cust_main->name_short. "\n"
122 # this option at least should propagate, or we risk generating invoices
123 # in the apparent future and then leaving them out of this group
124 my $error = $cust_main->bill( 'time' => $$sdate );
126 die "Error pre-billing agent customer: $error" if $error;
128 my @cust_bill = qsearch({
129 table => 'cust_bill',
130 hashref => { 'custnum' => $cust_main->custnum },
131 extra_sql => $date_range,
132 order_by => ' ORDER BY _date ASC',
135 foreach my $cust_bill (@cust_bill) {
137 # do we want the itemize setting to be purely cosmetic, or to actually
138 # change how the calculation is done? for now let's make it purely
139 # cosmetic, and round at the level of the individual invoice. can
140 # change this if needed.
141 $charged_cents += $cust_bill->charged * 100;
142 $wholesale_cents += sprintf('%.0f', $cust_bill->charged * $percent);
144 if ( $itemize eq 'cust_bill' ) {
147 $cust_main->name_short,
148 $cust_main->time2str_local('short', $cust_bill->_date),
149 sprintf('%.2f', $wholesale_cents / 100),
151 my $detail = FS::cust_bill_pkg_detail->new({
153 startdate => $cust_bill->_date,
154 amount => sprintf('%.2f', $wholesale_cents / 100),
155 detail => $csv->string,
157 push @$details, $detail;
159 $total += $wholesale_cents;
160 $charged_cents = $wholesale_cents = 0;
165 if ( $itemize eq 'cust_main' ) {
169 $cust_main->name_short,
170 sprintf('%.2f', $wholesale_cents / 100),
172 my $detail = FS::cust_bill_pkg_detail->new({
174 amount => sprintf('%.2f', $wholesale_cents / 100),
175 detail => $csv->string,
177 push @$details, $detail;
179 $total += $wholesale_cents;
180 $charged_cents = $wholesale_cents = 0;
183 } # foreach $cust_main
185 if ( $itemize eq 'agent' ) {
187 $cust_pkg->mt('[_1] customers', $agent->agent),
188 sprintf('%.2f', $wholesale_cents / 100),
190 my $detail = FS::cust_bill_pkg_detail->new({
192 amount => sprintf('%.2f', $wholesale_cents / 100),
193 detail => $csv->string,
195 push @$details, $detail;
197 $total += $wholesale_cents;
198 $charged_cents = $wholesale_cents = 0;
203 if ( @$details and $itemize_header{$itemize} ) {
204 unshift @$details, FS::cust_bill_pkg_detail->new({
206 detail => $itemize_header{$itemize},
210 my $charges = ($total / 100) + $self->calc_recur_Common(@_);
212 sprintf('%.2f', $charges );
216 sub can_discount { 0; }
218 sub hide_svc_detail { 1; }
222 sub can_usageprice { 0; }