package FS::detail_format; use strict; use vars qw( $DEBUG ); use FS::Conf; use FS::cdr; use FS::cust_bill_pkg_detail; use FS::L10N; use Date::Language; use Text::CSV_XS; my $me = '[FS::detail_format]'; =head1 NAME FS::detail_format - invoice detail formatter =head1 DESCRIPTION An FS::detail_format object is a converter to create invoice details (L) from call detail records (L) or other usage information. FS::detail_format inherits from nothing. Subclasses of FS::detail_format represent specific detail formats. =head1 CLASS METHODS =over 4 =item new FORMAT, OPTIONS Returns a new detail formatter. The FORMAT argument is the name of a subclass. OPTIONS may contain: - buffer: an arrayref to store details into. This may avoid the need for a large copy operation at the end of processing. However, since summary formats will produce nothing until the end of processing, C must be called after all CDRs have been appended. - inbound: a flag telling the formatter to format CDRs for display to the receiving party, rather than the originator. In this case, the L object will be fetched and its values used for rated_price, rated_seconds, rated_minutes, and svcnum. This can be changed with the C method. - locale: a locale string to use for static text and date formats. This is optional. - rounding: the number of decimal places to show in the amount column. This is optional, and defaults to whatever's in the schema (which is 4). =cut sub new { my $class = shift; if ( $class eq 'FS::detail_format' ) { my $format = shift or die "$me format name required"; $class = "FS::detail_format::$format" unless $format =~ /^FS::detail_format::/; } eval "use $class"; die "$me error loading $class: $@" if $@; my %opt = @_; my $locale = $opt{'locale'} || ''; my $conf = FS::Conf->new({ locale => $locale }); $locale ||= $conf->config('locale') || 'en_US'; my %locale_info = FS::Locales->locale_info($locale); my $language_name = $locale_info{'name'}; my $self = { conf => FS::Conf->new({ locale => $locale }), csv => Text::CSV_XS->new({ binary => 1 }), inbound => ($opt{'inbound'} ? 1 : 0), rounding => $opt{'rounding'}, buffer => ($opt{'buffer'} || []), _lh => FS::L10N->get_handle($locale), _dh => eval { Date::Language->new($language_name) } || Date::Language->new() }; bless $self, $class; } =back =head1 METHODS =over 4 =item inbound VALUE Set/get the 'inbound' flag. =cut sub inbound { my $self = shift; $self->{inbound} = ($_[0] > 0) if (@_); $self->{inbound}; } =item phonenum VALUE Set/get the locally meaningful phone number. This is used to tag call details for presentation on certain kinds of invoices. =cut sub phonenum { my $self = shift; $self->{phonenum} = shift if @_; $self->{phonenum}; } =item append CDRS Takes any number of call detail records (as L objects), formats them, and appends them to the internal buffer. By default, this simply calls C on each CDR in the set. Subclasses should override C and maybe C if they do not produce detail lines from CDRs in a 1:1 fashion. The 'billpkgnum', 'invnum', 'pkgnum', and 'phonenum' fields will be set later. =cut sub append { my $self = shift; foreach (@_) { push @{ $self->{buffer} }, $self->single_detail($_); } } =item details Returns all invoice detail records in the buffer. This will perform a C first. Subclasses generally shouldn't override this. =cut sub details { my $self = shift; $self->finish; @{ $self->{buffer} } } =item finish Ensures that all invoice details are generated given the CDRs that have been appended. By default, this does nothing. =cut sub finish {} =item header Returns a header row for the format, as an L object. By default this has 'format' = 'C', 'detail' = the value returned by C, and all other fields empty. This is called after C, so it can use information from the CDRs. =cut sub header { my $self = shift; FS::cust_bill_pkg_detail->new( { 'format' => 'C', 'detail' => $self->header_detail } ) } =item single_detail CDR Takes a single CDR and returns an invoice detail to describe it. By default, this maps the following fields from the CDR: acctid => acctid rated_price => amount rated_classnum => classnum rated_seconds => duration rated_regionname => regionname accountcode => accountcode startdate => startdate If the formatter is in inbound mode, it will look up a C record and use rated_price and rated_seconds from that, and acctid will be set to null to avoid linking the CDR to the detail record for the inbound leg of the call. 'phonenum' is set to the internal C value set on the formatter object. It then calls C on the CDR to obtain a list of detail columns, formats them as a CSV string, and stores that in the 'detail' field. =cut sub single_detail { my $self = shift; my $cdr = shift; my @columns = $self->columns($cdr); my $status = $self->csv->combine(@columns); die "$me error combining ".$self->csv->error_input."\n" if !$status; my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr; my $price = $object->rated_price if $object; $price = 0 if $cdr->freesidestatus eq 'no-charge'; $price = sprintf('%.*f', $self->{'rounding'}, $price) if $self->{'rounding'} and length($price); FS::cust_bill_pkg_detail->new( { 'acctid' => ($self->{inbound} ? '' : $cdr->acctid), 'amount' => $price, 'classnum' => $cdr->rated_classnum, 'duration' => $object->rated_seconds, 'regionname' => $cdr->rated_regionname, 'accountcode' => $cdr->accountcode, 'startdate' => $cdr->startdate, 'format' => 'C', 'detail' => $self->csv->string, 'phonenum' => $self->phonenum, }); } =item columns CDR Returns a list of CSV columns (to be shown on the invoice) for the CDR. This is the method most subclasses should override. =cut sub columns { my $self = shift; die "$me no columns method in ".ref($self); } =item header_detail Returns the 'detail' field for the header row. This should probably be a CSV string of column headers for the values returned by C. =cut sub header_detail { my $self = shift; die "$me no header_detail method in ".ref($self); } # convenience methods for subclasses sub conf { $_[0]->{conf} } sub csv { $_[0]->{csv} } sub date_format { my $self = shift; $self->{date_format} ||= ($self->conf->config('date_format') || '%m/%d/%Y'); } sub money_char { my $self = shift; $self->{money_char} ||= ($self->conf->config('money_char') || '$'); } # localization methods sub time2str_local { my $self = shift; $self->{_dh}->time2str(@_); } # header strings are now localized in FS::TemplateItem_Mixin::detail #imitate previous behavior for now sub duration { my $self = shift; my $cdr = shift; my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr; my $sec = $object->rated_seconds if $object; $sec ||= 0; # termination objects now have rated_granularity. if ( $object->rated_granularity eq '0' ) { '1 call'; } elsif ( $object->rated_granularity eq '60' ) { sprintf('%dm', ($sec + 59)/60); } else { sprintf('%dm %ds', $sec / 60, $sec % 60); } } sub price { my $self = shift; my $cdr = shift; my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr; my $price = $object->rated_price if $object; $price = '0.00' if $object->freesidestatus eq 'no-charge'; $price = sprintf('%.*f', $self->{'rounding'}, $price) if $self->{'rounding'}; if (length($price)) { $price = sprintf('%.*f', $self->{'rounding'}, $price) if $self->{'rounding'}; return $self->money_char . $price; } else { return ''; } } 1;