respect granularity settings for display of inbound call duration, #71715
[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 phonenum VALUE
102
103 Set/get the locally meaningful phone number.  This is used to tag call details
104 for presentation on certain kinds of invoices.
105
106 =cut
107
108 sub phonenum {
109   my $self = shift;
110   $self->{phonenum} = shift if @_;
111   $self->{phonenum};
112 }
113
114 =item append CDRS
115
116 Takes any number of call detail records (as L<FS::cdr> objects),
117 formats them, and appends them to the internal buffer.
118
119 By default, this simply calls C<single_detail> on each CDR in the 
120 set.  Subclasses should override C<append> and maybe C<finish> if 
121 they do not produce detail lines from CDRs in a 1:1 fashion.
122
123 The 'billpkgnum', 'invnum', 'pkgnum', and 'phonenum' fields will 
124 be set later.
125
126 =cut
127
128 sub append {
129   my $self = shift;
130   foreach (@_) {
131     push @{ $self->{buffer} }, $self->single_detail($_);
132   }
133 }
134
135 =item details
136
137 Returns all invoice detail records in the buffer.  This will perform 
138 a C<finish> first.  Subclasses generally shouldn't override this.
139
140 =cut
141
142 sub details {
143   my $self = shift;
144   $self->finish;
145   @{ $self->{buffer} }
146 }
147
148 =item finish
149
150 Ensures that all invoice details are generated given the CDRs that 
151 have been appended.  By default, this does nothing.
152
153 =cut
154
155 sub finish {}
156
157 =item header
158
159 Returns a header row for the format, as an L<FS::cust_bill_pkg_detail>
160 object.  By default this has 'format' = 'C', 'detail' = the value 
161 returned by C<header_detail>, and all other fields empty.
162
163 This is called after C<finish>, so it can use information from the CDRs.
164
165 =cut
166
167 sub header {
168   my $self = shift;
169
170   FS::cust_bill_pkg_detail->new(
171     { 'format' => 'C', 'detail' => $self->header_detail }
172   )
173 }
174
175 =item single_detail CDR
176
177 Takes a single CDR and returns an invoice detail to describe it.
178
179 By default, this maps the following fields from the CDR:
180
181 acctid            => acctid
182 rated_price       => amount
183 rated_classnum    => classnum
184 rated_seconds     => duration
185 rated_regionname  => regionname
186 accountcode       => accountcode
187 startdate         => startdate
188
189 If the formatter is in inbound mode, it will look up a C<cdr_termination>
190 record and use rated_price and rated_seconds from that, and acctid will be
191 set to null to avoid linking the CDR to the detail record for the inbound
192 leg of the call.
193
194 'phonenum' is set to the internal C<phonenum> value set on the formatter
195 object.
196
197 It then calls C<columns> on the CDR to obtain a list of detail
198 columns, formats them as a CSV string, and stores that in the 
199 'detail' field.
200
201 =cut
202
203 sub single_detail {
204   my $self = shift;
205   my $cdr = shift;
206
207   my @columns = $self->columns($cdr);
208   my $status = $self->csv->combine(@columns);
209   die "$me error combining ".$self->csv->error_input."\n"
210     if !$status;
211
212   my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
213   my $price = $object->rated_price if $object;
214   $price = 0 if $cdr->freesidestatus eq 'no-charge';
215
216   FS::cust_bill_pkg_detail->new( {
217       'acctid'      => ($self->{inbound} ? '' : $cdr->acctid),
218       'amount'      => $price,
219       'classnum'    => $cdr->rated_classnum,
220       'duration'    => $object->rated_seconds,
221       'regionname'  => $cdr->rated_regionname,
222       'accountcode' => $cdr->accountcode,
223       'startdate'   => $cdr->startdate,
224       'format'      => 'C',
225       'detail'      => $self->csv->string,
226       'phonenum'    => $self->phonenum,
227   });
228 }
229
230 =item columns CDR
231
232 Returns a list of CSV columns (to be shown on the invoice) for
233 the CDR.  This is the method most subclasses should override.
234
235 =cut
236
237 sub columns {
238   my $self = shift;
239   die "$me no columns method in ".ref($self);
240 }
241
242 =item header_detail
243
244 Returns the 'detail' field for the header row.  This should 
245 probably be a CSV string of column headers for the values returned
246 by C<columns>.
247
248 =cut
249
250 sub header_detail {
251   my $self = shift;
252   die "$me no header_detail method in ".ref($self);
253 }
254
255 # convenience methods for subclasses
256
257 sub conf { $_[0]->{conf} }
258
259 sub csv { $_[0]->{csv} }
260
261 sub date_format {
262   my $self = shift;
263   $self->{date_format} ||= ($self->conf->config('date_format') || '%m/%d/%Y');
264 }
265
266 sub money_char {
267   my $self = shift;
268   $self->{money_char} ||= ($self->conf->config('money_char') || '$');
269 }
270
271 # localization methods
272
273 sub time2str_local {
274   my $self = shift;
275   $self->{_dh}->time2str(@_);
276 }
277
278 # header strings are now localized in FS::TemplateItem_Mixin::detail
279
280 #imitate previous behavior for now
281
282 sub duration {
283   my $self = shift;
284   my $cdr = shift;
285   my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
286   my $sec = $object->rated_seconds if $object;
287   $sec ||= 0;
288   # termination objects now have rated_granularity.
289   if ( $object->rated_granularity eq '0' ) {
290     '1 call';
291   }
292   elsif ( $object->rated_granularity eq '60' ) {
293     sprintf('%dm', ($sec + 59)/60);
294   }
295   else {
296     sprintf('%dm %ds', $sec / 60, $sec % 60);
297   }
298 }
299
300 sub price {
301   my $self = shift;
302   my $cdr = shift;
303   my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
304   my $price = $object->rated_price if $object;
305   $price = '0.00' if $object->freesidestatus eq 'no-charge';
306   length($price) ? $self->money_char . $price : '';
307 }
308
309 1;