RT# 74666 - fixed display error on v3.
[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
39 formats will produce nothing until the end of processing, C<finish> must be
40 called 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 - rounding: the number of decimal places to show in the amount column. This
52 is optional, and defaults to whatever's in the schema (which is 4).
53
54 =cut
55
56 sub new {
57   my $class = shift;
58   if ( $class eq 'FS::detail_format' ) {
59     my $format = shift
60       or die "$me format name required";
61     $class = "FS::detail_format::$format"
62       unless $format =~ /^FS::detail_format::/;
63   }
64   eval "use $class";
65   die "$me error loading $class: $@" if $@;
66   my %opt = @_;
67
68   my $locale = $opt{'locale'} || '';
69   my $conf = FS::Conf->new({ locale => $locale });
70   $locale ||= $conf->config('locale') || 'en_US';
71
72   my %locale_info = FS::Locales->locale_info($locale);
73   my $language_name = $locale_info{'name'};
74
75   my $self = { conf => FS::Conf->new({ locale => $locale }),
76                csv  => Text::CSV_XS->new({ binary => 1 }),
77                inbound  => ($opt{'inbound'} ? 1 : 0),
78                rounding => $opt{'rounding'},
79                buffer   => ($opt{'buffer'} || []),
80                _lh      => FS::L10N->get_handle($locale),
81                _dh      => eval { Date::Language->new($language_name) } ||
82                            Date::Language->new()
83              };
84   bless $self, $class;
85 }
86
87 =back
88
89 =head1 METHODS
90
91 =over 4
92
93 =item inbound VALUE
94
95 Set/get the 'inbound' flag.
96
97 =cut
98
99 sub inbound {
100   my $self = shift;
101   $self->{inbound} = ($_[0] > 0) if (@_);
102   $self->{inbound};
103 }
104
105 =item phonenum VALUE
106
107 Set/get the locally meaningful phone number.  This is used to tag call details
108 for presentation on certain kinds of invoices.
109
110 =cut
111
112 sub phonenum {
113   my $self = shift;
114   $self->{phonenum} = shift if @_;
115   $self->{phonenum};
116 }
117
118 =item append CDRS
119
120 Takes any number of call detail records (as L<FS::cdr> objects),
121 formats them, and appends them to the internal buffer.
122
123 By default, this simply calls C<single_detail> on each CDR in the 
124 set.  Subclasses should override C<append> and maybe C<finish> if 
125 they do not produce detail lines from CDRs in a 1:1 fashion.
126
127 The 'billpkgnum', 'invnum', 'pkgnum', and 'phonenum' fields will 
128 be set later.
129
130 =cut
131
132 sub append {
133   my $self = shift;
134   foreach (@_) {
135     push @{ $self->{buffer} }, $self->single_detail($_);
136   }
137 }
138
139 =item details
140
141 Returns all invoice detail records in the buffer.  This will perform 
142 a C<finish> first.  Subclasses generally shouldn't override this.
143
144 =cut
145
146 sub details {
147   my $self = shift;
148   $self->finish;
149   @{ $self->{buffer} }
150 }
151
152 =item finish
153
154 Ensures that all invoice details are generated given the CDRs that 
155 have been appended.  By default, this does nothing.
156
157 =cut
158
159 sub finish {}
160
161 =item header
162
163 Returns a header row for the format, as an L<FS::cust_bill_pkg_detail>
164 object.  By default this has 'format' = 'C', 'detail' = the value 
165 returned by C<header_detail>, and all other fields empty.
166
167 This is called after C<finish>, so it can use information from the CDRs.
168
169 =cut
170
171 sub header {
172   my $self = shift;
173
174   FS::cust_bill_pkg_detail->new(
175     { 'format' => 'C', 'detail' => $self->header_detail }
176   )
177 }
178
179 =item single_detail CDR
180
181 Takes a single CDR and returns an invoice detail to describe it.
182
183 By default, this maps the following fields from the CDR:
184
185 acctid            => acctid
186 rated_price       => amount
187 rated_classnum    => classnum
188 rated_seconds     => duration
189 rated_regionname  => regionname
190 accountcode       => accountcode
191 startdate         => startdate
192
193 If the formatter is in inbound mode, it will look up a C<cdr_termination>
194 record and use rated_price and rated_seconds from that, and acctid will be
195 set to null to avoid linking the CDR to the detail record for the inbound
196 leg of the call.
197
198 'phonenum' is set to the internal C<phonenum> value set on the formatter
199 object.
200
201 It then calls C<columns> on the CDR to obtain a list of detail
202 columns, formats them as a CSV string, and stores that in the 
203 'detail' field.
204
205 =cut
206
207 sub single_detail {
208   my $self = shift;
209   my $cdr = shift;
210
211   my @columns = $self->columns($cdr);
212   my $status = $self->csv->combine(@columns);
213   die "$me error combining ".$self->csv->error_input."\n"
214     if !$status;
215
216   my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
217   my $price = $object->rated_price if $object;
218   $price = 0 if $cdr->freesidestatus eq 'no-charge';
219   $price = sprintf('%.*f', $self->{'rounding'}, $price)
220     if $self->{'rounding'} and length($price);
221
222   FS::cust_bill_pkg_detail->new( {
223       'acctid'      => ($self->{inbound} ? '' : $cdr->acctid),
224       'amount'      => $price,
225       'classnum'    => $cdr->rated_classnum,
226       'duration'    => $object->rated_seconds,
227       'regionname'  => $cdr->rated_regionname,
228       'accountcode' => $cdr->accountcode,
229       'startdate'   => $cdr->startdate,
230       'format'      => 'C',
231       'detail'      => $self->csv->string,
232       'phonenum'    => $self->phonenum,
233   });
234 }
235
236 =item columns CDR
237
238 Returns a list of CSV columns (to be shown on the invoice) for
239 the CDR.  This is the method most subclasses should override.
240
241 =cut
242
243 sub columns {
244   my $self = shift;
245   die "$me no columns method in ".ref($self);
246 }
247
248 =item header_detail
249
250 Returns the 'detail' field for the header row.  This should 
251 probably be a CSV string of column headers for the values returned
252 by C<columns>.
253
254 =cut
255
256 sub header_detail {
257   my $self = shift;
258   die "$me no header_detail method in ".ref($self);
259 }
260
261 # convenience methods for subclasses
262
263 sub conf { $_[0]->{conf} }
264
265 sub csv { $_[0]->{csv} }
266
267 sub date_format {
268   my $self = shift;
269   $self->{date_format} ||= ($self->conf->config('date_format') || '%m/%d/%Y');
270 }
271
272 sub money_char {
273   my $self = shift;
274   $self->{money_char} ||= ($self->conf->config('money_char') || '$');
275 }
276
277 # localization methods
278
279 sub time2str_local {
280   my $self = shift;
281   $self->{_dh}->time2str(@_);
282 }
283
284 # header strings are now localized in FS::TemplateItem_Mixin::detail
285
286 #imitate previous behavior for now
287
288 sub duration {
289   my $self = shift;
290   my $cdr = shift;
291   my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
292   my $sec = $object->rated_seconds if $object;
293   $sec ||= 0;
294   # termination objects now have rated_granularity.
295   if ( $object->rated_granularity eq '0' ) {
296     '1 call';
297   }
298   elsif ( $object->rated_granularity eq '60' ) {
299     sprintf('%dm', ($sec + 59)/60);
300   }
301   else {
302     sprintf('%dm %ds', $sec / 60, $sec % 60);
303   }
304 }
305
306 sub price {
307   my $self = shift;
308   my $cdr = shift;
309   my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
310   my $price = $object->rated_price if $object;
311   $price = '0.00' if $object->freesidestatus eq 'no-charge';
312   $price = sprintf('%.*f', $self->{'rounding'}, $price)
313     if $self->{'rounding'};
314   if (length($price)) {
315     $price = sprintf('%.*f', $self->{'rounding'}, $price)
316       if $self->{'rounding'};
317     return $self->money_char . $price;
318   } else {
319     return '';
320   }
321 }
322
323 1;