1 package FS::detail_format;
7 use FS::cust_bill_pkg_detail;
12 my $me = '[FS::detail_format]';
16 FS::detail_format - invoice detail formatter
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.
24 Subclasses of FS::detail_format represent specific detail formats.
30 =item new FORMAT, OPTIONS
32 Returns a new detail formatter. The FORMAT argument is the name of
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.
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.
48 - locale: a locale string to use for static text and date formats. This is
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).
58 if ( $class eq 'FS::detail_format' ) {
60 or die "$me format name required";
61 $class = "FS::detail_format::$format"
62 unless $format =~ /^FS::detail_format::/;
65 die "$me error loading $class: $@" if $@;
68 my $locale = $opt{'locale'} || '';
69 my $conf = FS::Conf->new({ locale => $locale });
70 $locale ||= $conf->config('locale') || 'en_US';
72 my %locale_info = FS::Locales->locale_info($locale);
73 my $language_name = $locale_info{'name'};
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) } ||
95 Set/get the 'inbound' flag.
101 $self->{inbound} = ($_[0] > 0) if (@_);
107 Set/get the locally meaningful phone number. This is used to tag call details
108 for presentation on certain kinds of invoices.
114 $self->{phonenum} = shift if @_;
120 Takes any number of call detail records (as L<FS::cdr> objects),
121 formats them, and appends them to the internal buffer.
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.
127 The 'billpkgnum', 'invnum', 'pkgnum', and 'phonenum' fields will
135 push @{ $self->{buffer} }, $self->single_detail($_);
141 Returns all invoice detail records in the buffer. This will perform
142 a C<finish> first. Subclasses generally shouldn't override this.
154 Ensures that all invoice details are generated given the CDRs that
155 have been appended. By default, this does nothing.
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.
167 This is called after C<finish>, so it can use information from the CDRs.
174 FS::cust_bill_pkg_detail->new(
175 { 'format' => 'C', 'detail' => $self->header_detail }
179 =item single_detail CDR
181 Takes a single CDR and returns an invoice detail to describe it.
183 By default, this maps the following fields from the CDR:
186 rated_price => amount
187 rated_classnum => classnum
188 rated_seconds => duration
189 rated_regionname => regionname
190 accountcode => accountcode
191 startdate => startdate
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
198 'phonenum' is set to the internal C<phonenum> value set on the formatter
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
211 my @columns = $self->columns($cdr);
212 my $status = $self->csv->combine(@columns);
213 die "$me error combining ".$self->csv->error_input."\n"
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);
222 FS::cust_bill_pkg_detail->new( {
223 'acctid' => ($self->{inbound} ? '' : $cdr->acctid),
225 'classnum' => $cdr->rated_classnum,
226 'duration' => $object->rated_seconds,
227 'regionname' => $cdr->rated_regionname,
228 'accountcode' => $cdr->accountcode,
229 'startdate' => $cdr->startdate,
231 'detail' => $self->csv->string,
232 'phonenum' => $self->phonenum,
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.
245 die "$me no columns method in ".ref($self);
250 Returns the 'detail' field for the header row. This should
251 probably be a CSV string of column headers for the values returned
258 die "$me no header_detail method in ".ref($self);
261 # convenience methods for subclasses
263 sub conf { $_[0]->{conf} }
265 sub csv { $_[0]->{csv} }
269 $self->{date_format} ||= ($self->conf->config('date_format') || '%m/%d/%Y');
274 $self->{money_char} ||= ($self->conf->config('money_char') || '$');
277 # localization methods
281 $self->{_dh}->time2str(@_);
284 # header strings are now localized in FS::TemplateItem_Mixin::detail
286 #imitate previous behavior for now
291 my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
292 my $sec = $object->rated_seconds if $object;
294 # termination objects now have rated_granularity.
295 if ( $object->rated_granularity eq '0' ) {
298 elsif ( $object->rated_granularity eq '60' ) {
299 sprintf('%dm', ($sec + 59)/60);
302 sprintf('%dm %ds', $sec / 60, $sec % 60);
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;