1 package FS::Template_Mixin;
4 use vars qw( $DEBUG $me
9 use vars qw( $invoice_lines @buf ); #yuck
10 use List::Util qw(sum); #can't import first, it conflicts with cust_main.first
13 use Time::Local qw( timelocal );
14 use Text::Template 1.20;
16 use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
21 use FS::Misc qw( send_email );
22 use FS::Record qw( qsearch qsearchs dbh );
24 use FS::Misc qw( generate_ps generate_pdf );
32 $me = '[FS::Template_Mixin]';
33 FS::UID->install_callback( sub {
34 my $conf = new FS::Conf; #global
35 $money_char = $conf->config('money_char') || '$';
36 $date_format = $conf->config('date_format') || '%x'; #/YY
41 Returns a configuration handle (L<FS::Conf>) set to the customer's locale.
43 If the "mode" pseudo-field is set on the object, the configuration handle
44 will be an L<FS::invoice_conf> for that invoice mode (and the customer's
51 my $mode = $self->get('mode');
52 if ($self->{_conf} and !defined($mode)) {
53 return $self->{_conf};
56 my $cust_main = $self->cust_main;
57 my $locale = $cust_main ? $cust_main->locale : '';
60 if ( ref $mode and $mode->isa('FS::invoice_mode') ) {
61 $mode = $mode->modenum;
62 } elsif ( $mode =~ /\D/ ) {
63 die "invalid invoice mode $mode";
65 $conf = qsearchs('invoice_conf', { modenum => $mode, locale => $locale });
67 $conf = qsearchs('invoice_conf', { modenum => $mode, locale => '' });
68 # it doesn't have a locale, but system conf still might
69 $conf->set('locale' => $locale) if $conf;
72 # if $mode is unspecified, or if there is no invoice_conf matching this mode
73 # and locale, then use the system config only (but with the locale)
74 $conf ||= FS::Conf->new({ 'locale' => $locale });
76 return $self->{_conf} = $conf;
79 =item print_text OPTIONS
81 Returns an text invoice, as a list of lines.
83 Options can be passed as a hash.
85 I<time>, if specified, is used to control the printing of overdue messages. The
86 default is now. It isn't the date of the invoice; that's the `_date' field.
87 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
88 L<Time::Local> and L<Date::Parse> for conversion functions.
90 I<template>, if specified, is the name of a suffix for alternate invoices.
92 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
100 %params = %{ shift() };
105 $params{'format'} = 'template'; # for some reason
107 $self->print_generic( %params );
110 =item print_latex HASHREF
112 Internal method - returns a filename of a filled-in LaTeX template for this
113 invoice (Note: add ".tex" to get the actual filename), and a filename of
114 an associated logo (with the .eps extension included).
116 See print_ps and print_pdf for methods that return PostScript and PDF output.
118 Options can be passed as a hash.
120 I<time>, if specified, is used to control the printing of overdue messages. The
121 default is now. It isn't the date of the invoice; that's the `_date' field.
122 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
123 L<Time::Local> and L<Date::Parse> for conversion functions.
125 I<template>, if specified, is the name of a suffix for alternate invoices.
126 This is strongly deprecated; see L<FS::invoice_conf> for the right way to
127 customize invoice templates for different purposes.
129 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
138 %params = %{ shift() };
143 $params{'format'} = 'latex';
144 my $conf = $self->conf;
146 # this needs to go away
147 my $template = $params{'template'};
148 # and this especially
149 $template ||= $self->_agent_template
150 if $self->can('_agent_template');
153 $self->set('mode', $params{mode})
156 my $pkey = $self->primary_key;
157 my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
159 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
160 my $lh = new File::Temp(
161 TEMPLATE => $tmp_template,
165 ) or die "can't open temp file: $!\n";
167 my $agentnum = $self->agentnum;
169 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
170 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
171 or die "can't write temp file: $!\n";
173 print $lh $conf->config_binary('logo.eps', $agentnum)
174 or die "can't write temp file: $!\n";
177 $params{'logo_file'} = $lh->filename;
179 if( $conf->exists('invoice-barcode')
180 && $self->can('invoice_barcode')
181 && $self->invnum ) { # don't try to barcode statements
182 my $png_file = $self->invoice_barcode($dir);
183 my $eps_file = $png_file;
184 $eps_file =~ s/\.png$/.eps/g;
185 $png_file =~ /(barcode.*png)/;
187 $eps_file =~ /(barcode.*eps)/;
190 my $curr_dir = cwd();
192 # after painfuly long experimentation, it was determined that sam2p won't
193 # accept : and other chars in the path, no matter how hard I tried to
194 # escape them, hence the chdir (and chdir back, just to be safe)
195 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
196 or die "sam2p failed: $!\n";
200 $params{'barcode_file'} = $eps_file;
203 my @filled_in = $self->print_generic( %params );
205 my $fh = new File::Temp( TEMPLATE => $tmp_template,
209 ) or die "can't open temp file: $!\n";
210 binmode($fh, ':utf8'); # language support
211 print $fh join('', @filled_in );
214 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
215 return ($1, $params{'logo_file'}, $params{'barcode_file'});
221 my $cust_main = $self->cust_main;
222 $cust_main ? $cust_main->agentnum : $self->prospect_main->agentnum;
225 =item print_generic OPTION => VALUE ...
227 Internal method - returns a filled-in template for this invoice as a scalar.
229 See print_ps and print_pdf for methods that return PostScript and PDF output.
237 The B<format> option is required and should be set to html, latex (print and PDF) or template (plaintext).
247 Overrides "Invoice" as the name of the sent document.
251 Used to control the printing of overdue messages. The
252 default is now. It isn't the date of the invoice; that's the `_date' field.
253 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
254 L<Time::Local> and L<Date::Parse> for conversion functions.
258 Logo file (path to temporary EPS file on the local filesystem)
262 CID for inline (emailed) images (logo)
266 Override customer's locale
270 Overrides any per customer cdr squelching when true
274 Supress the (invoice, quotation, statement, etc.) number
282 Supress the payment coupon
286 Barcode file (path to temporary EPS file on the local filesystem)
290 Flag indicating the barcode image should be a link (normal HTML dipaly)
294 Barcode CID for inline (emailed) images
296 =item preref_callback
298 Coderef run for each line item, code should return HTML to be displayed
299 before that line item (quotations only)
303 Deprecated. Used as a suffix for a configuration template. Please
304 don't use this, it deprecated in favor of more flexible alternatives.
310 #what's with all the sprintf('%10.2f')'s in here? will it cause any
311 # (alignment in text invoice?) problems to change them all to '%.2f' ?
312 # yes: fixed width/plain text printing will be borked
314 my( $self, %params ) = @_;
315 my $conf = $self->conf;
317 my $today = $params{today} ? $params{today} : time;
318 warn "$me print_generic called on $self with suffix $params{template}\n"
321 my $format = $params{format};
322 die "Unknown format: $format"
323 unless $format =~ /^(latex|html|template)$/;
325 my $cust_main = $self->cust_main || $self->prospect_main;
326 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
327 unless $cust_main->payname
328 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
330 my $locale = $params{'locale'} || $cust_main->locale;
332 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
333 'html' => [ '<%=', '%>' ],
334 'template' => [ '{', '}' ],
337 warn "$me print_generic creating template\n"
340 # set the notice name here, and nowhere else.
341 my $notice_name = $params{notice_name}
342 || $conf->config('notice_name')
343 || $self->notice_name;
346 my $template = $params{template} ? $params{template} : $self->_agent_template;
347 my $templatefile = $self->template_conf. $format;
348 $templatefile .= "_$template"
349 if length($template) && $conf->exists($templatefile."_$template");
352 my @invoice_template = map "$_\n", $conf->config($templatefile)
353 or die "cannot load config data $templatefile";
355 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
356 #change this to a die when the old code is removed
357 # it's been almost ten years, changing it to a die on the next release.
358 warn "old-style invoice template $templatefile; ".
359 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
360 #$old_latex = 'true';
361 #@invoice_template = _translate_old_latex_format(@invoice_template);
364 warn "$me print_generic creating T:T object\n"
367 my $text_template = new Text::Template(
369 SOURCE => \@invoice_template,
370 DELIMITERS => $delimiters{$format},
373 warn "$me print_generic compiling T:T object\n"
376 $text_template->compile()
377 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
380 # additional substitution could possibly cause breakage in existing templates
383 'notes' => sub { map "$_", @_ },
384 'footer' => sub { map "$_", @_ },
385 'smallfooter' => sub { map "$_", @_ },
386 'returnaddress' => sub { map "$_", @_ },
387 'coupon' => sub { map "$_", @_ },
388 'summary' => sub { map "$_", @_ },
394 s/%%(.*)$/<!-- $1 -->/g;
395 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
396 s/\\begin\{enumerate\}/<ol>/g;
398 s/\\end\{enumerate\}/<\/ol>/g;
399 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
408 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
410 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
416 s/\\hyphenation\{[\w\s\-]+}//;
421 'coupon' => sub { "" },
422 'summary' => sub { "" },
429 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
430 s/\\begin\{enumerate\}//g;
432 s/\\end\{enumerate\}//g;
433 s/\\textbf\{(.*)\}/$1/g;
440 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
442 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
447 s/\\\\\*?\s*$/\n/; # dubious
448 s/\\hyphenation\{[\w\s\-]+}//;
452 'coupon' => sub { "" },
453 'summary' => sub { "" },
458 # hashes for differing output formats
459 my %nbsps = ( 'latex' => '~',
460 'html' => '', # '&nbps;' would be nice
461 'template' => '', # not used
463 my $nbsp = $nbsps{$format};
465 my %escape_functions = ( 'latex' => \&_latex_escape,
466 'html' => \&_html_escape_nbsp,#\&encode_entities,
467 'template' => sub { shift },
469 my $escape_function = $escape_functions{$format};
470 my $escape_function_nonbsp = ($format eq 'html')
471 ? \&_html_escape : $escape_function;
473 my %newline_tokens = ( 'latex' => '\\\\',
477 my $newline_token = $newline_tokens{$format};
479 warn "$me generating template variables\n"
482 # generate template variables
486 defined( $conf->config_orbase( "invoice_${format}returnaddress",
490 && length( $conf->config_orbase( "invoice_${format}returnaddress",
496 $returnaddress = join("\n",
497 $conf->config_orbase("invoice_${format}returnaddress", $template)
501 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
503 my $convert_map = $convert_maps{$format}{'returnaddress'};
506 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
511 } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
513 my $convert_map = $convert_maps{$format}{'returnaddress'};
514 $returnaddress = join( "\n", &$convert_map(
515 map { s/( {2,})/'~' x length($1)/eg;
519 ( $conf->config('company_name', $cust_main->agentnum),
520 $conf->config('company_address', $cust_main->agentnum),
527 my $warning = "Couldn't find a return address; ".
528 "do you need to set the company_address configuration value?";
530 $returnaddress = $nbsp;
531 #$returnaddress = $warning;
535 warn "$me generating invoice data\n"
538 my $agentnum = $cust_main->agentnum;
543 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
544 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
545 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
546 'returnaddress' => $returnaddress,
547 'agent' => &$escape_function($cust_main->agent->agent),
549 #invoice/quotation info
550 'no_number' => $params{'no_number'},
551 'invnum' => ( $params{'no_number'} ? '' : $self->invnum ),
552 'quotationnum' => $self->quotationnum,
553 'no_date' => $params{'no_date'},
554 '_date' => ( $params{'no_date'} ? '' : $self->_date ),
555 # workaround for inconsistent behavior in the early plain text
556 # templates; see RT#28271
557 'date' => ( $params{'no_date'}
559 : ($format eq 'template'
561 : $self->time2str_local('long', $self->_date, $format)
564 'today' => $self->time2str_local('long', $today, $format),
565 'terms' => $self->terms,
566 'template' => $template, #params{'template'},
567 'notice_name' => $notice_name, # escape?
568 'current_charges' => sprintf("%.2f", $self->charged),
569 'duedate' => $self->due_date2str('rdate'), #date_format?
570 'duedate_long' => $self->due_date2str('long'),
573 'custnum' => $cust_main->display_custnum,
574 'prospectnum' => $cust_main->prospectnum,
575 'agent_custid' => &$escape_function($cust_main->agent_custid),
576 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
577 payname company address1 address2 city state zip fax
581 'ship_enable' => $cust_main->invoice_ship_address || $conf->exists('invoice-ship_address'),
582 'unitprices' => $conf->exists('invoice-unitprice'),
583 'smallernotes' => $conf->exists('invoice-smallernotes'),
584 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
585 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
587 #layout info -- would be fancy to calc some of this and bury the template
589 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
590 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
591 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
592 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
593 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
594 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
595 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
596 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
597 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
598 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
600 # better hang on to conf_dir for a while (for old templates)
601 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
603 #these are only used when doing paged plaintext
610 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
611 # prototype here to silence warnings
612 $invoice_data{'time2str'} = sub ($;$$) { $self->time2str_local(@_, $format) };
614 my $min_sdate = 999999999999;
616 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
617 next unless $cust_bill_pkg->pkgnum > 0;
618 $min_sdate = $cust_bill_pkg->sdate
619 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
620 $max_edate = $cust_bill_pkg->edate
621 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
624 $invoice_data{'bill_period'} = '';
625 $invoice_data{'bill_period'} =
626 $self->time2str_local('%e %h', $min_sdate, $format)
628 $self->time2str_local('%e %h', $max_edate, $format)
629 if ($max_edate != 0 && $min_sdate != 999999999999);
631 $invoice_data{finance_section} = '';
632 if ( $conf->config('finance_pkgclass') ) {
634 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
635 $invoice_data{finance_section} = $pkg_class->categoryname;
637 $invoice_data{finance_amount} = '0.00';
638 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
640 my $countrydefault = $conf->config('countrydefault') || 'US';
641 foreach ( qw( address1 address2 city state zip country fax) ){
642 my $method = 'ship_'.$_;
643 $invoice_data{"ship_$_"} = $escape_function->($cust_main->$method);
645 if ( length($cust_main->ship_company) ) {
646 $invoice_data{'ship_company'} = $escape_function->($cust_main->ship_company);
648 $invoice_data{'ship_company'} = $escape_function->($cust_main->company);
650 $invoice_data{'ship_contact'} = $escape_function->($cust_main->contact);
651 $invoice_data{'ship_country'} = ''
652 if ( $invoice_data{'ship_country'} eq $countrydefault );
654 $invoice_data{'cid'} = $params{'cid'}
657 if ( $cust_main->country eq $countrydefault ) {
658 $invoice_data{'country'} = '';
660 $invoice_data{'country'} = &$escape_function($cust_main->bill_country_full);
664 $invoice_data{'address'} = \@address;
667 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
668 ? " (P.O. #". $cust_main->payinfo. ")"
672 push @address, $cust_main->company
673 if $cust_main->company;
674 push @address, $cust_main->address1;
675 push @address, $cust_main->address2
676 if $cust_main->address2;
678 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
679 push @address, $invoice_data{'country'}
680 if $invoice_data{'country'};
682 while (scalar(@address) < 5);
684 $invoice_data{'logo_file'} = $params{'logo_file'}
685 if $params{'logo_file'};
686 $invoice_data{'barcode_file'} = $params{'barcode_file'}
687 if $params{'barcode_file'};
688 $invoice_data{'barcode_img'} = $params{'barcode_img'}
689 if $params{'barcode_img'};
690 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
691 if $params{'barcode_cid'};
693 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
694 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
695 #my $balance_due = $self->owed + $pr_total - $cr_total;
696 my $balance_due = $self->owed;
697 if ( $self->enable_previous ) {
698 $balance_due += $pr_total;
700 # otherwise the previous balance is not shown, so including it in the
701 # balance due is just confusing
703 # the sum of amount owed on all invoices
704 # (this is used in the summary & on the payment coupon)
705 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
707 # flag telling this invoice to have a first-page summary
708 my $summarypage = '';
710 if ( $self->custnum && $self->invnum ) {
711 # XXX should be an FS::cust_bill method to set the defaults, instead
712 # of checking the type here
714 # info from customer's last invoice before this one, for some
716 $invoice_data{'last_bill'} = {};
718 my $last_bill = $self->previous_bill;
721 # "balance_date_range" unfortunately is unsuitable for this, since it
722 # cares about application dates. We want to know the sum of all
723 # _top-level transactions_ dated before the last invoice.
725 # still do this for the "Previous Balance" line of the summary block
727 map "$_ WHERE _date <= ? AND custnum = ?", (
728 "SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill",
729 "SELECT -1 * COALESCE( SUM(amount), 0 ) FROM cust_credit",
730 "SELECT -1 * COALESCE( SUM(paid), 0 ) FROM cust_pay",
731 "SELECT COALESCE( SUM(refund), 0 ) FROM cust_refund",
734 # the customer's current balance immediately after generating the last
737 my $last_bill_balance = $last_bill->charged;
739 my $delta = FS::Record->scalar_sql(
741 $last_bill->_date - 1,
744 $last_bill_balance += $delta;
747 $last_bill_balance = sprintf("%.2f", $last_bill_balance);
749 warn sprintf("LAST BILL: INVNUM %d, DATE %s, BALANCE %.2f\n\n",
751 $self->time2str_local('%D', $last_bill->_date),
754 # ("true_previous_balance" is a terrible name, but at least it's no
755 # longer stored in the database)
756 $invoice_data{'true_previous_balance'} = $last_bill_balance;
758 # Now, get all applications of credits/payments dated on or after the
759 # previous bill, to invoices before the current bill. (The
760 # credit/payment date restriction prevents these from intersecting
761 # the "Previous Balance" set.)
762 # These are "adjustments". The past due balance will be shown as
763 # Previous Balance - Adjustments.
766 "SELECT COALESCE(SUM(y.amount),0) FROM $_ JOIN cust_bill USING (invnum)
767 WHERE cust_bill._date < ?
769 AND cust_bill.custnum = ?"
770 } "cust_credit AS x JOIN cust_credit_bill y USING (crednum)",
771 "cust_pay AS x JOIN cust_bill_pay y USING (paynum)"
774 my $delta = FS::Record->scalar_sql(
780 $adjustments += $delta;
782 $invoice_data{'balance_adjustments'} = sprintf("%.2f", $adjustments);
784 warn sprintf("BALANCE ADJUSTMENTS: %.2f\n\n",
785 $invoice_data{'balance_adjustments'}
788 # the sum of amount owed on all previous invoices
789 # ($pr_total is used elsewhere but not as $previous_balance)
790 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
792 $invoice_data{'last_bill'}{'_date'} = $last_bill->_date; #unformatted
793 my (@payments, @credits);
794 # for formats that itemize previous payments
795 foreach my $cust_pay ( qsearch('cust_pay', {
796 'custnum' => $self->custnum,
797 '_date' => { op => '>=',
798 value => $last_bill->_date }
801 next if $cust_pay->_date > $self->_date;
803 '_date' => $cust_pay->_date,
804 'date' => $self->time2str_local('long', $cust_pay->_date, $format),
805 'payinfo' => $cust_pay->payby_payinfo_pretty,
806 'amount' => sprintf('%.2f', $cust_pay->paid),
808 # not concerned about applications
810 foreach my $cust_credit ( qsearch('cust_credit', {
811 'custnum' => $self->custnum,
812 '_date' => { op => '>=',
813 value => $last_bill->_date }
816 next if $cust_credit->_date > $self->_date;
818 '_date' => $cust_credit->_date,
819 'date' => $self->time2str_local('long', $cust_credit->_date, $format),
820 'creditreason'=> $cust_credit->reason,
821 'amount' => sprintf('%.2f', $cust_credit->amount),
824 $invoice_data{'previous_payments'} = \@payments;
825 $invoice_data{'previous_credits'} = \@credits;
827 # there is no $last_bill
828 $invoice_data{'true_previous_balance'} =
829 $invoice_data{'balance_adjustments'} =
830 $invoice_data{'previous_balance'} = '0.00';
831 $invoice_data{'previous_payments'} = [];
832 $invoice_data{'previous_credits'} = [];
835 if ( $conf->config_bool('invoice_usesummary', $agentnum) ) {
836 $invoice_data{'summarypage'} = $summarypage = 1;
839 } # if this is an invoice
841 warn "$me substituting variables in notes, footer, smallfooter\n"
844 my $tc = $self->template_conf;
845 my @include = ( [ $tc, 'notes' ],
846 [ 'invoice_', 'footer' ],
847 [ 'invoice_', 'smallfooter', ],
848 [ 'invoice_', 'watermark' ],
850 push @include, [ $tc, 'coupon', ]
851 unless $params{'no_coupon'};
853 foreach my $i (@include) {
855 # load the configuration for this sub-template
857 my($base, $include) = @$i;
859 my $inc_file = $conf->key_orbase("$base$format$include", $template);
861 my @inc_src = $conf->config($inc_file, $agentnum);
863 my $converter = $convert_maps{$format}{$include};
865 # then attempt to convert LaTeX to the requested format
866 $inc_file = $conf->key_orbase($base.'latex'.$include, $template);
867 @inc_src = &$converter( $conf->config($inc_file, $agentnum) );
869 # this isn't included in the convert_maps
870 my ($open, $close) = @{ $delimiters{$format} };
875 } # else @inc_src is empty and that's fine
877 # make a Text::Template out of it
879 my $inc_tt = new Text::Template (
881 SOURCE => [ map "$_\n", @inc_src ],
882 DELIMITERS => $delimiters{$format},
883 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
885 unless ( $inc_tt->compile() ) {
886 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
887 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
893 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
895 $invoice_data{$include} =~ s/\n+$//
896 if ($format eq 'latex');
899 # let invoices use either of these as needed
900 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
901 ? $cust_main->payinfo : '';
902 $invoice_data{'po_line'} =
903 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
904 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
907 my %money_chars = ( 'latex' => '',
908 'html' => $conf->config('money_char') || '$',
911 my $money_char = $money_chars{$format};
914 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
915 'html' => $conf->config('money_char') || '$',
918 my $other_money_char = $other_money_chars{$format};
919 $invoice_data{'dollar'} = $other_money_char;
921 my %minus_signs = ( 'latex' => '$-$',
923 'template' => '- ' );
924 my $minus = $minus_signs{$format};
926 my @detail_items = ();
927 my @total_items = ();
931 $invoice_data{'detail_items'} = \@detail_items;
932 $invoice_data{'total_items'} = \@total_items;
933 $invoice_data{'buf'} = \@buf;
934 $invoice_data{'sections'} = \@sections;
936 warn "$me generating sections\n"
939 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
940 my $multisection = $self->has_sections;
941 if ( $multisection ) {
942 $invoice_data{multisection} = $conf->config($tc.'sections_method') || 1;
944 my $section_with_taxes = 1
945 if $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum);
947 my $extra_sections = [];
948 my $extra_lines = ();
950 # default section ('Charges')
951 my $default_section = { 'description' => '',
956 # Previous Charges section
957 # subtotal is the first return value from $self->previous
958 my $previous_section;
959 # if the invoice has major sections, or if we're summarizing previous
960 # charges with a single line, or if we've been specifically told to put them
961 # in a section, create a section for previous charges:
962 if ( $multisection or
963 $conf->exists('previous_balance-summary_only') or
964 $conf->exists('previous_balance-section') ) {
966 $previous_section = { 'description' => $self->mt('Previous Charges'),
967 'subtotal' => $other_money_char.
968 sprintf('%.2f', $pr_total),
969 'summarized' => '', #why? $summarypage ? 'Y' : '',
971 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
972 join(' / ', map { $cust_main->balance_date_range(@$_) }
973 $self->_prior_month30s
975 if $conf->exists('invoice_include_aging');
978 # otherwise put them in the main section
979 $previous_section = $default_section;
982 my $adjust_section = {
983 'description' => $self->mt('Credits, Payments, and Adjustments'),
984 'adjust_section' => 1,
985 'subtotal' => 0, # adjusted below
987 my $adjust_weight = _pkg_category($adjust_section->{description})
988 ? _pkg_category($adjust_section->{description})->weight
990 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
991 # Note: 'sort_weight' here is actually a flag telling whether there is an
992 # explicit package category for the adjust section. If so, certain behavior
994 $adjust_section->{'sort_weight'} = $adjust_weight;
997 if ( $multisection ) {
998 ($extra_sections, $extra_lines) =
999 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
1000 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
1001 && $self->can('_items_extra_usage_sections');
1003 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
1005 push @detail_items, @$extra_lines if $extra_lines;
1007 # the code is written so that both methods can be used together, but
1008 # we haven't yet changed the template to take advantage of that, so for
1009 # now, treat them as mutually exclusive.
1010 my %section_method = ( by_category => 1 );
1011 if ( $conf->config($tc.'sections_method') eq 'location' ) {
1012 %section_method = ( by_location => 1 );
1014 my ($early, $late) =
1015 $self->_items_sections( 'summary' => $summarypage,
1016 'escape' => $escape_function_nonbsp,
1017 'extra_sections' => $extra_sections,
1018 'format' => $format,
1021 push @sections, @$early;
1022 $late_sections = $late;
1024 if ( $conf->exists('svc_phone_sections')
1025 && $self->can('_items_svc_phone_sections')
1028 my ($phone_sections, $phone_lines) =
1029 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
1030 push @{$late_sections}, @$phone_sections;
1031 push @detail_items, @$phone_lines;
1033 if ( $conf->exists('voip-cust_accountcode_cdr')
1034 && $cust_main->accountcode_cdr
1035 && $self->can('_items_accountcode_cdr')
1038 my ($accountcode_section, $accountcode_lines) =
1039 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
1040 if ( scalar(@$accountcode_lines) ) {
1041 push @{$late_sections}, $accountcode_section;
1042 push @detail_items, @$accountcode_lines;
1045 } else {# not multisection
1046 # make a default section
1047 push @sections, $default_section;
1048 # and calculate the finance charge total, since it won't get done otherwise.
1049 # and the default section total
1050 # XXX possibly finance_pkgclass should not be used in this manner?
1051 my @finance_charges;
1053 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1054 if ( $invoice_data{finance_section} and
1055 grep { $_->section eq $invoice_data{finance_section} }
1056 $cust_bill_pkg->cust_bill_pkg_display ) {
1057 # I think these are always setup fees, but just to be sure...
1058 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1060 push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1063 $invoice_data{finance_amount} =
1064 sprintf('%.2f', sum( @finance_charges ) || 0);
1065 $default_section->{subtotal} = $other_money_char.
1066 sprintf('%.2f', sum( @charges ) || 0);
1069 # start setting up summary subtotals
1070 my @summary_subtotals;
1071 my $method = $conf->config('summary_subtotals_method');
1072 if ( ( ref($self) ne 'FS::quotation' ) and $method and $method ne $conf->config($tc.'sections_method') ) {
1073 # then re-section them by the correct method
1074 my %section_method = ( by_category => 1 );
1075 if ( $conf->config('summary_subtotals_method') eq 'location' ) {
1076 %section_method = ( by_location => 1 );
1078 my ($early, $late) =
1079 $self->_items_sections( 'summary' => $summarypage,
1080 'escape' => $escape_function_nonbsp,
1081 'extra_sections' => $extra_sections,
1082 'format' => $format,
1085 foreach ( @$early ) {
1086 next if $_->{subtotal} == 0;
1087 $_->{subtotal} = $other_money_char.sprintf('%.2f', $_->{subtotal});
1088 push @summary_subtotals, $_;
1091 # subtotal sectioning is the same as for the actual invoice sections
1092 @summary_subtotals = @sections;
1095 # Hereafter, push sections to both @sections and @summary_subtotals
1096 # if they belong in both places (e.g. tax section). Late sections are
1097 # never in @summary_subtotals.
1099 # previous invoice balances in the Previous Charges section if there
1100 # is one, otherwise in the main detail section
1101 # (except if summary_only is enabled, don't show them at all)
1102 if ( $self->can('_items_previous') &&
1103 $self->enable_previous &&
1104 ! $conf->exists('previous_balance-summary_only') ) {
1106 warn "$me adding previous balances\n"
1109 foreach my $line_item ( $self->_items_previous ) {
1112 ref => $line_item->{'pkgnum'},
1113 pkgpart => $line_item->{'pkgpart'},
1114 #quantity => 1, # not really correct
1115 section => $previous_section, # which might be $default_section
1116 description => &$escape_function($line_item->{'description'}),
1117 ext_description => [ map { &$escape_function($_) }
1118 @{ $line_item->{'ext_description'} || [] }
1120 amount => $money_char . $line_item->{'amount'},
1121 product_code => $line_item->{'pkgpart'} || 'N/A',
1124 push @detail_items, $detail;
1125 push @buf, [ $detail->{'description'},
1126 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1132 if ( @pr_cust_bill && $self->enable_previous ) {
1133 push @buf, ['','-----------'];
1134 push @buf, [ $self->mt('Total Previous Balance'),
1135 $money_char. sprintf("%10.2f", $pr_total) ];
1139 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
1140 warn "$me adding DID summary\n"
1143 my ($didsummary,$minutes) = $self->_did_summary;
1144 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
1146 { 'description' => $didsummary_desc,
1147 'ext_description' => [ $didsummary, $minutes ],
1151 foreach my $section (@sections, @$late_sections) {
1153 # begin some normalization
1154 $section->{'subtotal'} = $section->{'amount'}
1156 && !exists($section->{subtotal})
1157 && exists($section->{amount});
1159 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
1160 if ( $invoice_data{finance_section} &&
1161 $section->{'description'} eq $invoice_data{finance_section} );
1163 if ( $multisection ) {
1165 if ( ref($section->{'subtotal'}) ) {
1167 $section->{'subtotal'} =
1168 sprintf("$other_money_char%.2f to $other_money_char%.2f",
1169 $section->{'subtotal'}[0],
1170 $section->{'subtotal'}[1]
1175 $section->{'subtotal'} = $other_money_char.
1176 sprintf('%.2f', $section->{'subtotal'})
1180 # continue some normalization
1181 $section->{'amount'} = $section->{'subtotal'}
1185 if ( $section->{'description'} ) {
1186 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1191 warn "$me setting options\n"
1195 $options{'section'} = $section if $multisection;
1196 $options{'section_with_taxes'} = 1
1198 && $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum);
1199 $options{'format'} = $format;
1200 $options{'escape_function'} = $escape_function;
1201 $options{'no_usage'} = 1 unless $unsquelched;
1202 $options{'unsquelched'} = $unsquelched;
1203 $options{'summary_page'} = $summarypage;
1204 $options{'skip_usage'} =
1205 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1206 $options{'preref_callback'} = $params{'preref_callback'};
1207 $options{'disable_line_item_date_ranges'} =
1208 $conf->exists('disable_line_item_date_ranges');
1210 warn "$me searching for line items\n"
1213 my %section_tax_lines;
1216 foreach my $line_item ( $self->_items_pkg(%options),
1217 $self->_items_fee(%options) ) {
1219 warn "$me adding line item ".
1220 join(', ', map "$_=>".$line_item->{$_}, keys %$line_item). "\n"
1223 push @buf, ( [ $line_item->{'description'},
1224 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1226 map { [ " ". $_, '' ] } @{$line_item->{'ext_description'}},
1229 $line_item->{'ref'} = $line_item->{'pkgnum'};
1230 $line_item->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; # mt()?
1231 $line_item->{'section'} = $section;
1232 $line_item->{'description'} = &$escape_function($line_item->{'description'});
1233 $line_item->{'amount'} = $money_char.$line_item->{'amount'};
1235 if ( length($line_item->{'unit_amount'}) ) {
1236 $line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'};
1238 $line_item->{'ext_description'} ||= [];
1240 if ( $section_with_taxes && ref $line_item->{pkg_tax} ) {
1241 for my $line_tax ( @{$ line_item->{pkg_tax} } ) {
1243 # It is rarely possible for the same tax record to be presented here
1244 # multiple times. See cust_bill_pkg::_pkg_tax_list for more info
1245 next if $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} };
1246 $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} } = 1;
1248 $section_tax_lines{ $line_tax->{taxname} } += $line_tax->{amount};
1252 push @detail_items, $line_item;
1255 # If conf flag invoice_sections_with_taxes:
1256 # - Add @detail_items for taxes into each section
1257 # - Update section subtotal to include taxes
1258 if ( $section_with_taxes && %section_tax_lines ) {
1259 for my $taxname ( keys %section_tax_lines ) {
1261 push @detail_items, {
1262 section => $section,
1263 amount => sprintf($money_char."%.2f",$section_tax_lines{$taxname}),
1264 description => &$escape_function($taxname),
1267 # Append taxes to total. If line format resembles "$5.00 to $12.00"
1268 # append to the second value.
1269 if ($section->{subtotal} =~ /to/) {
1270 my @subtotal = split /\s/, $section->{subtotal};
1271 $subtotal[2] =~ s/[^\d\.]//g;
1272 $subtotal[2] = sprintf(
1274 ( $subtotal[2] + $section_tax_lines{$taxname} )
1276 $section->{subtotal} = join ' ', @subtotal;
1278 $section->{subtotal} =~ s/[^\d\.]//g;
1279 $section->{subtotal} = sprintf(
1280 $money_char . "%.2f",
1281 ( $section->{subtotal} + $section_tax_lines{$taxname} )
1288 if ( $section->{'description'} ) {
1289 push @buf, ( ['','-----------'],
1290 [ $section->{'description'}. ' sub-total',
1291 $section->{'subtotal'} # already formatted this
1300 $invoice_data{current_less_finance} =
1301 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1303 # if there's anything in the Previous Charges section, prepend it to the list
1304 if ( $pr_total and $previous_section ne $default_section ) {
1305 unshift @sections, $previous_section;
1306 # but not @summary_subtotals
1309 warn "$me adding taxes\n"
1312 # create a tax section if we don't yet have one
1313 my $tax_description = 'Taxes, Surcharges, and Fees';
1315 List::Util::first { $_->{description} eq $tax_description } @sections;
1316 if (!$tax_section) {
1317 $tax_section = { 'description' => $tax_description };
1318 push @sections, $tax_section if $multisection;
1320 $tax_section->{tax_section} = 1; # mark this section as containing taxes
1321 # if this is an existing tax section, we're merging the tax items into it.
1322 # grab the taxtotal that's already there, strip the money symbol if any
1323 my $taxtotal = $tax_section->{'subtotal'} || 0;
1324 $taxtotal =~ s/^\Q$other_money_char\E//;
1327 #my $tax_weight = _pkg_category($tax_section->{description})
1328 # ? _pkg_category($tax_section->{description})->weight
1330 #$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
1331 #$tax_section->{'sort_weight'} = $tax_weight;
1333 my @items_tax = $self->_items_tax;
1334 foreach my $tax ( @items_tax ) {
1336 $taxtotal += $tax->{'amount'};
1338 my $description = &$escape_function( $tax->{'description'} );
1339 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1341 if ( $multisection ) {
1343 push @detail_items, {
1344 ext_description => [],
1347 description => $description,
1348 amount => $money_char. $amount,
1350 section => $tax_section,
1355 push @total_items, {
1356 'total_item' => $description,
1357 'total_amount' => $other_money_char. $amount,
1362 push @buf,[ $description,
1363 $money_char. $amount,
1370 $total->{'total_item'} = $self->mt('Sub-total');
1371 $total->{'total_amount'} =
1372 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1374 if ( $multisection ) {
1375 if ( $taxtotal > 0 ) {
1376 # there are taxes, so prepare the section to be displayed.
1377 # $taxtotal already includes any line items that were already in the
1378 # section (fees, taxes that are charged as packages for some reason).
1379 # also set 'summarized' to false so that this isn't a summary-only
1381 $tax_section->{'subtotal'} = $other_money_char.
1382 sprintf('%.2f', $taxtotal);
1383 $tax_section->{'pretotal'} = 'New charges sub-total '.
1384 $total->{'total_amount'};
1385 $tax_section->{'description'} = $self->mt($tax_description);
1386 $tax_section->{'summarized'} = '';
1388 if ( $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum) ) {
1390 # remove tax section if taxes are itemized within other sections
1391 @sections = grep{ $_ ne $tax_section } @sections;
1393 } elsif ( !grep $tax_section, @sections ) {
1395 # append it if it's not already there
1396 push @sections, $tax_section;
1397 push @summary_subtotals, $tax_section;
1404 unshift @total_items, $total;
1407 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1413 my %embolden_functions = (
1414 'latex' => sub { return '\textbf{'. shift(). '}' },
1415 'html' => sub { return '<b>'. shift(). '</b>' },
1416 'template' => sub { shift },
1418 my $embolden_function = $embolden_functions{$format};
1420 if ( $multisection ) {
1422 if ( $adjust_section->{'sort_weight'} ) {
1423 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1424 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1426 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1427 $other_money_char. sprintf('%.2f', $self->charged );
1432 if ( $self->can('_items_total') ) { # should always be true now
1434 # even for multisection, need plain text version
1436 my @new_total_items = $self->_items_total;
1438 push @buf,['','-----------'];
1440 foreach ( @new_total_items ) {
1441 my ($item, $amount) = ($_->{'total_item'}, $_->{'total_amount'});
1442 $_->{'total_item'} = &$embolden_function( $item );
1444 if ( ref($amount) ) {
1445 $_->{'total_amount'} = &$embolden_function(
1446 $other_money_char.$amount->[0]. ' to '.
1447 $other_money_char.$amount->[1]
1450 $_->{'total_amount'} = &$embolden_function( $other_money_char.$amount );
1453 # but if it's multisection, don't append to @total_items. the adjust
1454 # section has all this stuff
1455 push @total_items, $_ if !$multisection;
1456 push @buf, [ $item, $money_char.sprintf('%10.2f',$amount) ];
1459 push @buf, [ '', '' ];
1461 # if we're showing previous invoices, also show previous
1462 # credits and payments
1463 if ( $self->enable_previous
1464 and $self->can('_items_credits')
1465 and $self->can('_items_payments') )
1469 my $credittotal = 0;
1470 foreach my $credit (
1471 $self->_items_credits( 'template' => $template, 'trim_len' => 40 )
1475 $total->{'total_item'} = &$escape_function($credit->{'description'});
1476 $credittotal += $credit->{'amount'};
1477 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1478 if ( $multisection ) {
1479 push @detail_items, {
1480 ext_description => [],
1483 description => &$escape_function($credit->{'description'}),
1484 amount => $money_char . $credit->{'amount'},
1486 section => $adjust_section,
1489 push @total_items, $total;
1493 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1496 foreach my $credit (
1497 $self->_items_credits( 'template' => $template, 'trim_len'=>32 )
1499 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1503 my $paymenttotal = 0;
1504 foreach my $payment (
1505 $self->_items_payments( 'template' => $template )
1508 $total->{'total_item'} = &$escape_function($payment->{'description'});
1509 $paymenttotal += $payment->{'amount'};
1510 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1511 if ( $multisection ) {
1512 push @detail_items, {
1513 ext_description => [],
1516 description => &$escape_function($payment->{'description'}),
1517 amount => $money_char . $payment->{'amount'},
1519 section => $adjust_section,
1522 push @total_items, $total;
1524 push @buf, [ $payment->{'description'},
1525 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1528 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1530 if ( $multisection ) {
1531 $adjust_section->{'subtotal'} = $other_money_char.
1532 sprintf('%.2f', $credittotal + $paymenttotal);
1534 #why this? because {sort_weight} forces the adjust_section to appear
1535 #in @extra_sections instead of @sections. obviously.
1536 push @sections, $adjust_section
1537 unless $adjust_section->{sort_weight};
1538 # do not summarize; adjustments there are shown according to
1542 # create Balance Due message
1545 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1546 $total->{'total_amount'} =
1547 &$embolden_function(
1548 $other_money_char. sprintf('%.2f', #why? $summarypage
1549 # ? $self->charged +
1550 # $self->billing_balance
1552 $self->owed + $pr_total
1555 if ( $multisection && !$adjust_section->{sort_weight} ) {
1556 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1557 $total->{'total_amount'};
1559 push @total_items, $total;
1561 push @buf,['','-----------'];
1562 push @buf,[$self->balance_due_msg, $money_char.
1563 sprintf("%10.2f", $balance_due ) ];
1566 if ( $conf->exists('previous_balance-show_credit')
1567 and $cust_main->balance < 0 ) {
1568 my $credit_total = {
1569 'total_item' => &$embolden_function($self->credit_balance_msg),
1570 'total_amount' => &$embolden_function(
1571 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1574 if ( $multisection ) {
1575 $adjust_section->{'posttotal'} .= $newline_token .
1576 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1579 push @total_items, $credit_total;
1581 push @buf,['','-----------'];
1582 push @buf,[$self->credit_balance_msg, $money_char.
1583 sprintf("%10.2f", -$cust_main->balance ) ];
1587 } #end of default total adding ! can('_items_total')
1589 if ( $multisection ) {
1590 if ( $conf->exists('svc_phone_sections')
1591 && $self->can('_items_svc_phone_sections')
1595 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1596 $total->{'total_amount'} =
1597 &$embolden_function(
1598 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1600 my $last_section = pop @sections;
1601 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1602 $total->{'total_amount'};
1603 push @sections, $last_section;
1605 push @sections, @$late_sections
1609 # make a discounts-available section, even without multisection
1610 if ( $conf->exists('discount-show_available')
1611 and my @discounts_avail = $self->_items_discounts_avail ) {
1612 my $discount_section = {
1613 'description' => $self->mt('Discounts Available'),
1618 push @sections, $discount_section; # do not summarize
1619 push @detail_items, map { +{
1620 'ref' => '', #should this be something else?
1621 'section' => $discount_section,
1622 'description' => &$escape_function( $_->{description} ),
1623 'amount' => $money_char . &$escape_function( $_->{amount} ),
1624 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1625 } } @discounts_avail;
1628 # not adding any more sections after this
1629 $invoice_data{summary_subtotals} = \@summary_subtotals;
1632 if ( $conf->exists('usage_class_summary')
1633 and $self->can('_items_usage_class_summary') ) {
1634 my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function, 'money_char' => $other_money_char);
1635 if ( @usage_subtotals ) {
1636 unshift @sections, $usage_subtotals[0]->{section}; # do not summarize
1637 unshift @detail_items, @usage_subtotals;
1641 # invoice history "section" (not really a section)
1642 # not to be included in any subtotals, completely independent of
1644 if ( $conf->exists('previous_invoice_history') and $cust_main->isa('FS::cust_main') ) {
1647 foreach my $cust_bill ( $cust_main->cust_bill ) {
1648 # XXX hardcoded format, and currently only 'charged'; add other fields
1649 # if they become necessary
1650 my $date = $self->time2str_local('%b %Y', $cust_bill->_date);
1651 $history{$date} ||= 0;
1652 $history{$date} += $cust_bill->charged;
1653 # just so we have a numeric sort key
1654 $monthorder{$date} ||= $cust_bill->_date;
1656 my @sorted_months = sort { $monthorder{$a} <=> $monthorder{$b} }
1658 my @sorted_amounts = map { sprintf('%.2f', $history{$_}) } @sorted_months;
1659 $invoice_data{monthly_history} = [ \@sorted_months, \@sorted_amounts ];
1662 # service locations: another option for template customization
1664 foreach my $item (@detail_items) {
1665 if ( $item->{locationnum} ) {
1666 $location_info{ $item->{locationnum} } ||= {
1667 FS::cust_location->by_key( $item->{locationnum} )->location_hash
1671 $invoice_data{location_info} = \%location_info;
1673 # debugging hook: call this with 'diag' => 1 to just get a hash of
1674 # the invoice variables
1675 return \%invoice_data if ( $params{'diag'} );
1677 # All sections and items are built; now fill in templates.
1678 my @includelist = ();
1679 push @includelist, 'summary' if $summarypage;
1680 foreach my $include ( @includelist ) {
1682 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1685 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1687 @inc_src = $conf->config($inc_file, $agentnum);
1691 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1693 my $convert_map = $convert_maps{$format}{$include};
1695 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1696 s/--\@\]/$delimiters{$format}[1]/g;
1699 &$convert_map( $conf->config($inc_file, $agentnum) );
1703 my $inc_tt = new Text::Template (
1705 SOURCE => [ map "$_\n", @inc_src ],
1706 DELIMITERS => $delimiters{$format},
1707 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1709 unless ( $inc_tt->compile() ) {
1710 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1711 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1715 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1717 $invoice_data{$include} =~ s/\n+$//
1718 if ($format eq 'latex');
1723 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1724 /invoice_lines\((\d*)\)/;
1725 $invoice_lines += $1 || scalar(@buf);
1728 die "no invoice_lines() functions in template?"
1729 if ( $format eq 'template' && !$wasfunc );
1731 if ( $invoice_lines ) {
1732 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1733 $invoice_data{'total_pages'}++
1734 if scalar(@buf) % $invoice_lines;
1737 #setup subroutine for the template
1738 $invoice_data{invoice_lines} = sub {
1739 my $lines = shift || scalar(@buf);
1748 if ($format eq 'template') {
1753 push @collect, split("\n",
1754 $text_template->fill_in( HASH => \%invoice_data )
1756 $invoice_data{'page'}++;
1758 map "$_\n", @collect;
1760 } else { # this is where we actually create the invoice
1762 if ( $params{no_addresses} ) {
1763 delete $invoice_data{$_} foreach qw(
1764 payname company address1 address2 city state zip country
1766 $invoice_data{returnaddress} = '~';
1769 warn "filling in template for invoice ". $self->invnum. "\n"
1771 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1774 $text_template->fill_in(HASH => \%invoice_data);
1778 sub notice_name { '('.shift->table.')'; }
1780 sub template_conf { 'invoice_'; }
1782 # helper routine for generating date ranges
1783 sub _prior_month30s {
1786 [ 1, 2592000 ], # 0-30 days ago
1787 [ 2592000, 5184000 ], # 30-60 days ago
1788 [ 5184000, 7776000 ], # 60-90 days ago
1789 [ 7776000, 0 ], # 90+ days ago
1792 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1793 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1798 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1800 Returns an postscript invoice, as a scalar.
1802 Options can be passed as a hashref (recommended) or as a list of time, template
1803 and then any key/value pairs for any other options.
1805 I<time> an optional value used to control the printing of overdue messages. The
1806 default is now. It isn't the date of the invoice; that's the `_date' field.
1807 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1808 L<Time::Local> and L<Date::Parse> for conversion functions.
1810 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1817 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1818 my $ps = generate_ps($file);
1820 unlink($barcodefile) if $barcodefile;
1825 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1827 Returns an PDF invoice, as a scalar.
1829 Options can be passed as a hashref (recommended) or as a list of time, template
1830 and then any key/value pairs for any other options.
1832 I<time> an optional value used to control the printing of overdue messages. The
1833 default is now. It isn't the date of the invoice; that's the `_date' field.
1834 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1835 L<Time::Local> and L<Date::Parse> for conversion functions.
1837 I<template>, if specified, is the name of a suffix for alternate invoices.
1839 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1846 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1847 my $pdf = generate_pdf($file);
1849 unlink($barcodefile) if $barcodefile;
1854 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1856 Returns an HTML invoice, as a scalar.
1858 I<time> an optional value used to control the printing of overdue messages. The
1859 default is now. It isn't the date of the invoice; that's the `_date' field.
1860 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1861 L<Time::Local> and L<Date::Parse> for conversion functions.
1863 I<template>, if specified, is the name of a suffix for alternate invoices.
1865 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1867 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1868 when emailing the invoice as part of a multipart/related MIME email.
1876 %params = %{ shift() };
1880 $params{'format'} = 'html';
1882 $self->print_generic( %params );
1885 # quick subroutine for print_latex
1887 # There are ten characters that LaTeX treats as special characters, which
1888 # means that they do not simply typeset themselves:
1889 # # $ % & ~ _ ^ \ { }
1891 # TeX ignores blanks following an escaped character; if you want a blank (as
1892 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1896 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1897 $value =~ s/([<>])/\$$1\$/g;
1903 encode_entities($value);
1907 sub _html_escape_nbsp {
1908 my $value = _html_escape(shift);
1909 $value =~ s/ +/ /g;
1913 #utility methods for print_*
1915 sub _translate_old_latex_format {
1916 warn "_translate_old_latex_format called\n"
1923 if ( $line =~ /^%%Detail\s*$/ ) {
1925 push @template, q![@--!,
1926 q! foreach my $_tr_line (@detail_items) {!,
1927 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1928 q! $_tr_line->{'description'} .= !,
1929 q! "\\tabularnewline\n~~".!,
1930 q! join( "\\tabularnewline\n~~",!,
1931 q! @{$_tr_line->{'ext_description'}}!,
1935 while ( ( my $line_item_line = shift )
1936 !~ /^%%EndDetail\s*$/ ) {
1937 $line_item_line =~ s/'/\\'/g; # nice LTS
1938 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1939 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1940 push @template, " \$OUT .= '$line_item_line';";
1943 push @template, '}',
1946 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1948 push @template, '[@--',
1949 ' foreach my $_tr_line (@total_items) {';
1951 while ( ( my $total_item_line = shift )
1952 !~ /^%%EndTotalDetails\s*$/ ) {
1953 $total_item_line =~ s/'/\\'/g; # nice LTS
1954 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1955 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1956 push @template, " \$OUT .= '$total_item_line';";
1959 push @template, '}',
1963 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1964 push @template, $line;
1970 warn "$_\n" foreach @template;
1982 my $conf = $self->conf;
1984 #check for an invoice-specific override
1985 return $self->invoice_terms if $self->invoice_terms;
1987 #check for a customer- specific override
1988 my $cust_main = $self->cust_main;
1989 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1993 $agentnum = $cust_main->agentnum;
1994 } elsif ( my $prospect_main = $self->prospect_main ) {
1995 $agentnum = $prospect_main->agentnum;
1998 #use configured default
1999 $conf->config('invoice_default_terms', $agentnum) || '';
2009 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2010 $duedate = $self->_date() + ( $1 * 86400 );
2011 } elsif ( $self->terms =~ /^End of Month$/ ) {
2012 my ($mon,$year) = (localtime($self->_date) )[4,5];
2014 until ( $mon < 12 ) { $mon -= 12; $year++; }
2015 my $nextmonth_first = timelocal(0,0,0,1,$mon,$year);
2016 $duedate = $nextmonth_first - 86400;
2027 $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
2030 =item invoice_pay_by_msg
2032 displays the invoice_pay_by_msg or default Please pay by [_1] if empty.
2036 sub invoice_pay_by_msg {
2040 $self->conf->config('invoice_pay_by_msg', $self->agentnum)
2041 || 'Please pay by [_1]';
2042 $msg .= ' - ' . $self->mt($please_pay_by, $self->due_date2str('short')) . ' ';
2047 =item balance_due_msg
2051 sub balance_due_msg {
2053 my $msg = $self->mt('Balance Due');
2054 return $msg unless $self->terms; # huh?
2055 if ( !$self->conf->exists('invoice_show_prior_due_date')
2056 || $self->has_sections ) {
2057 # if enabled, the due date is shown with Total New Charges (see
2058 # _items_total) and not here
2059 # (yes, or if invoice_sections is enabled; this is just for compatibility)
2060 if ( $self->due_date ) {
2061 $msg .= $self->invoice_pay_by_msg
2062 unless $self->conf->config_bool('invoice_omit_due_date',$self->agentnum);
2063 } elsif ( $self->terms ) {
2064 $msg .= ' - '. $self->mt($self->terms);
2070 =item balance_due_date
2074 sub balance_due_date {
2076 my $conf = $self->conf;
2078 my $terms = $self->terms;
2079 if ( $terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2080 $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
2085 sub credit_balance_msg {
2087 $self->mt('Credit Balance Remaining')
2092 Returns a string with the date, for example: "3/20/2008", localized for the
2093 customer. Use _date_pretty_unlocalized for non-end-customer display use.
2099 $self->time2str_local('short', $self->_date);
2102 =item _date_pretty_unlocalized
2104 Returns a string with the date, for example: "3/20/2008", in the format
2105 configured for the back-office. Use _date_pretty for end-customer display use.
2109 sub _date_pretty_unlocalized {
2111 time2str($date_format, $self->_date);
2116 Emails this template.
2118 Options are passed as a hashref. Available options:
2124 If specified, overrides the default From: address.
2128 If specified, overrides the name of the sent document ("Invoice" or "Quotation")
2132 (Deprecated) If specified, is the name of a suffix for alternate template files.
2136 Options accepted by generate_email can also be used.
2142 my $opt = shift || {};
2143 if ($opt and !ref($opt)) {
2144 die ref($self). '->email called with positional parameters';
2147 return if $self->hide;
2149 my $error = send_email(
2150 $self->generate_email(
2151 'subject' => $self->email_subject($opt->{template}),
2152 %$opt, # template, etc.
2156 die "can't email: $error\n" if $error;
2159 =item generate_email OPTION => VALUE ...
2167 sender address, required
2171 alternate template name, optional
2175 email subject, optional
2179 notice name instead of "Invoice", optional
2183 Returns an argument list to be passed to L<FS::Misc/send_email>.
2190 sub generate_email {
2194 my $conf = $self->conf;
2196 my $me = '[FS::Template_Mixin::generate_email]';
2199 'from' => $args{'from'},
2200 'subject' => ($args{'subject'} || $self->email_subject),
2201 'custnum' => $self->custnum,
2202 'msgtype' => 'invoice',
2205 $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
2207 my $cust_main = $self->cust_main;
2209 if (ref($args{'to'}) eq 'ARRAY') {
2210 $return{'to'} = $args{'to'};
2211 } elsif ( $cust_main ) {
2212 $return{'to'} = [ $cust_main->invoicing_list_emailonly ];
2215 my $tc = $self->template_conf;
2217 my @text; # array of lines
2218 my $html; # a big string
2219 my @related_parts; # will contain the text/HTML alternative, and images
2220 my $related; # will contain the multipart/related object
2222 if ( $conf->exists($tc. 'email_pdf') ) {
2223 if ( my $msgnum = $conf->config($tc.'email_pdf_msgnum') ) {
2225 warn "$me using '${tc}email_pdf_msgnum' in multipart message"
2228 my $msg_template = FS::msg_template->by_key($msgnum)
2229 or die "${tc}email_pdf_msgnum $msgnum not found\n";
2230 my %prepared = $msg_template->prepare(
2231 cust_main => $self->cust_main,
2235 @text = split(/(?=\n)/, $prepared{'text_body'});
2236 $html = $prepared{'html_body'};
2238 } elsif ( my @note = $conf->config($tc.'email_pdf_note') ) {
2240 warn "$me using '${tc}email_pdf_note' in multipart message"
2242 @text = $conf->config($tc.'email_pdf_note');
2243 $html = join('<BR>', @text);
2245 } # else use the plain text invoice
2250 if ( $conf->exists($tc.'template') ) {
2252 warn "$me generating plain text invoice"
2255 @text = $self->print_text(\%args);
2259 warn "$me no plain text version exists; sending empty message body"
2266 my $text_part = build MIME::Entity (
2267 'Type' => 'text/plain',
2268 'Encoding' => 'quoted-printable',
2269 'Charset' => 'UTF-8',
2270 #'Encoding' => '7bit',
2273 { Encode::encode('UTF-8', $_, Encode::FB_WARN | Encode::LEAVE_SRC ) }
2276 'Disposition' => 'inline',
2281 if ( $conf->exists($tc.'html') ) {
2282 warn "$me generating HTML invoice"
2285 $args{'from'} =~ /\@([\w\.\-]+)/;
2286 my $from = $1 || 'example.com';
2287 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2290 my $agentnum = $cust_main ? $cust_main->agentnum
2291 : $self->prospect_main->agentnum;
2292 if ( defined($args{'template'}) && length($args{'template'})
2293 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
2296 $logo = 'logo_'. $args{'template'}. '.png';
2300 my $image_data = $conf->config_binary( $logo, $agentnum);
2302 push @related_parts, build MIME::Entity
2303 'Type' => 'image/png',
2304 'Encoding' => 'base64',
2305 'Data' => $image_data,
2306 'Filename' => 'logo.png',
2307 'Content-ID' => "<$content_id>",
2310 if ( ref($self) eq 'FS::cust_bill' && $conf->exists('invoice-barcode') ) {
2311 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2312 push @related_parts, build MIME::Entity
2313 'Type' => 'image/png',
2314 'Encoding' => 'base64',
2315 'Data' => $self->invoice_barcode(0),
2316 'Filename' => 'barcode.png',
2317 'Content-ID' => "<$barcode_content_id>",
2319 $args{'barcode_cid'} = $barcode_content_id;
2322 $html = $self->print_html({ 'cid'=>$content_id, %args });
2329 warn "$me creating HTML/text multipart message"
2332 $return{'nobody'} = 1;
2334 my $alternative = build MIME::Entity
2335 'Type' => 'multipart/alternative',
2336 #'Encoding' => '7bit',
2337 'Disposition' => 'inline'
2341 $alternative->add_part($text_part);
2344 $alternative->attach(
2345 'Type' => 'text/html',
2346 'Encoding' => 'quoted-printable',
2347 'Data' => [ '<html>',
2350 ' '. encode_entities($return{'subject'}),
2353 ' <body bgcolor="#e8e8e8">',
2357 Encode::FB_WARN | Encode::LEAVE_SRC
2362 'Disposition' => 'inline',
2363 #'Filename' => 'invoice.pdf',
2366 unshift @related_parts, $alternative;
2368 $related = build MIME::Entity 'Type' => 'multipart/related',
2369 'Encoding' => '7bit';
2371 #false laziness w/Misc::send_email
2372 $related->head->replace('Content-type',
2373 $related->mime_type.
2374 '; boundary="'. $related->head->multipart_boundary. '"'.
2375 '; type=multipart/alternative'
2378 $related->add_part($_) foreach @related_parts;
2382 my @otherparts = ();
2383 if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) {
2385 if ( $conf->config('voip-cdr_email_attach') eq 'zip' ) {
2387 my $data = join('', map "$_\n",
2388 $self->call_details(prepend_billed_number=>1)
2391 my $zip = new Archive::Zip;
2392 my $file = $zip->addString( $data, 'usage-'.$self->invnum.'.csv' );
2393 $file->desiredCompressionMethod( COMPRESSION_DEFLATED );
2396 my $SH = IO::Scalar->new(\$zipdata);
2397 my $status = $zip->writeToFileHandle($SH);
2398 die "Error zipping CDR attachment: $!" unless $status == AZ_OK;
2400 push @otherparts, build MIME::Entity
2401 'Type' => 'application/zip',
2402 'Encoding' => 'base64',
2404 'Disposition' => 'attachment',
2405 'Filename' => 'usage-'. $self->invnum. '.zip',
2408 } else { # } elsif ( $conf->config('voip-cdr_email_attach') eq 'csv' ) {
2410 push @otherparts, build MIME::Entity
2411 'Type' => 'text/csv',
2412 'Encoding' => '7bit',
2413 'Data' => [ map { "$_\n" }
2414 $self->call_details('prepend_billed_number' => 1)
2416 'Disposition' => 'attachment',
2417 'Filename' => 'usage-'. $self->invnum. '.csv',
2424 if ( $conf->exists($tc.'email_pdf') ) {
2429 # multipart/alternative
2435 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
2436 push @otherparts, $pdf;
2440 $return{'content-type'} = 'multipart/mixed'; # of the outer container
2442 $return{'mimeparts'} = [ $related, @otherparts ];
2443 $return{'type'} = 'multipart/related'; # of the first part
2445 $return{'mimeparts'} = [ $text_part, @otherparts ];
2446 $return{'type'} = 'text/plain';
2448 } elsif ( $html ) { # no PDF or CSV, strip the outer container
2449 $return{'mimeparts'} = \@related_parts;
2450 $return{'content-type'} = 'multipart/related';
2451 $return{'type'} = 'multipart/alternative';
2452 } else { # no HTML either
2453 $return{'body'} = \@text;
2454 $return{'content-type'} = 'text/plain';
2463 Returns a list suitable for passing to MIME::Entity->build(), representing
2464 this quotation or invoice as PDF attachment.
2471 'Type' => 'application/pdf',
2472 'Encoding' => 'base64',
2473 'Data' => [ $self->print_pdf(@_) ],
2474 'Disposition' => 'attachment',
2475 'Filename' => $self->pdf_filename,
2479 =item postal_mail_fsinc
2481 Sends this invoice to the Freeside Internet Services, Inc. print and mail
2487 use IO::Socket::SSL;
2489 use HTTP::Request::Common qw( POST );
2490 use Cpanel::JSON::XS;
2492 sub postal_mail_fsinc {
2493 my ( $self, %opt ) = @_;
2495 my $url = 'https://ws.freeside.biz/print';
2497 my $cust_main = $self->cust_main;
2498 my $agentnum = $cust_main->agentnum;
2499 my $bill_location = $cust_main->bill_location;
2501 die "Extra charges for international mailing; contact support\@freeside.biz to enable\n"
2502 if $bill_location->country ne 'US';
2504 my $conf = new FS::Conf;
2506 my @company_address = $conf->config('company_address', $agentnum);
2507 my ( $company_address1, $company_address2, $company_city, $company_state, $company_zip );
2508 if ( $company_address[2] =~ /^\s*(\S.*\S)\s*[\s,](\w\w),?\s*(\d{5}(-\d{4})?)\s*$/ ) {
2509 $company_address1 = $company_address[0];
2510 $company_address2 = $company_address[1];
2512 $company_state = $2;
2514 } elsif ( $company_address[1] =~ /^\s*(\S.*\S)\s*[\s,](\w\w),?\s*(\d{5}(-\d{4})?)\s*$/ ) {
2515 $company_address1 = $company_address[0];
2516 $company_address2 = '';
2518 $company_state = $2;
2521 die "Unparsable company_address; contact support\@freeside.biz\n";
2523 $company_city =~ s/,$//;
2525 my $file = $self->print_pdf(%opt, 'no_addresses' => 1);
2526 my $pages = CAM::PDF->new($file)->numPages;
2528 my $ua = LWP::UserAgent->new(
2530 verify_hostname => 0,
2531 SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
2532 SSL_version => 'SSLv3',
2535 my $response = $ua->request( POST $url, [
2536 'support-key' => scalar($conf->config('support-key')),
2537 'file' => encode_base64($file),
2541 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2542 'company_address1' => $company_address1,
2543 'company_address2' => $company_address2,
2544 'company_city' => $company_city,
2545 'company_state' => $company_state,
2546 'company_zip' => $company_zip,
2547 'company_country' => 'US',
2548 'company_phonenum' => scalar($conf->config('company_phonenum', $agentnum)),
2549 'company_email' => scalar($conf->config('invoice_from', $agentnum)),
2552 'name' => ( $cust_main->payname
2553 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/
2554 ? $cust_main->payname
2555 : $cust_main->contact_firstlast
2557 'company' => $cust_main->company,
2558 'address1' => $bill_location->address1,
2559 'address2' => $bill_location->address2,
2560 'city' => $bill_location->city,
2561 'state' => $bill_location->state,
2562 'zip' => $bill_location->zip,
2563 'country' => $bill_location->country,
2566 die "Print connection error: ". $response->message.
2567 ' ('. $response->as_string. ")\n"
2568 unless $response->is_success;
2571 my $content = eval { decode_json($response->content) };
2572 die "Print JSON error : $@\n" if $@;
2574 die $content->{error}."\n"
2575 if $content->{error};
2577 #TODO: store this so we can query for a status later
2578 warn "Invoice printed, ID ". $content->{id}. "\n";
2583 =item _items_sections OPTIONS
2585 Generate section information for all items appearing on this invoice.
2586 This will only be called for multi-section invoices.
2588 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
2589 related display records (L<FS::cust_bill_pkg_display>) and organize
2590 them into two groups ("early" and "late" according to whether they come
2591 before or after the total), then into sections. A subtotal is calculated
2594 Section descriptions are returned in sort weight order. Each consists
2595 of a hash containing:
2597 description: the package category name, escaped
2598 subtotal: the total charges in that section
2599 tax_section: a flag indicating that the section contains only tax charges
2600 summarized: same as tax_section, for some reason
2601 sort_weight: the package category's sort weight
2603 If 'condense' is set on the display record, it also contains everything
2604 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
2605 coderefs to generate parts of the invoice. This is not advised.
2607 The method returns two arrayrefs, one of "early" sections and one of "late"
2610 OPTIONS may include:
2612 by_location: a flag to divide the invoice into sections by location.
2613 Each section hash will have a 'location' element containing a hashref of
2614 the location fields (see L<FS::cust_location>). The section description
2615 will be the location label, but the template can use any of the location
2616 fields to create a suitable label.
2618 by_category: a flag to divide the invoice into sections using display
2619 records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
2620 behavior. Each section hash will have a 'category' element containing
2621 the section name from the display record (which probably equals the
2622 category name of the package, but may not in some cases).
2624 summary: a flag indicating that this is a summary-format invoice.
2625 Turning this on has the following effects:
2626 - Ignores display items with the 'summary' flag.
2627 - Places all sections in the "early" group even if they have post_total.
2628 - Creates sections for all non-disabled package categories, even if they
2629 have no charges on this invoice, as well as a section with no name.
2631 escape: an escape function to use for section titles.
2633 extra_sections: an arrayref of additional sections to return after the
2634 sorted list. If there are any of these, section subtotals exclude
2637 format: 'latex', 'html', or 'template' (i.e. text). Not used, but
2638 passed through to C<_condense_section()>.
2642 use vars qw(%pkg_category_cache);
2643 sub _items_sections {
2647 my $escape = $opt{escape};
2648 my @extra_sections = @{ $opt{extra_sections} || [] };
2650 # $subtotal{$locationnum}{$categoryname} = amount.
2651 # if we're not using by_location, $locationnum is undef.
2652 # if we're not using by_category, you guessed it, $categoryname is undef.
2653 # if we're not using either one, we shouldn't be here in the first place...
2655 my %late_subtotal = ();
2658 # About tax items + multisection invoices:
2659 # If either invoice_*summary option is enabled, AND there is a
2660 # package category with the name of the tax, then there will be
2661 # a display record assigning the tax item to that category.
2663 # However, the taxes are always placed in the "Taxes, Surcharges,
2664 # and Fees" section regardless of that. The only effect of the
2665 # display record is to create a subtotal for the summary page.
2668 my $pkg_hash = $self->cust_pkg_hash;
2670 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2673 my $usage = $cust_bill_pkg->usage;
2676 if ( $opt{by_location} ) {
2677 if ( $cust_bill_pkg->pkgnum ) {
2678 $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
2683 $locationnum = undef;
2686 # as in _items_cust_pkg, if a line item has no display records,
2687 # cust_bill_pkg_display() returns a default record for it
2689 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2690 next if ( $display->summary && $opt{summary} );
2692 #my $section = $display->section;
2693 #false laziness with the method, but for efficiency inside this loop
2694 my $section = $display->get('section');
2695 if ( !$section && !$cust_bill_pkg->hidden ) {
2696 $section = $cust_bill_pkg->get('categoryname'); #cust_bill->cust_bill_pkg added it (XXX quotations / quotation_section)
2699 my $type = $display->type;
2700 # Set $section = undef if we're sectioning by location and this
2701 # line item _has_ a location (i.e. isn't a fee).
2702 $section = undef if $locationnum;
2704 # set this flag if the section is not tax-only
2705 $not_tax{$locationnum}{$section} = 1
2706 if $cust_bill_pkg->pkgnum or $cust_bill_pkg->feepart;
2708 # there's actually a very important piece of logic buried in here:
2709 # incrementing $late_subtotal{$section} CREATES
2710 # $late_subtotal{$section}. keys(%late_subtotal) is later used
2711 # to define the list of late sections, and likewise keys(%subtotal).
2712 # When _items_cust_bill_pkg is called to generate line items for
2713 # real, it will be called with 'section' => $section for each
2715 if ( $display->post_total && !$opt{summary} ) {
2716 if (! $type || $type eq 'S') {
2717 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2718 if $cust_bill_pkg->setup != 0
2719 || $cust_bill_pkg->setup_show_zero;
2723 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
2724 if $cust_bill_pkg->recur != 0
2725 || $cust_bill_pkg->recur_show_zero;
2728 if ($type && $type eq 'R') {
2729 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2730 if $cust_bill_pkg->recur != 0
2731 || $cust_bill_pkg->recur_show_zero;
2734 if ($type && $type eq 'U') {
2735 $late_subtotal{$locationnum}{$section} += $usage
2736 unless scalar(@extra_sections);
2739 } else { # it's a pre-total (normal) section
2741 # skip tax items unless they're explicitly included in a section
2742 next if $cust_bill_pkg->pkgnum == 0 and
2743 ! $cust_bill_pkg->feepart and
2746 if ( $type eq 'S' ) {
2747 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2748 if $cust_bill_pkg->setup != 0
2749 || $cust_bill_pkg->setup_show_zero;
2750 } elsif ( $type eq 'R' ) {
2751 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2752 if $cust_bill_pkg->recur != 0
2753 || $cust_bill_pkg->recur_show_zero;
2754 } elsif ( $type eq 'U' ) {
2755 $subtotal{$locationnum}{$section} += $usage
2756 unless scalar(@extra_sections);
2757 } elsif ( !$type ) {
2758 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2759 + $cust_bill_pkg->recur;
2768 %pkg_category_cache = ();
2770 # summary invoices need subtotals for all non-disabled package categories,
2771 # even if they're zero
2772 # but currently assume that there are no location sections, or at least
2773 # that the summary page doesn't care about them
2774 if ( $opt{summary} ) {
2775 foreach my $category (qsearch('pkg_category', {disabled => ''})) {
2776 $subtotal{''}{$category->categoryname} ||= 0;
2778 $subtotal{''}{''} ||= 0;
2782 foreach my $post_total (0,1) {
2784 my $s = $post_total ? \%late_subtotal : \%subtotal;
2785 foreach my $locationnum (keys %$s) {
2786 foreach my $sectionname (keys %{ $s->{$locationnum} }) {
2788 'subtotal' => $s->{$locationnum}{$sectionname},
2791 if ( $locationnum ) {
2792 $section->{'locationnum'} = $locationnum;
2793 my $location = FS::cust_location->by_key($locationnum);
2794 $section->{'description'} = &{ $escape }($location->location_label);
2795 # Better ideas? This will roughly group them by proximity,
2796 # which alpha sorting on any of the address fields won't.
2797 # Sorting by locationnum is meaningless.
2798 # We have to sort on _something_ or the order may change
2799 # randomly from one invoice to the next, which will confuse
2801 $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
2803 $section->{'location'} = {
2804 label_prefix => &{ $escape }($location->label_prefix),
2805 map { $_ => &{ $escape }($location->get($_)) }
2809 $section->{'category'} = $sectionname;
2810 $section->{'description'} = &{ $escape }($sectionname);
2811 if ( _pkg_category($sectionname) ) {
2812 $section->{'sort_weight'} = _pkg_category($sectionname)->weight;
2813 if ( _pkg_category($sectionname)->condense ) {
2814 $section = { %$section, $self->_condense_section($opt{format}) };
2818 if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
2819 # then it's a tax-only section
2820 $section->{'summarized'} = 'Y';
2821 $section->{'tax_section'} = 'Y';
2823 push @these, $section;
2824 } # foreach $sectionname
2825 } #foreach $locationnum
2826 push @these, @extra_sections if $post_total == 0;
2827 # need an alpha sort for location sections, because postal codes can
2829 $sections[ $post_total ] = [ sort {
2830 $opt{'by_location'} ?
2831 ($a->{sort_weight} cmp $b->{sort_weight}) :
2832 ($a->{sort_weight} <=> $b->{sort_weight})
2834 } #foreach $post_total
2836 return @sections; # early, late
2839 #helper subs for above
2843 $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2847 my $categoryname = shift;
2848 $pkg_category_cache{$categoryname} ||=
2849 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2852 my %condensed_format = (
2853 'label' => [ qw( Description Qty Amount ) ],
2855 sub { shift->{description} },
2856 sub { shift->{quantity} },
2857 sub { my($href, %opt) = @_;
2858 ($opt{dollar} || ''). $href->{amount};
2861 'align' => [ qw( l r r ) ],
2862 'span' => [ qw( 5 1 1 ) ], # unitprices?
2863 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
2866 sub _condense_section {
2867 my ( $self, $format ) = ( shift, shift );
2869 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2870 qw( description_generator
2873 total_line_generator
2878 sub _condensed_generator_defaults {
2879 my ( $self, $format ) = ( shift, shift );
2880 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2889 sub _condensed_header_generator {
2890 my ( $self, $format ) = ( shift, shift );
2892 my ( $f, $prefix, $suffix, $separator, $column ) =
2893 _condensed_generator_defaults($format);
2895 if ($format eq 'latex') {
2896 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2897 $suffix = "\\\\\n\\hline";
2900 sub { my ($d,$a,$s,$w) = @_;
2901 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2903 } elsif ( $format eq 'html' ) {
2904 $prefix = '<th></th>';
2908 sub { my ($d,$a,$s,$w) = @_;
2909 return qq!<th align="$html_align{$a}">$d</th>!;
2917 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2919 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2922 $prefix. join($separator, @result). $suffix;
2927 sub _condensed_description_generator {
2928 my ( $self, $format ) = ( shift, shift );
2930 my ( $f, $prefix, $suffix, $separator, $column ) =
2931 _condensed_generator_defaults($format);
2933 my $money_char = '$';
2934 if ($format eq 'latex') {
2935 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2937 $separator = " & \n";
2939 sub { my ($d,$a,$s,$w) = @_;
2940 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2942 $money_char = '\\dollar';
2943 }elsif ( $format eq 'html' ) {
2944 $prefix = '"><td align="center"></td>';
2948 sub { my ($d,$a,$s,$w) = @_;
2949 return qq!<td align="$html_align{$a}">$d</td>!;
2951 #$money_char = $conf->config('money_char') || '$';
2952 $money_char = ''; # this is madness
2960 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2962 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2964 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2965 map { $f->{$_}->[$i] } qw(align span width)
2969 $prefix. join( $separator, @result ). $suffix;
2974 sub _condensed_total_generator {
2975 my ( $self, $format ) = ( shift, shift );
2977 my ( $f, $prefix, $suffix, $separator, $column ) =
2978 _condensed_generator_defaults($format);
2981 if ($format eq 'latex') {
2984 $separator = " & \n";
2986 sub { my ($d,$a,$s,$w) = @_;
2987 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2989 }elsif ( $format eq 'html' ) {
2993 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2995 sub { my ($d,$a,$s,$w) = @_;
2996 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3005 # my $r = &{$f->{fields}->[$i]}(@args);
3006 # $r .= ' Total' unless $i;
3008 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3010 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3011 map { $f->{$_}->[$i] } qw(align span width)
3015 $prefix. join( $separator, @result ). $suffix;
3020 =item total_line_generator FORMAT
3022 Returns a coderef used for generation of invoice total line items for this
3023 usage_class. FORMAT is either html or latex
3027 # should not be used: will have issues with hash element names (description vs
3028 # total_item and amount vs total_amount -- another array of functions?
3030 sub _condensed_total_line_generator {
3031 my ( $self, $format ) = ( shift, shift );
3033 my ( $f, $prefix, $suffix, $separator, $column ) =
3034 _condensed_generator_defaults($format);
3037 if ($format eq 'latex') {
3040 $separator = " & \n";
3042 sub { my ($d,$a,$s,$w) = @_;
3043 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3045 }elsif ( $format eq 'html' ) {
3049 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3051 sub { my ($d,$a,$s,$w) = @_;
3052 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3061 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3063 &{$column}( &{$f->{fields}->[$i]}(@args),
3064 map { $f->{$_}->[$i] } qw(align span width)
3068 $prefix. join( $separator, @result ). $suffix;
3073 =item _items_pkg [ OPTIONS ]
3075 Return line item hashes for each package item on this invoice. Nearly
3078 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
3080 OPTIONS are passed through to _items_cust_bill_pkg, and should include
3081 'format' and 'escape_function' at minimum.
3083 To produce items for a specific invoice section, OPTIONS should include
3084 'section', a hashref containing 'category' and/or 'locationnum' keys.
3086 'section' may also contain a key named 'condensed'. If this is present
3087 and has a true value, _items_pkg will try to merge identical items into items
3088 with 'quantity' equal to the number of items (not the sum of their separate
3089 quantities, for some reason).
3095 # The order of these is important. Bundled line items will be merged into
3096 # the most recent non-hidden item, so it needs to be the one with:
3098 # - the same start date
3099 # - no pkgpart_override
3101 # So: sort by pkgnum,
3103 # then sort the base line item before any overrides
3104 # then sort hidden before non-hidden add-ons
3105 # then sort by override pkgpart (for consistency)
3106 sort { $a->pkgnum <=> $b->pkgnum or
3107 $a->sdate <=> $b->sdate or
3108 ($a->pkgpart_override ? 0 : -1) or
3109 ($b->pkgpart_override ? 0 : 1) or
3110 $b->hidden cmp $a->hidden or
3111 $a->pkgpart_override <=> $b->pkgpart_override
3113 # and of course exclude taxes and fees
3114 grep { $_->pkgnum > 0 } $self->cust_bill_pkg;
3120 my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
3121 my $escape_function = $options{escape_function};
3123 my $locale = $self->cust_main
3124 ? $self->cust_main->locale
3125 : $self->prospect_main->locale;
3128 foreach my $cust_bill_pkg (@cust_bill_pkg) {
3129 # cache this, so we don't look it up again in every section
3130 my $part_fee = $cust_bill_pkg->get('part_fee')
3131 || $cust_bill_pkg->part_fee;
3132 $cust_bill_pkg->set('part_fee', $part_fee);
3134 #die "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; # might make more sense
3135 warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
3138 if ( exists($options{section}) and exists($options{section}{category}) )
3140 my $categoryname = $options{section}{category};
3141 # then filter for items that have that section
3142 if ( $part_fee->categoryname ne $categoryname ) {
3143 warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
3146 } # otherwise include them all in the main section
3147 # XXX what to do when sectioning by location?
3150 my %base_invnums; # invnum => invoice date
3151 foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
3152 if ($_->base_invnum) {
3153 # XXX what if base_bill has been voided?
3154 my $base_bill = FS::cust_bill->by_key($_->base_invnum);
3155 my $base_date = $self->time2str_local('short', $base_bill->_date)
3157 $base_invnums{$_->base_invnum} = $base_date || '';
3160 foreach (sort keys(%base_invnums)) {
3161 next if $_ == $self->invnum;
3162 # per convention, we must escape ext_description lines
3164 &{$escape_function}(
3165 $self->mt('from invoice #[_1] on [_2]', $_, $base_invnums{$_})
3168 my $desc = $part_fee->itemdesc_locale($locale);
3169 # but not escape the base description line
3171 my @pkg_tax = $cust_bill_pkg->_pkg_tax_list
3172 if $options{section_with_taxes};
3175 { feepart => $cust_bill_pkg->feepart,
3176 amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
3177 description => $desc,
3178 pkg_tax => \@pkg_tax,
3179 ext_description => \@ext_desc,
3190 warn "$me _items_pkg searching for all package line items\n"
3193 my @cust_bill_pkg = $self->_items_nontax;
3195 warn "$me _items_pkg filtering line items\n"
3197 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3199 if ($options{section} && $options{section}->{condensed}) {
3201 warn "$me _items_pkg condensing section\n"
3205 local $Storable::canonical = 1;
3206 foreach ( @items ) {
3208 delete $item->{ref};
3209 delete $item->{ext_description};
3210 my $key = freeze($item);
3211 $itemshash{$key} ||= 0;
3212 $itemshash{$key} ++; # += $item->{quantity};
3214 @items = sort { $a->{description} cmp $b->{description} }
3215 map { my $i = thaw($_);
3216 $i->{quantity} = $itemshash{$_};
3218 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3224 warn "$me _items_pkg returning ". scalar(@items). " items\n"
3231 return 0 unless $a->itemdesc cmp $b->itemdesc;
3232 return -1 if $b->itemdesc eq 'Tax';
3233 return 1 if $a->itemdesc eq 'Tax';
3234 return -1 if $b->itemdesc eq 'Other surcharges';
3235 return 1 if $a->itemdesc eq 'Other surcharges';
3236 $a->itemdesc cmp $b->itemdesc;
3241 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart }
3242 $self->cust_bill_pkg;
3243 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3245 if ( $self->conf->exists('always_show_tax') ) {
3246 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
3247 if (0 == grep { $_->{description} eq $itemdesc } @items) {
3249 { 'description' => $itemdesc,
3256 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
3258 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
3259 list of hashrefs describing the line items they generate on the invoice.
3261 OPTIONS may include:
3263 format: the invoice format.
3265 escape_function: the function used to escape strings.
3267 DEPRECATED? (expensive, mostly unused?)
3268 format_function: the function used to format CDRs.
3270 section: a hashref containing 'category' and/or 'locationnum'; if this
3271 is present, only returns line items that belong to that category and/or
3272 location (whichever is defined).
3274 multisection: a flag indicating that this is a multisection invoice,
3275 which does something complicated.
3277 preref_callback: coderef run for each line item, code should return HTML to be
3278 displayed before that line item (quotations only)
3280 section_with_taxes: Look up and include applied taxes for each record
3282 Returns a list of hashrefs, each of which may contain:
3284 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
3285 ext_description, which is an arrayref of detail lines to show below
3290 sub _items_cust_bill_pkg {
3292 my $conf = $self->conf;
3293 my $cust_bill_pkgs = shift;
3296 my $format = $opt{format} || '';
3297 my $escape_function = $opt{escape_function} || sub { shift };
3298 my $format_function = $opt{format_function} || '';
3299 my $no_usage = $opt{no_usage} || '';
3300 my $unsquelched = $opt{unsquelched} || ''; #unused
3301 my ($section, $locationnum, $category);
3302 if ( $opt{section} ) {
3303 $category = $opt{section}->{category};
3304 $locationnum = $opt{section}->{locationnum};
3306 my $summary_page = $opt{summary_page} || ''; #unused
3307 my $multisection = defined($category) || defined($locationnum);
3308 # this variable is the value of the config setting, not whether it applies
3309 # to this particular line item.
3310 my $discount_show_always = $conf->exists('discount-show-always');
3312 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
3314 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
3316 my $agentnum = $self->agentnum;
3318 # for location labels: use default location on the invoice date
3319 my $default_locationnum;
3320 if ( $conf->exists('invoice-all_pkg_addresses') ) {
3321 $default_locationnum = 0; # treat them all as non-default
3322 } elsif ( $self->custnum ) {
3324 my @h_search = FS::h_cust_main->sql_h_search($self->_date);
3325 $h_cust_main = qsearchs({
3326 'table' => 'h_cust_main',
3327 'hashref' => { custnum => $self->custnum },
3328 'extra_sql' => $h_search[1],
3329 'addl_from' => $h_search[3],
3331 $default_locationnum = $h_cust_main->ship_locationnum;
3332 } elsif ( $self->prospectnum ) {
3333 my $cust_location = qsearchs('cust_location',
3334 { prospectnum => $self->prospectnum,
3336 $default_locationnum = $cust_location->locationnum if $cust_location;
3339 my @b = (); # accumulator for the line item hashes that we'll return
3340 my ($s, $r, $u, $d) = ( undef, undef, undef, undef );
3341 # the 'current' line item hashes for setup, recur, usage, discount
3342 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
3344 # if the current line item is waiting to go out, and the one we're about
3345 # to start is not bundled, then push out the current one and start a new
3347 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3348 if ( $_ && !$cust_bill_pkg->hidden ) {
3349 $_->{amount} = sprintf( "%.2f", $_->{amount} );
3350 $_->{amount} =~ s/^\-0\.00$/0.00/;
3351 if (exists($_->{unit_amount})) {
3352 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3355 # we already decided to create this display line; don't reconsider it
3357 # if $_->{amount} != 0
3358 # || $discount_show_always
3359 # || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3360 # || ( $_->{_is_setup} && $_->{setup_show_zero} )
3366 if ( $locationnum ) {
3367 # this is a location section; skip packages that aren't at this
3369 next if $cust_bill_pkg->pkgnum == 0; # skips fees...
3370 next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
3374 # Consider display records for this item to determine if it belongs
3375 # in this section. Note that if there are no display records, there
3376 # will be a default pseudo-record that includes all charge types
3377 # and has no section name.
3378 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
3379 ? $cust_bill_pkg->cust_bill_pkg_display
3380 : ( $cust_bill_pkg );
3382 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
3383 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
3386 if ( defined($category) ) {
3387 # then this is a package category section; process all display records
3388 # that belong to this section.
3389 @cust_bill_pkg_display = grep { $_->section eq $category }
3390 @cust_bill_pkg_display;
3392 # otherwise, process all display records that aren't usage summaries
3393 # (I don't think there should be usage summaries if you aren't using
3394 # category sections, but this is the historical behavior)
3395 @cust_bill_pkg_display = grep { !$_->summary }
3396 @cust_bill_pkg_display;
3399 my $classname = ''; # package class name, will fill in later
3401 foreach my $display (@cust_bill_pkg_display) {
3403 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
3404 $display->billpkgdisplaynum. "\n"
3407 my $type = $display->type;
3409 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
3410 $desc = substr($desc, 0, $maxlength). '...'
3411 if $format eq 'latex' && length($desc) > $maxlength;
3413 my %details_opt = ( 'format' => $format,
3414 'escape_function' => $escape_function,
3415 'format_function' => $format_function,
3416 'no_usage' => $opt{'no_usage'},
3419 my @pkg_tax = $cust_bill_pkg->_pkg_tax_list
3420 if $opt{section_with_taxes};
3422 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
3423 # XXX this should be pulled out into quotation_pkg
3425 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
3427 # quotation_pkgs are never fees, so don't worry about the case where
3428 # part_pkg is undefined
3430 my @details = $cust_bill_pkg->details;
3432 # and I guess they're never bundled either?
3433 if (( $cust_bill_pkg->setup != 0 ) || ( $cust_bill_pkg->setup_show_zero )) {
3434 my $description = $desc;
3435 $description .= ' Setup'
3436 if $cust_bill_pkg->recur != 0
3437 || $discount_show_always
3438 || $cust_bill_pkg->recur_show_zero;
3440 # keep it consistent, please
3442 'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref
3443 'description' => $description,
3444 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
3445 'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup),
3446 'quantity' => $cust_bill_pkg->quantity,
3447 'pkg_tax' => \@pkg_tax,
3448 'ext_description' => \@details,
3449 'preref_html' => ( $opt{preref_callback}
3450 ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3455 if (( $cust_bill_pkg->recur != 0 ) || ( $cust_bill_pkg->recur_show_zero )) {
3458 'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref
3459 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
3460 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
3461 'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur),
3462 'quantity' => $cust_bill_pkg->quantity,
3463 'pkg_tax' => \@pkg_tax,
3464 'ext_description' => \@details,
3465 'preref_html' => ( $opt{preref_callback}
3466 ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3472 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
3473 # a "normal" package line item (not a quotation, not a fee, not a tax)
3475 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
3478 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3480 unless ( $cust_pkg ) {
3481 # There is no related row in cust_pkg for this cust_bill_pkg.pkgnum.
3482 # This invoice may have been broken by an unusual combination
3483 # of manually editing package dates, and aborted package changes
3484 # when the manually edited dates used are nonsensical.
3487 'cust_bill_pkg(billpkgnum:%s) '.
3488 'is missing related row in cust_pkg(pkgnum:%s)! '.
3489 'cust_bill(invnum:%s) is corrupted by bad database data, '.
3490 'and should be investigated',
3491 $cust_bill_pkg->billpkgnum,
3492 $cust_bill_pkg->pkgnum,
3493 $cust_bill_pkg->invnum;
3495 FS::Log->new('FS::cust_bill_pkg')->critical( $error );
3500 my $part_pkg = $cust_pkg->part_pkg;
3502 # which pkgpart to show for display purposes?
3503 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
3505 # start/end dates for invoice formats that do nonstandard
3507 my %item_dates = ();
3508 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
3509 unless $part_pkg->option('disable_line_item_date_ranges',1);
3511 # not normally used, but pass this to the template anyway
3512 $classname = $part_pkg->classname;
3514 if ( (!$type || $type eq 'S')
3515 && ( $cust_bill_pkg->setup != 0
3516 || $cust_bill_pkg->setup_show_zero
3517 || ($discount_show_always and $cust_bill_pkg->unitsetup > 0)
3522 warn "$me _items_cust_bill_pkg adding setup\n"
3525 # append the word 'Setup' to the setup line if there's going to be
3526 # a recur line for the same package (i.e. not a one-time charge)
3528 my $description = $desc;
3529 $description .= ' Setup'
3530 if $cust_bill_pkg->recur != 0
3531 || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
3532 || $cust_bill_pkg->recur_show_zero;
3534 my $disable_date_ranges =
3535 $opt{disable_line_item_date_ranges}
3536 || $part_pkg->option('disable_line_item_date_ranges', 1);
3538 $description .= $cust_bill_pkg->time_period_pretty(
3541 disable_date_ranges => $disable_date_ranges,
3543 if $part_pkg->is_prepaid #for prepaid, "display the validity period
3544 # triggered by the recurring charge freq
3546 && $cust_bill_pkg->recur == 0
3547 && ! $cust_bill_pkg->recur_show_zero;
3552 # always pass the svc_label through to the template, even if
3553 # not displaying it as an ext_description
3554 my @svc_labels = map &{$escape_function}($_),
3555 $cust_pkg->h_labels_short($self->_date,
3558 $self->conf->{locale},
3560 $svc_label = $svc_labels[0];
3562 unless ( $cust_pkg->part_pkg->hide_svc_detail
3563 || $cust_bill_pkg->hidden )
3566 push @d, @svc_labels
3567 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3568 # show the location label if it's not the customer's default
3569 # location, and we're not grouping items by location already
3570 if ( $cust_pkg->locationnum != $default_locationnum
3571 and !defined($locationnum) ) {
3572 my $loc = $cust_pkg->location_label;
3573 $loc = substr($loc, 0, $maxlength). '...'
3574 if $format eq 'latex' && length($loc) > $maxlength;
3575 push @d, &{$escape_function}($loc);
3578 } #unless hiding service details
3580 push @d, $cust_bill_pkg->details(%details_opt)
3581 if $cust_bill_pkg->recur == 0;
3583 if ( $cust_bill_pkg->hidden ) {
3584 $s->{amount} += $cust_bill_pkg->setup;
3585 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3586 push @{ $s->{ext_description} }, @d;
3590 description => $description,
3591 pkgpart => $pkgpart,
3592 pkgnum => $cust_bill_pkg->pkgnum,
3593 amount => $cust_bill_pkg->setup,
3594 setup_show_zero => $cust_bill_pkg->setup_show_zero,
3595 unit_amount => $cust_bill_pkg->unitsetup,
3596 quantity => $cust_bill_pkg->quantity,
3597 pkg_tax => \@pkg_tax,
3598 ext_description => \@d,
3599 svc_label => ($svc_label || ''),
3600 locationnum => $cust_pkg->locationnum, # sure, why not?
3606 # should we show a recur line?
3607 # if type eq 'S', then NO, because we've been told not to.
3608 # otherwise, show the recur line if:
3609 # - there's a recurring charge
3610 # - or recur_show_zero is on
3611 # - or there's a positive unitrecur (so it's been discounted to zero)
3612 # and discount-show-always is on
3613 if ( ( !$type || $type eq 'R' || $type eq 'U' )
3615 $cust_bill_pkg->recur != 0
3617 || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
3618 || $cust_bill_pkg->recur_show_zero
3623 warn "$me _items_cust_bill_pkg adding recur/usage\n"
3626 my $is_summary = $display->summary;
3627 my $description = $desc;
3628 if ( $type eq 'U' and defined($r) ) {
3629 # don't just show the same description as the recur line
3630 $description = $self->mt('Usage charges');
3633 my $disable_date_ranges =
3634 $opt{disable_line_item_date_ranges}
3635 || $part_pkg->option('disable_line_item_date_ranges', 1);
3637 $description .= $cust_bill_pkg->time_period_pretty(
3640 disable_date_ranges => $disable_date_ranges,
3644 my @seconds = (); # for display of usage info
3647 #at least until cust_bill_pkg has "past" ranges in addition to
3648 #the "future" sdate/edate ones... see #3032
3649 my @dates = ( $self->_date );
3650 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3651 push @dates, $prev->sdate if $prev;
3652 push @dates, undef if !$prev;
3654 my @svc_labels = map &{$escape_function}($_),
3655 $cust_pkg->h_labels_short(@dates,
3657 $self->conf->{locale});
3658 $svc_label = $svc_labels[0];
3660 # show service labels, unless...
3661 # the package is set not to display them
3662 unless ( $part_pkg->hide_svc_detail
3663 # or this is a tax-like line item
3664 || $cust_bill_pkg->itemdesc
3665 # or this is a hidden (bundled) line item
3666 || $cust_bill_pkg->hidden
3667 # or this is a usage summary line
3668 || $is_summary && $type && $type eq 'U'
3669 # or this is a usage line and there's a recurring line
3670 # for the package in the same section (which will
3671 # have service labels already)
3672 || ($type eq 'U' and defined($r))
3676 warn "$me _items_cust_bill_pkg adding service details\n"
3679 push @d, @svc_labels
3680 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3681 warn "$me _items_cust_bill_pkg done adding service details\n"
3684 # show the location label if it's not the customer's default
3685 # location, and we're not grouping items by location already
3686 if ( $cust_pkg->locationnum != $default_locationnum
3687 and !defined($locationnum) ) {
3688 my $loc = $cust_pkg->location_label;
3689 $loc = substr($loc, 0, $maxlength). '...'
3690 if $format eq 'latex' && length($loc) > $maxlength;
3691 push @d, &{$escape_function}($loc);
3694 # Display of seconds_since_sqlradacct:
3695 # On the invoice, when processing @detail_items, look for a field
3696 # named 'seconds'. This will contain total seconds for each
3697 # service, in the same order as @ext_description. For services
3698 # that don't support this it will show undef.
3699 if ( $conf->exists('svc_acct-usage_seconds')
3700 and ! $cust_bill_pkg->pkgpart_override ) {
3701 foreach my $cust_svc (
3702 $cust_pkg->h_cust_svc(@dates, 'I')
3705 # eval because not having any part_export_usage exports
3706 # is a fatal error, last_bill/_date because that's how
3707 # sqlradius_hour billing does it
3709 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
3711 push @seconds, $sec;
3713 } #if svc_acct-usage_seconds
3715 } # if we are showing service labels
3717 unless ( $is_summary ) {
3718 warn "$me _items_cust_bill_pkg adding details\n"
3721 #instead of omitting details entirely in this case (unwanted side
3722 # effects), just omit CDRs
3723 $details_opt{'no_usage'} = 1
3724 if $type && $type eq 'R';
3726 push @d, $cust_bill_pkg->details(%details_opt);
3729 warn "$me _items_cust_bill_pkg calculating amount\n"
3734 $amount = $cust_bill_pkg->recur;
3735 } elsif ($type eq 'R') {
3736 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3737 } elsif ($type eq 'U') {
3738 $amount = $cust_bill_pkg->usage;
3741 if ( !$type || $type eq 'R' ) {
3743 warn "$me _items_cust_bill_pkg adding recur\n"
3747 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
3750 if ( $cust_bill_pkg->hidden ) {
3751 $r->{amount} += $amount;
3752 $r->{unit_amount} += $unit_amount;
3753 push @{ $r->{ext_description} }, @d;
3756 description => $description,
3757 pkgpart => $pkgpart,
3758 pkgnum => $cust_bill_pkg->pkgnum,
3760 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3761 unit_amount => $unit_amount,
3762 quantity => $cust_bill_pkg->quantity,
3763 pkg_tax => \@pkg_tax,
3765 ext_description => \@d,
3766 svc_label => ($svc_label || ''),
3767 locationnum => $cust_pkg->locationnum,
3769 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
3772 } else { # $type eq 'U'
3774 warn "$me _items_cust_bill_pkg adding usage\n"
3777 if ( $cust_bill_pkg->hidden and defined($u) ) {
3778 # if this is a hidden package and there's already a usage
3779 # line for the bundle, add this package's total amount and
3780 # usage details to it
3781 $u->{amount} += $amount;
3782 push @{ $u->{ext_description} }, @d;
3783 } elsif ( $amount ) {
3784 # create a new usage line
3786 description => $description,
3787 pkgpart => $pkgpart,
3788 pkgnum => $cust_bill_pkg->pkgnum,
3791 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3792 pkg_tax => \@pkg_tax,
3794 ext_description => \@d,
3795 locationnum => $cust_pkg->locationnum,
3797 } # else this has no usage, so don't create a usage section
3800 } # recurring or usage with recurring charge
3802 } else { # taxes and fees
3804 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
3807 # items of this kind should normally not have sdate/edate.
3809 'description' => $desc,
3810 'amount' => sprintf('%.2f', $cust_bill_pkg->setup
3811 + $cust_bill_pkg->recur)
3814 } # if quotation / package line item / other line item
3816 # decide whether to show active discounts here
3818 # case 1: we are showing a single line for the package
3820 # case 2: we are showing a setup line for a package that has
3821 # no base recurring fee
3822 or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
3823 # case 3: we are showing a recur line for a package that has
3824 # a base recurring fee
3825 or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
3828 my $item_discount = $cust_bill_pkg->_item_discount;
3829 if ( $item_discount ) {
3830 # $item_discount->{amount} is negative
3832 if ( $d and $cust_bill_pkg->hidden ) {
3833 $d->{amount} += $item_discount->{amount};
3835 $d = $item_discount;
3836 $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
3839 # update the active line (before the discount) to show the
3840 # original price (whether this is a hidden line or not)
3842 # quotation discounts keep track of setup and recur; invoice
3843 # discounts currently don't
3844 if ( exists $item_discount->{setup_amount} ) {
3846 $s->{amount} -= $item_discount->{setup_amount} if $s;
3847 $r->{amount} -= $item_discount->{recur_amount} if $r;
3851 # $active_line is the line item hashref for the line that will
3852 # show the original price
3853 # (use the recur or single line for the package, unless we're
3854 # showing a setup line for a package with no recurring fee)
3855 my $active_line = $r;
3856 if ( $type eq 'S' ) {
3859 $active_line->{amount} -= $item_discount->{amount};
3863 } # if there are any discounts
3864 } # if this is an appropriate place to show discounts
3866 } # foreach $display
3870 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3872 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3873 if exists($_->{amount});
3874 $_->{amount} =~ s/^\-0\.00$/0.00/;
3875 if (exists($_->{unit_amount})) {
3876 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3880 #if $_->{amount} != 0
3881 # || $discount_show_always
3882 # || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3883 # || ( $_->{_is_setup} && $_->{setup_show_zero} )
3887 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
3894 =item _items_discounts_avail
3896 Returns an array of line item hashrefs representing available term discounts
3897 for this invoice. This makes the same assumptions that apply to term
3898 discounts in general: that the package is billed monthly, at a flat rate,
3899 with no usage charges. A prorated first month will be handled, as will
3900 a setup fee if the discount is allowed to apply to setup fees.
3904 sub _items_discounts_avail {
3907 #maybe move this method from cust_bill when quotations support discount_plans
3908 return () unless $self->can('discount_plans');
3909 my %plans = $self->discount_plans;
3911 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
3912 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
3916 my $plan = $plans{$months};
3918 my $term_total = sprintf('%.2f', $plan->discounted_total);
3919 my $percent = sprintf('%.0f',
3920 100 * (1 - $term_total / $plan->base_total) );
3921 my $permonth = sprintf('%.2f', $term_total / $months);
3922 my $detail = $self->mt('discount on item'). ' '.
3923 join(', ', map { "#$_" } $plan->pkgnums)
3926 # discounts for non-integer months don't work anyway
3927 $months = sprintf("%d", $months);
3930 description => $self->mt('Save [_1]% by paying for [_2] months',
3932 amount => $self->mt('[_1] ([_2] per month)',
3933 $term_total, $money_char.$permonth),
3934 ext_description => ($detail || ''),
3937 sort { $b <=> $a } keys %plans;
3941 =item has_sections AGENTNUM
3943 Return true if invoice_sections should be enabled for this bill.
3944 (Inherited by both cust_bill and cust_bill_void)
3947 * False if not an invoice
3948 * True always if conf invoice_sections is enabled
3949 * True always if sections_by_location is enabled
3950 * True if conf invoice_sections_multilocation > 1,
3951 and location_count >= invoice_sections_multilocation
3957 my ($self, $agentnum) = @_;
3959 return 0 unless $self->invnum > 0;
3961 $agentnum ||= $self->agentnum;
3962 return 1 if $self->conf->config_bool('invoice_sections', $agentnum);
3963 return 1 if $self->conf->exists('sections_by_location', $agentnum);
3965 my $location_min = $self->conf->config(
3966 'invoice_sections_multilocation', $agentnum,
3971 && $self->location_count >= $location_min;
3977 =item location_count
3979 Return the number of locations billed on this invoice
3983 sub location_count {
3985 return 0 unless $self->invnum;
3987 # SELECT COUNT( DISTINCT cust_pkg.locationnum )
3988 # FROM cust_bill_pkg
3989 # LEFT JOIN cust_pkg USING (pkgnum)
3990 # WHERE invnum = 278
3991 # AND cust_bill_pkg.pkgnum > 0
3993 my $result = qsearchs({
3994 select => 'COUNT(DISTINCT cust_pkg.locationnum) as location_count',
3995 table => 'cust_bill_pkg',
3996 addl_from => 'LEFT JOIN cust_pkg USING (pkgnum)',
3997 extra_sql => 'WHERE invnum = '.dbh->quote( $self->invnum )
3998 . ' AND cust_bill_pkg.pkgnum > 0'
4000 ref $result ? $result->location_count : 0;