From c2f7d8ba623194ad1fae37b231b2e29b33d05674 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 3 Jun 2014 16:59:41 -0700 Subject: [PATCH] changes to support new invoice template features, #28080 --- FS/FS/Conf.pm | 26 ++++++++++ FS/FS/Misc.pm | 8 ++- FS/FS/Schema.pm | 2 +- FS/FS/TemplateItem_Mixin.pm | 19 +++---- FS/FS/Template_Mixin.pm | 59 ++++++++++++++++++--- FS/FS/cdr.pm | 8 ++- FS/FS/cust_bill.pm | 43 ++++++++++++++++ FS/FS/cust_bill_pkg_detail.pm | 25 ++++----- FS/FS/detail_format.pm | 36 ++++++++----- FS/FS/detail_format/sum_count_class.pm | 93 ++++++++++++++++++++++++++++++++++ FS/FS/part_pkg/voip_cdr.pm | 9 ++++ 11 files changed, 280 insertions(+), 48 deletions(-) create mode 100644 FS/FS/detail_format/sum_count_class.pm diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index b19859848..9404c0691 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -1060,6 +1060,7 @@ sub reason_type_options { '%m/%d/%Y' => 'MM/DD/YYYY', '%d/%m/%Y' => 'DD/MM/YYYY', '%Y/%m/%d' => 'YYYY/MM/DD', + '%e %b %Y' => 'DD Mon YYYY', ], 'per_locale' => 1, }, @@ -1576,6 +1577,13 @@ and customer address. Include units.', # 'per_agent' => 1, #}, + { + 'key' => 'usage_class_summary', + 'section' => 'invoicing', + 'description' => 'Summarize total usage by usage class in a separate section.', + 'type' => 'checkbox', + }, + { 'key' => 'usage_class_as_a_section', 'section' => 'invoicing', @@ -1691,6 +1699,14 @@ and customer address. Include units.', }, { + 'key' => 'papersize', + 'section' => 'billing', + 'description' => 'Invoice paper size. Default is "letter" (U.S. standard). The LaTeX template must be configured to match this size.', + 'type' => 'select', + 'select_enum' => [ qw(letter a4) ], + }, + + { 'key' => 'money_char', 'section' => '', 'description' => 'Currency symbol - defaults to `$\'', @@ -4246,6 +4262,16 @@ and customer address. Include units.', }, { + 'key' => 'previous_invoice_history', + 'section' => 'invoicing', + 'description' => 'Show a month-by-month history of the customer\'s '. + 'billing amounts. This requires template '. + 'modification and is currently not supported on the '. + 'stock template.', + 'type' => 'checkbox', + }, + + { 'key' => 'balance_due_below_line', 'section' => 'invoicing', 'description' => 'Place the balance due message below a line. Only meaningful when when invoice_sections is false.', diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm index 93445abde..380f8959d 100644 --- a/FS/FS/Misc.pm +++ b/FS/FS/Misc.pm @@ -718,7 +718,9 @@ sub generate_ps { _pslatex($file); - system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0 + my $papersize = $conf->config('papersize') || 'letter'; + + system('dvips', '-q', '-t', $papersize, "$file.dvi", '-o', "$file.ps" ) == 0 or die "dvips failed"; open(POSTSCRIPT, "<$file.ps") @@ -773,8 +775,10 @@ sub generate_pdf { my $sfile = shell_quote $file; #system('dvipdf', "$file.dvi", "$file.pdf" ); + my $papersize = $conf->config('papersize') || 'letter'; + system( - "dvips -q -t letter -f $sfile.dvi ". + "dvips -q -f $sfile.dvi -t $papersize ". "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ". " -c save pop -" ) == 0 diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 4e67cf7ad..fb02e6b79 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -1008,7 +1008,7 @@ sub tables_hashref { 'cust_bill_pkg_detail' => { 'columns' => [ - 'detailnum', 'serial', '', '', '', '', + 'detailnum', 'serial', '', '', '', '', 'billpkgnum', 'int', 'NULL', '', '', '', # should not be nullable 'pkgnum', 'int', 'NULL', '', '', '', # deprecated 'invnum', 'int', 'NULL', '', '', '', # deprecated diff --git a/FS/FS/TemplateItem_Mixin.pm b/FS/FS/TemplateItem_Mixin.pm index fa20c240f..6ae3364d1 100644 --- a/FS/FS/TemplateItem_Mixin.pm +++ b/FS/FS/TemplateItem_Mixin.pm @@ -128,20 +128,21 @@ sub time_period_pretty { Returns an array of detail information for the invoice line item. -Currently available options are: I, I and -I. +Options may include: -If I is set to html or latex then the array members are improved -for tabular appearance in those environments if possible. +I: set to 'html' or 'latex' to have the detail lines formatted for +inclusion in an HTML table (wrapped in and elements) or LaTeX table +(delimited with & and \\ operators). -If I is set then the array members are processed by this +I: if present, then the array elements are processed by this function before being returned. -I overrides the normal HTML or LaTeX function for returning -formatted CDRs. It can be set to a subroutine which returns an empty list -to skip usage detail: +I: overrides the normal HTML or LaTeX function for returning +formatted CDRs. - 'format_function' => sub { () }, +I: excludes call detail records. The method will still return +some special-case records like prorate details, and manually created package +details. =cut diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index e0ea6ab2f..bfa03bcdb 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -1051,6 +1051,7 @@ sub print_generic { $detail->{'edate'} = $line_item->{'edate'}; $detail->{'seconds'} = $line_item->{'seconds'}; $detail->{'svc_label'} = $line_item->{'svc_label'}; + $detail->{'usage_item'} = $line_item->{'usage_item'}; push @detail_items, $detail; push @buf, ( [ $detail->{'description'}, @@ -1390,6 +1391,37 @@ sub print_generic { } $invoice_data{summary_subtotals} = \@summary_subtotals; + # usage subtotals + if ( $conf->exists('usage_class_summary') + and $self->can('_items_usage_class_summary') ) { + my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function); + if ( @usage_subtotals ) { + unshift @sections, $usage_subtotals[0]->{section}; + unshift @detail_items, @usage_subtotals; + } + } + + # invoice history "section" (not really a section) + # not to be included in any subtotals, completely independent of + # everything... + if ( $conf->exists('previous_invoice_history') ) { + my %history; + my %monthorder; + foreach my $cust_bill ( $cust_main->cust_bill ) { + # XXX hardcoded format, and currently only 'charged'; add other fields + # if they become necessary + my $date = $self->time2str_local('%b %Y', $cust_bill->_date); + $history{$date} ||= 0; + $history{$date} += $cust_bill->charged; + # just so we have a numeric sort key + $monthorder{$date} ||= $cust_bill->_date; + } + my @sorted_months = sort { $monthorder{$a} <=> $monthorder{$b} } + keys %history; + my @sorted_amounts = map { sprintf('%.2f', $history{$_}) } @sorted_months; + $invoice_data{monthly_history} = [ \@sorted_months, \@sorted_amounts ]; + } + # debugging hook: call this with 'diag' => 1 to just get a hash of # the invoice variables return \%invoice_data if ( $params{'diag'} ); @@ -2494,6 +2526,9 @@ sub _items_cust_bill_pkg { @cust_bill_pkg_display = grep { !$_->summary } @cust_bill_pkg_display; } + + my $classname = ''; # package class name, will fill in later + foreach my $display (@cust_bill_pkg_display) { warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ". @@ -2554,6 +2589,9 @@ sub _items_cust_bill_pkg { %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate') unless $part_pkg->option('disable_line_item_date_ranges',1); + # not normally used, but pass this to the template anyway + $classname = $part_pkg->classname; + if ( (!$type || $type eq 'S') && ( $cust_bill_pkg->setup != 0 || $cust_bill_pkg->setup_show_zero @@ -2580,16 +2618,20 @@ sub _items_cust_bill_pkg { my @d = (); my $svc_label; + + # always pass the svc_label through to the template, even if + # not displaying it as an ext_description + my @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date, undef, 'I'); + + $svc_label = $svc_labels[0]; + unless ( $cust_pkg->part_pkg->hide_svc_detail || $cust_bill_pkg->hidden ) { - my @svc_labels = map &{$escape_function}($_), - $cust_pkg->h_labels_short($self->_date, undef, 'I'); push @d, @svc_labels unless $cust_bill_pkg->pkgpart_override; #don't redisplay services - $svc_label = $svc_labels[0]; - my $lnum = $cust_main ? $cust_main->ship_locationnum : $self->prospect_main->locationnum; # show the location label if it's not the customer's default @@ -2663,6 +2705,10 @@ sub _items_cust_bill_pkg { push @dates, $prev->sdate if $prev; push @dates, undef if !$prev; + my @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short(@dates, 'I'); + $svc_label = $svc_labels[0]; + # show service labels, unless... # the package is set not to display them unless ( $part_pkg->hide_svc_detail @@ -2682,12 +2728,8 @@ sub _items_cust_bill_pkg { warn "$me _items_cust_bill_pkg adding service details\n" if $DEBUG > 1; - my @svc_labels = map &{$escape_function}($_), - $cust_pkg->h_labels_short(@dates, 'I'); push @d, @svc_labels unless $cust_bill_pkg->pkgpart_override; #don't redisplay services - $svc_label = $svc_labels[0]; - warn "$me _items_cust_bill_pkg done adding service details\n" if $DEBUG > 1; @@ -2796,6 +2838,7 @@ sub _items_cust_bill_pkg { pkgpart => $pkgpart, pkgnum => $cust_bill_pkg->pkgnum, amount => $amount, + usage_item => 1, recur_show_zero => $cust_bill_pkg->recur_show_zero, %item_dates, ext_description => \@d, diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index c2be4f2ec..4126d5f9a 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -338,7 +338,7 @@ sub check { #check the foreign keys even? #do we want to outright *reject* the CDR? my $error = - $self->ut_numbern('acctid') + $self->ut_numbern('acctid'); #add a config option to turn these back on if someone needs 'em # @@ -350,7 +350,7 @@ sub check { # # # Telstra =1, Optus = 2, RSL COM = 3 # || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' ) - ; + return $error if $error; $self->SUPER::check; @@ -1210,6 +1210,10 @@ my %export_names = ( 'name' => 'Summary, one line per destination prefix', 'invoice_header' => 'Caller,Rate,Calls,Minutes,Price', }, + 'sum_count_class' => { + 'name' => 'Summary, one line per usage class', + 'invoice_header' => 'Caller,Class,Calls,Price', + }, ); my %export_formats = (); diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 1b765fa32..85c4bac5c 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -2919,6 +2919,49 @@ sub _items_svc_phone_sections { } +=sub _items_usage_class_summary OPTIONS + +Returns a list of detail items summarizing the usage charges on this +invoice. Each one will have 'amount', 'description' (the usage charge name), +and 'usage_classnum'. + +OPTIONS can include 'escape' (a function to escape the descriptions). + +=cut + +sub _items_usage_class_summary { + my $self = shift; + my %opt = @_; + + my $escape = $opt{escape} || sub { $_[0] }; + my $invnum = $self->invnum; + my @classes = qsearch({ + 'table' => 'usage_class', + 'select' => 'classnum, classname, SUM(amount) AS amount', + 'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' . + ' LEFT JOIN cust_bill_pkg USING (billpkgnum)', + 'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum". + ' GROUP BY classnum, classname, weight'. + ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'. + ' ORDER BY weight ASC', + }); + my @l; + my $section = { + description => &{$escape}($self->mt('Usage Summary')), + no_subtotal => 1, + usage_section => 1, + }; + foreach my $class (@classes) { + push @l, { + 'description' => &{$escape}($class->classname), + 'amount' => sprintf('%.2f', $class->amount), + 'usage_classnum' => $class->classnum, + 'section' => $section, + }; + } + return @l; +} + sub _items_previous { my $self = shift; my $conf = $self->conf; diff --git a/FS/FS/cust_bill_pkg_detail.pm b/FS/FS/cust_bill_pkg_detail.pm index 46f6e170d..d0cbdbec0 100644 --- a/FS/FS/cust_bill_pkg_detail.pm +++ b/FS/FS/cust_bill_pkg_detail.pm @@ -86,27 +86,15 @@ sub table { 'cust_bill_pkg_detail'; } Adds this record to the database. If there is an error, returns the error, otherwise returns false. -=cut - -# the insert method can be inherited from FS::Record - =item delete Delete this record from the database. -=cut - -# the delete method can be inherited from FS::Record - =item replace OLD_RECORD Replaces the OLD_RECORD with this one in the database. If there is an error, returns the error, otherwise returns false. -=cut - -# the replace method can be inherited from FS::Record - =item check Checks all fields to make sure this is a valid line item detail. If there is @@ -145,6 +133,7 @@ sub check { || $self->ut_text('detail') || $self->ut_foreign_keyn('classnum', 'usage_class', 'classnum') || $self->$phonenum_check_method('phonenum') + || $self->ut_numbern('startdate') || $self->SUPER::check ; @@ -237,6 +226,18 @@ sub formatted { ; } +=item cust_bill_pkg + +Returns the L object (the invoice line item) that +this detail belongs to. + +=cut + +sub cust_bill_pkg { + my $self = shift; + my $billpkgnum = $self->billpkgnum or return ''; + FS::cust_bill_pkg->by_key($billpkgnum); +} # Used by FS::Upgrade to migrate to a new database schema sub _upgrade_schema { # class method diff --git a/FS/FS/detail_format.pm b/FS/FS/detail_format.pm index c90d31306..b072ff58d 100644 --- a/FS/FS/detail_format.pm +++ b/FS/FS/detail_format.pm @@ -98,6 +98,19 @@ sub inbound { $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), @@ -165,21 +178,15 @@ Takes a single CDR and returns an invoice detail to describe it. By default, this maps the following fields from the CDR: -=over 4 +rated_price => amount +rated_classnum => classnum +rated_seconds => duration +rated_regionname => regionname +accountcode => accountcode +startdate => startdate -=item rated_price => amount - -=item rated_classnum => classnum - -=item rated_seconds => duration - -=item rated_regionname => regionname - -=item accountcode => accountcode - -=item startdate => startdate - -=back +'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 @@ -209,6 +216,7 @@ sub single_detail { 'startdate' => $cdr->startdate, 'format' => 'C', 'detail' => $self->csv->string, + 'phonenum' => $self->phonenum, }); } diff --git a/FS/FS/detail_format/sum_count_class.pm b/FS/FS/detail_format/sum_count_class.pm new file mode 100644 index 000000000..749d45288 --- /dev/null +++ b/FS/FS/detail_format/sum_count_class.pm @@ -0,0 +1,93 @@ +package FS::detail_format::sum_count_class; + +use strict; +use vars qw( $DEBUG ); +use base qw(FS::detail_format); +use FS::Record qw(qsearchs); +use FS::cust_svc; +use FS::svc_Common; # for label + +$DEBUG = 0; + +sub name { 'Summary, one line per service and usage class' }; + +sub header_detail { + my $self = shift; + if ( $self->{inbound} ) { + 'Destination,Charge Class,Quantity,Price' + } + else { + 'Source,Charge Class,Quantity,Price' + } +} + +sub append { + my $self = shift; + my $svcnums = ($self->{svcnums} ||= {}); + my $acctids = $self->{acctids} ||= {}; + foreach my $cdr (@_) { + my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr; + my $svcnum = $object->svcnum; # yes, $object->svcnum. + + my $subtotal = ($svcnums->{$svcnum}->{$cdr->rated_classnum} ||= + { count => 0, duration => 0, amount => 0 }); + $subtotal->{count}++; + $subtotal->{duration} += $object->rated_seconds; + $subtotal->{amount} += $object->rated_price + if $object->freesidestatus ne 'no-charge'; + + my $these_acctids = $acctids->{$cdr->rated_classnum} ||= []; + push @$these_acctids, $cdr->acctid; + } +} + +sub finish { + my $self = shift; + my $svcnums = $self->{svcnums}; + my $buffer = $self->{buffer}; + foreach my $svcnum (keys %$svcnums) { + + my $classnums = $svcnums->{$svcnum}; + + my $cust_svc = qsearchs('cust_svc', { svcnum => $svcnum }) + or die "svcnum #$svcnum not found"; + my $phonenum = $cust_svc->svc_x->label; + warn "processing $phonenum\n" if $DEBUG; + + foreach my $classnum (keys %$classnums) { + my $subtotal = $classnums->{$classnum}; + next if $subtotal->{amount} < 0.01; + my $classname = ($classnum ? + FS::usage_class->by_key($classnum)->classname : + ''); + $self->csv->combine( + $phonenum, + $classname, + $subtotal->{count}, + $self->money_char . sprintf('%.02f',$subtotal->{amount}), + ); + + warn "adding detail: ".$self->csv->string."\n" if $DEBUG; + + push @$buffer, FS::cust_bill_pkg_detail->new({ + amount => $subtotal->{amount}, + format => 'C', + classnum => $classnum, + duration => $subtotal->{duration}, + phonenum => $phonenum, + accountcode => '', #ignored in this format + startdate => '', #could use the earliest startdate in the bunch? + regionname => '', + detail => $self->csv->string, + acctid => $self->{acctids}->{$classnum}, + }); + } #foreach $classnum + } #foreach $svcnum + + # supposedly the compiler is smart enough to do this in place + @$buffer = sort { $a->{Hash}->{phonenum} cmp $b->{Hash}->{phonenum} or + $a->{Hash}->{classnum} <=> $b->{Hash}->{classnum} } + @$buffer; +} + +1; diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm index b8f1eee03..d3eff355f 100644 --- a/FS/FS/part_pkg/voip_cdr.pm +++ b/FS/FS/part_pkg/voip_cdr.pm @@ -465,6 +465,15 @@ sub calc_usage { #my @invoice_details_sort; + # for tagging invoice details + my $phonenum; + if ( $svc_table eq 'svc_phone' ) { + $phonenum = $svc_x->phonenum; + } elsif ( $svc_table eq 'svc_pbx' ) { + $phonenum = $svc_x->title; + } + $formatter->phonenum($phonenum); + #first rate any outstanding CDRs not yet rated # XXX eventually use an FS::Cursor for this my $cdr_search = $svc_x->psearch_cdrs(%options); -- 2.11.0