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