From 0d16979c948a691c95ef92f4ac7a2921a9e8c998 Mon Sep 17 00:00:00 2001 From: mark Date: Tue, 3 Jan 2012 21:13:33 +0000 Subject: detail format refactor, #15535 --- FS/FS/detail_format.pm | 256 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 FS/FS/detail_format.pm (limited to 'FS/FS/detail_format.pm') diff --git a/FS/FS/detail_format.pm b/FS/FS/detail_format.pm new file mode 100644 index 0000000..144aaa7 --- /dev/null +++ b/FS/FS/detail_format.pm @@ -0,0 +1,256 @@ +package FS::detail_format; + +use strict; +use vars qw( $DEBUG ); +use FS::Conf; +use FS::cdr; +use FS::cust_bill_pkg_detail; +use Date::Format qw(time2str); +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. + +=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 $self = { conf => FS::Conf->new, + csv => Text::CSV_XS->new, + inbound => ($opt{'inbound'} ? 1 : 0), + buffer => ($opt{'buffer'} || []), + }; + bless $self, $class; +} + +=back + +=head1 METHODS + +=item inbound VALUE + +Set/get the 'inbound' flag. + +=cut + +sub inbound { + my $self = shift; + $self->{inbound} = ($_[0] > 0) if (@_); + $self->{inbound}; +} + +=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: + +rated_price => amount +rated_classnum => classnum +rated_seconds => duration +rated_regionname => regionname +accountcode => accountcode +startdate => startdate + +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; + + FS::cust_bill_pkg_detail->new( { + 'amount' => $cdr->rated_price, + 'classnum' => $cdr->rated_classnum, + 'duration' => $cdr->rated_seconds, + 'regionname' => $cdr->rated_regionname, + 'accountcode' => $cdr->accountcode, + 'startdate' => $cdr->startdate, + 'format' => 'C', + 'detail' => $self->csv->string, + }); +} + +=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') || '$'); +} + +#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; + # XXX termination objects don't have rated_granularity so this may + # result in inbound CDRs being displayed as min/sec when they shouldn't. + # Should probably fix this. + if ( $cdr->rated_granularity eq '0' ) { + '1 call'; + } + elsif ( $cdr->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; + length($price) ? $self->money_char . $price : ''; +} + +1; -- cgit v1.1