don't generate invoice details for CDRs that were ignored by package options, #16596
[freeside.git] / FS / FS / detail_format.pm
1 package FS::detail_format;
2
3 use strict;
4 use vars qw( $DEBUG );
5 use FS::Conf;
6 use FS::cdr;
7 use FS::cust_bill_pkg_detail;
8 use Date::Format qw(time2str);
9 use Text::CSV_XS;
10
11 my $me = '[FS::detail_format]';
12
13 =head1 NAME
14
15 FS::detail_format - invoice detail formatter
16
17 =head1 DESCRIPTION
18
19 An FS::detail_format object is a converter to create invoice details 
20 (L<FS::cust_bill_pkg_detail>) from call detail records (L<FS::cdr>)
21 or other usage information.  FS::detail_format inherits from nothing.
22
23 Subclasses of FS::detail_format represent specific detail formats.
24
25 =head1 CLASS METHODS
26
27 =over 4
28
29 =item new FORMAT, OPTIONS
30
31 Returns a new detail formatter.  The FORMAT argument is the name of 
32 a subclass.
33
34 OPTIONS may contain:
35
36 - buffer: an arrayref to store details into.  This may avoid the need for 
37   a large copy operation at the end of processing.  However, since 
38   summary formats will produce nothing until the end of processing, 
39   C<finish> must be called after all CDRs have been appended.
40
41 - inbound: a flag telling the formatter to format CDRs for display to 
42   the receiving party, rather than the originator.  In this case, the 
43   L<FS::cdr_termination> object will be fetched and its values used for
44   rated_price, rated_seconds, rated_minutes, and svcnum.  This can be 
45   changed with the C<inbound> method.
46
47 =cut
48
49 sub new {
50   my $class = shift;
51   if ( $class eq 'FS::detail_format' ) {
52     my $format = shift
53       or die "$me format name required";
54     $class = "FS::detail_format::$format"
55       unless $format =~ /^FS::detail_format::/;
56   }
57   eval "use $class";
58   die "$me error loading $class: $@" if $@;
59   my %opt = @_;
60
61   my $self = { conf => FS::Conf->new,
62                csv  => Text::CSV_XS->new,
63                inbound  => ($opt{'inbound'} ? 1 : 0),
64                buffer   => ($opt{'buffer'} || []),
65              }; 
66   bless $self, $class;
67 }
68
69 =back
70
71 =head1 METHODS
72
73 =item inbound VALUE
74
75 Set/get the 'inbound' flag.
76
77 =cut
78
79 sub inbound {
80   my $self = shift;
81   $self->{inbound} = ($_[0] > 0) if (@_);
82   $self->{inbound};
83 }
84
85 =item append CDRS
86
87 Takes any number of call detail records (as L<FS::cdr> objects),
88 formats them, and appends them to the internal buffer.
89
90 By default, this simply calls C<single_detail> on each CDR in the 
91 set.  Subclasses should override C<append> and maybe C<finish> if 
92 they do not produce detail lines from CDRs in a 1:1 fashion.
93
94 The 'billpkgnum', 'invnum', 'pkgnum', and 'phonenum' fields will 
95 be set later.
96
97 =cut
98
99 sub append {
100   my $self = shift;
101   foreach (@_) {
102     push @{ $self->{buffer} }, $self->single_detail($_);
103   }
104 }
105
106 =item details
107
108 Returns all invoice detail records in the buffer.  This will perform 
109 a C<finish> first.  Subclasses generally shouldn't override this.
110
111 =cut
112
113 sub details {
114   my $self = shift;
115   $self->finish;
116   @{ $self->{buffer} }
117 }
118
119 =item finish
120
121 Ensures that all invoice details are generated given the CDRs that 
122 have been appended.  By default, this does nothing.
123
124 =cut
125
126 sub finish {}
127
128 =item header
129
130 Returns a header row for the format, as an L<FS::cust_bill_pkg_detail>
131 object.  By default this has 'format' = 'C', 'detail' = the value 
132 returned by C<header_detail>, and all other fields empty.
133
134 This is called after C<finish>, so it can use information from the CDRs.
135
136 =cut
137
138 sub header {
139   my $self = shift;
140
141   FS::cust_bill_pkg_detail->new(
142     { 'format' => 'C', 'detail' => $self->header_detail }
143   )
144 }
145
146 =item single_detail CDR
147
148 Takes a single CDR and returns an invoice detail to describe it.
149
150 By default, this maps the following fields from the CDR:
151
152 rated_price       => amount
153 rated_classnum    => classnum
154 rated_seconds     => duration
155 rated_regionname  => regionname
156 accountcode       => accountcode
157 startdate         => startdate
158
159 It then calls C<columns> on the CDR to obtain a list of detail
160 columns, formats them as a CSV string, and stores that in the 
161 'detail' field.
162
163 =cut
164
165 sub single_detail {
166   my $self = shift;
167   my $cdr = shift;
168
169   my @columns = $self->columns($cdr);
170   my $status = $self->csv->combine(@columns);
171   die "$me error combining ".$self->csv->error_input."\n"
172     if !$status;
173
174   my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
175   my $price = $object->rated_price if $object;
176   $price = 0 if $cdr->freesidestatus eq 'no-charge';
177
178   FS::cust_bill_pkg_detail->new( {
179       'amount'      => $price,
180       'classnum'    => $cdr->rated_classnum,
181       'duration'    => $cdr->rated_seconds,
182       'regionname'  => $cdr->rated_regionname,
183       'accountcode' => $cdr->accountcode,
184       'startdate'   => $cdr->startdate,
185       'format'      => 'C',
186       'detail'      => $self->csv->string,
187   });
188 }
189
190 =item columns CDR
191
192 Returns a list of CSV columns (to be shown on the invoice) for
193 the CDR.  This is the method most subclasses should override.
194
195 =cut
196
197 sub columns {
198   my $self = shift;
199   die "$me no columns method in ".ref($self);
200 }
201
202 =item header_detail
203
204 Returns the 'detail' field for the header row.  This should 
205 probably be a CSV string of column headers for the values returned
206 by C<columns>.
207
208 =cut
209
210 sub header_detail {
211   my $self = shift;
212   die "$me no header_detail method in ".ref($self);
213 }
214
215 # convenience methods for subclasses
216
217 sub conf { $_[0]->{conf} }
218
219 sub csv { $_[0]->{csv} }
220
221 sub date_format {
222   my $self = shift;
223   $self->{date_format} ||= ($self->conf->config('date_format') || '%m/%d/%Y');
224 }
225
226 sub money_char {
227   my $self = shift;
228   $self->{money_char} ||= ($self->conf->config('money_char') || '$');
229 }
230
231 #imitate previous behavior for now
232
233 sub duration {
234   my $self = shift;
235   my $cdr = shift;
236   my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
237   my $sec = $object->rated_seconds if $object;
238   $sec ||= 0;
239   # XXX termination objects don't have rated_granularity so this may 
240   # result in inbound CDRs being displayed as min/sec when they shouldn't.
241   # Should probably fix this.
242   if ( $cdr->rated_granularity eq '0' ) {
243     '1 call';
244   }
245   elsif ( $cdr->rated_granularity eq '60' ) {
246     sprintf('%dm', ($sec + 59)/60);
247   }
248   else {
249     sprintf('%dm %ds', $sec / 60, $sec % 60);
250   }
251 }
252
253 sub price {
254   my $self = shift;
255   my $cdr = shift;
256   my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
257   my $price = $object->rated_price if $object;
258   $price = '0.00' if $object->freesidestatus eq 'no-charge';
259   length($price) ? $self->money_char . $price : '';
260 }
261
262 1;