From: mark Date: Fri, 16 Sep 2011 00:15:48 +0000 (+0000) Subject: invoice template and config localization, #12367 X-Git-Tag: freeside_2_3_1~288 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=9c866ccad0f187f29d21f12b93f15f2787aa9843 invoice template and config localization, #12367 --- diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 79b7d8c93..7dcbf044c 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -47,16 +47,25 @@ but this may change in the future. =over 4 -=item new +=item new [ HASHREF ] Create a new configuration object. +HASHREF may contain options to set the configuration context. Currently +accepts C, and C to disable fallback to the null locale. + =cut sub new { - my($proto) = @_; + my($proto) = shift; + my $opts = shift || {}; my($class) = ref($proto) || $proto; - my($self) = { 'base_dir' => $base_dir }; + my $self = { + 'base_dir' => $base_dir, + 'locale' => $opts->{locale}, + 'localeonly' => $opts->{localeonly}, # for config-view.cgi ONLY + }; + warn "FS::Conf created with no locale fallback.\n" if $self->{localeonly}; bless ($self, $class); } @@ -108,14 +117,26 @@ sub _usecompat { sub _config { my($self,$name,$agentnum,$agentonly)=@_; my $hashref = { 'name' => $name }; - $hashref->{agentnum} = $agentnum; local $FS::Record::conf = undef; # XXX evil hack prevents recursion - my $cv = FS::Record::qsearchs('conf', $hashref); - if (!$agentonly && !$cv && defined($agentnum) && $agentnum) { - $hashref->{agentnum} = ''; - $cv = FS::Record::qsearchs('conf', $hashref); + my $cv; + my @a = ( + ($agentnum || ()), + ($agentonly && $agentnum ? () : '') + ); + my @l = ( + ($self->{locale} || ()), + ($self->{localeonly} && $self->{locale} ? () : '') + ); + # try with the agentnum first, then fall back to no agentnum if allowed + foreach my $a (@a) { + $hashref->{agentnum} = $a; + foreach my $l (@l) { + $hashref->{locale} = $l; + $cv = FS::Record::qsearchs('conf', $hashref); + return $cv if $cv; + } } - return $cv; + return undef; } sub config { @@ -268,10 +289,14 @@ sub set { warn "[FS::Conf] SET $name\n" if $DEBUG; - my $old = FS::Record::qsearchs('conf', {name => $name, agentnum => $agentnum}); - my $new = new FS::conf { $old ? $old->hash - : ('name' => $name, 'agentnum' => $agentnum) - }; + my $hashref = { + name => $name, + agentnum => $agentnum, + locale => $self->{locale} + }; + + my $old = FS::Record::qsearchs('conf', $hashref); + my $new = new FS::conf { $old ? $old->hash : %$hashref }; $new->value($value); my $error; @@ -312,7 +337,7 @@ sub delete { return $self->_usecompat('delete', @_) if use_confcompat; my($name, $agentnum) = @_; - if ( my $cv = FS::Record::qsearchs('conf', {name => $name, agentnum => $agentnum}) ) { + if ( my $cv = FS::Record::qsearchs('conf', {name => $name, agentnum => $agentnum, locale => $self->{locale}}) ) { warn "[FS::Conf] DELETE $name\n" if $DEBUG; my $oldAutoCommit = $FS::UID::AutoCommit; @@ -1035,6 +1060,7 @@ my %payment_gateway_options = ( 'description' => 'Subject: header on email invoices. Defaults to "Invoice". The following substitutions are available: $name, $name_short, $invoice_number, and $invoice_date.', 'type' => 'text', 'per_agent' => 1, + 'per_locale' => 1, }, { @@ -1065,6 +1091,7 @@ my %payment_gateway_options = ( 'description' => 'Notes section for HTML invoices. Defaults to the same data in invoice_latexnotes if not specified.', 'type' => 'textarea', 'per_agent' => 1, + 'per_locale' => 1, }, { @@ -1073,6 +1100,7 @@ my %payment_gateway_options = ( 'description' => 'Footer for HTML invoices. Defaults to the same data in invoice_latexfooter if not specified.', 'type' => 'textarea', 'per_agent' => 1, + 'per_locale' => 1, }, { @@ -1081,6 +1109,7 @@ my %payment_gateway_options = ( 'description' => 'Summary initial page for HTML invoices.', 'type' => 'textarea', 'per_agent' => 1, + 'per_locale' => 1, }, { @@ -1088,6 +1117,7 @@ my %payment_gateway_options = ( 'section' => 'invoicing', 'description' => 'Return address for HTML invoices. Defaults to the same data in invoice_latexreturnaddress if not specified.', 'type' => 'textarea', + 'per_locale' => 1, }, { @@ -1152,6 +1182,7 @@ and customer address. Include units.', 'description' => 'Notes section for LaTeX typeset PostScript invoices.', 'type' => 'textarea', 'per_agent' => 1, + 'per_locale' => 1, }, { @@ -1160,6 +1191,7 @@ and customer address. Include units.', 'description' => 'Footer for LaTeX typeset PostScript invoices.', 'type' => 'textarea', 'per_agent' => 1, + 'per_locale' => 1, }, { @@ -1168,6 +1200,7 @@ and customer address. Include units.', 'description' => 'Summary initial page for LaTeX typeset PostScript invoices.', 'type' => 'textarea', 'per_agent' => 1, + 'per_locale' => 1, }, { @@ -1176,6 +1209,7 @@ and customer address. Include units.', 'description' => 'Remittance coupon for LaTeX typeset PostScript invoices.', 'type' => 'textarea', 'per_agent' => 1, + 'per_locale' => 1, }, { @@ -1254,6 +1288,7 @@ and customer address. Include units.', 'description' => 'Optional small footer for multi-page LaTeX typeset PostScript invoices.', 'type' => 'textarea', 'per_agent' => 1, + 'per_locale' => 1, }, { @@ -1832,7 +1867,12 @@ and customer address. Include units.', 'section' => 'UI', 'description' => 'Default locale', 'type' => 'select', - 'select_enum' => [ FS::Locales->locales ], + 'options_sub' => sub { + map { $_ => FS::Locales->description($_) } FS::Locales->locales; + }, + 'option_sub' => sub { + FS::Locales->description(shift) + }, }, { @@ -3361,6 +3401,7 @@ and customer address. Include units.', 'type' => 'image', 'per_agent' => 1, #XXX just view/logo.cgi, which is for the global #old-style editor anyway...? + 'per_locale' => 1, }, { @@ -3369,6 +3410,7 @@ and customer address. Include units.', 'description' => 'Company logo for printed and PDF invoices, in EPS format.', 'type' => 'image', 'per_agent' => 1, #XXX as above, kinda + 'per_locale' => 1, }, { @@ -4554,6 +4596,20 @@ and customer address. Include units.', }, { + 'key' => 'available-locales', + 'section' => '', + 'description' => 'Limit available locales (employee preferences, per-customer locale selection, etc.) to a particular set.', + 'type' => 'select-sub', + 'multiple' => 1, + 'options_sub' => sub { + map { $_ => FS::Locales->description($_) } + grep { $_ ne 'en_US' } + FS::Locales->locales; + }, + 'option_sub' => sub { FS::Locales->description(shift) }, + }, + + { 'key' => 'translate-auto-insert', 'section' => '', 'description' => 'Auto-insert untranslated strings for selected non-en_US locales with their default/en_US values. DO NOT TURN THIS ON.', diff --git a/FS/FS/L10N/en_ca.pm b/FS/FS/L10N/en_ca.pm new file mode 100644 index 000000000..1b71f7b9a --- /dev/null +++ b/FS/FS/L10N/en_ca.pm @@ -0,0 +1,4 @@ +package FS::L10N::en_ca; +use base qw(FS::L10N::en_us); + +1; diff --git a/FS/FS/L10N/en_us.pm b/FS/FS/L10N/en_us.pm index e8a592d78..6ad136be0 100644 --- a/FS/FS/L10N/en_us.pm +++ b/FS/FS/L10N/en_us.pm @@ -1,6 +1,5 @@ package FS::L10N::en_us; use base qw(FS::L10N); -#use strict; our %Lexicon = ( _AUTO=>1 ); diff --git a/FS/FS/L10N/fr_ca.pm b/FS/FS/L10N/fr_ca.pm new file mode 100644 index 000000000..22ede1b33 --- /dev/null +++ b/FS/FS/L10N/fr_ca.pm @@ -0,0 +1,4 @@ +package FS::L10N::fr_ca; +use base qw(FS::L10N::fr_fr); + +1; diff --git a/FS/FS/L10N/fr_fr.pm b/FS/FS/L10N/fr_fr.pm new file mode 100644 index 000000000..537241033 --- /dev/null +++ b/FS/FS/L10N/fr_fr.pm @@ -0,0 +1,6 @@ +package FS::L10N::fr_fr; +use base qw(FS::L10N::DBI); + +our %Lexicon = ( _AUTO => 1 ); + +1; diff --git a/FS/FS/Locales.pm b/FS/FS/Locales.pm index 607f2be2d..351f47875 100644 --- a/FS/FS/Locales.pm +++ b/FS/FS/Locales.pm @@ -28,8 +28,11 @@ Returns a list of the available locales. =cut tie our %locales, 'Tie::IxHash', - 'en_US', { name => 'English', country => 'United States', }, - 'iw_IL', { name => 'Hebrew', country => 'Israel', rtl=>1, }, + 'en_CA', { name => 'English', country => 'Canada', }, + 'en_US', { name => 'English', country => 'United States', }, + 'fr_CA', { name => 'French', country => 'Canada', }, + 'fr_FR', { name => 'French', country => 'France', }, + 'iw_IL', { name => 'Hebrew', country => 'Israel', rtl=>1, }, ; sub locales { @@ -47,6 +50,17 @@ sub locale_info { %{ $locales{$locale} }; } +=item description LOCALE + +Returns "Language (Country)" for a locale. + +=cut + +sub description { + my($class, $locale) = @_; + $locales{$locale}->{'name'} . ' (' . $locales{$locale}->{'country'} . ')'; +} + =back =head1 BUGS diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 67ce21b18..b36205c12 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -884,6 +884,7 @@ sub tables_hashref { 'accountcode_cdr', 'char', 'NULL', 1, '', '', 'billday', 'int', 'NULL', '', '', '', 'edit_subject', 'char', 'NULL', 1, '', '', + 'locale', 'varchar', 'NULL', 16, '', '', ], 'primary_key' => 'custnum', 'unique' => [ [ 'agentnum', 'agent_custid' ] ], @@ -3265,13 +3266,14 @@ sub tables_hashref { 'conf' => { 'columns' => [ - 'confnum', 'serial', '', '', '', '', - 'agentnum', 'int', 'NULL', '', '', '', - 'name', 'varchar', '', $char_d, '', '', - 'value', 'text', 'NULL', '', '', '', + 'confnum', 'serial', '', '', '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'locale', 'varchar','NULL', 16, '', '', + 'name', 'varchar', '', $char_d, '', '', + 'value', 'text', 'NULL', '', '', '', ], 'primary_key' => 'confnum', - 'unique' => [ [ 'agentnum', 'name' ]], + 'unique' => [ [ 'agentnum', 'locale', 'name' ] ], 'index' => [], }, diff --git a/FS/FS/conf.pm b/FS/FS/conf.pm index 3faab1470..b467cec70 100644 --- a/FS/FS/conf.pm +++ b/FS/FS/conf.pm @@ -3,6 +3,7 @@ package FS::conf; use strict; use vars qw( @ISA ); use FS::Record; +use FS::Locales; @ISA = qw(FS::Record); @@ -94,6 +95,7 @@ sub check { || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum') || $self->ut_text('name') || $self->ut_anything('value') + || $self->ut_enum('locale', [ '', FS::Locales->locales ]) ; return $error if $error; diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index d8d310a99..6a604e01c 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1,8 +1,9 @@ package FS::cust_bill; use strict; -use vars qw( @ISA $DEBUG $me $conf +use vars qw( @ISA $DEBUG $me $money_char $date_format $rdate_format $date_format_long ); + # but NOT $conf use vars qw( $invoice_lines @buf ); #yuck use Fcntl qw(:flock); #for spool_csv use Cwd; @@ -41,6 +42,7 @@ use FS::bill_batch; use FS::cust_bill_batch; use FS::cust_bill_pay_pkg; use FS::cust_credit_bill_pkg; +use FS::L10N; @ISA = qw( FS::cust_main_Mixin FS::Record ); @@ -49,7 +51,7 @@ $me = '[FS::cust_bill]'; #ask FS::UID to run this stuff for us later FS::UID->install_callback( sub { - $conf = new FS::Conf; + my $conf = new FS::Conf; #global $money_char = $conf->config('money_char') || '$'; $date_format = $conf->config('date_format') || '%x'; #/YY $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY @@ -364,6 +366,7 @@ cust_bill-default_agent_invid is set and it has a value, invnum otherwise. sub display_invnum { my $self = shift; + my $conf = $self->conf; if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){ return $self->agent_invid; } else { @@ -807,6 +810,7 @@ If there is an error, returns the error, otherwise returns false. sub apply_payments_and_credits { my( $self, %options ) = @_; + my $conf = $self->conf; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -955,6 +959,7 @@ sub generate_email { my $self = shift; my %args = @_; + my $conf = $self->conf; my $me = '[FS::cust_bill::generate_email]'; @@ -989,7 +994,7 @@ sub generate_email { my $alternative = build MIME::Entity 'Type' => 'multipart/alternative', - 'Encoding' => '7bit', + #'Encoding' => '7bit', 'Disposition' => 'inline' ; @@ -1017,8 +1022,8 @@ sub generate_email { $alternative->attach( 'Type' => 'text/plain', - #'Encoding' => 'quoted-printable', - 'Encoding' => '7bit', + 'Encoding' => 'quoted-printable', + #'Encoding' => '7bit', 'Data' => $data, 'Disposition' => 'inline', ); @@ -1240,6 +1245,7 @@ sub queueable_send { sub send { my $self = shift; + my $conf = $self->conf; my( $template, $invoice_from, $notice_name ); my $agentnums = ''; @@ -1329,6 +1335,7 @@ sub queueable_email { #sub email_invoice { sub email { my $self = shift; + my $conf = $self->conf; my( $template, $invoice_from, $notice_name, $no_coupon ); if ( ref($_[0]) ) { @@ -1378,6 +1385,7 @@ sub email { sub email_subject { my $self = shift; + my $conf = $self->conf; #my $template = scalar(@_) ? shift : ''; #per-template? @@ -1409,6 +1417,7 @@ I, if specified, overrides "Invoice" as the name of the sent docume sub lpr_data { my $self = shift; + my $conf = $self->conf; my( $template, $notice_name ); if ( ref($_[0]) ) { my $opt = shift; @@ -1444,6 +1453,7 @@ I, if specified, overrides "Invoice" as the name of the sent docume #sub print_invoice { sub print { my $self = shift; + my $conf = $self->conf; my( $template, $notice_name ); if ( ref($_[0]) ) { my $opt = shift; @@ -1483,6 +1493,7 @@ I, if specified, overrides "Invoice" as the name of the sent docume sub fax_invoice { my $self = shift; + my $conf = $self->conf; my( $template, $notice_name ); if ( ref($_[0]) ) { my $opt = shift; @@ -1538,6 +1549,7 @@ enabled) sub get_open_bill_batch { my $self = shift; + my $conf = $self->conf; my $hashref = { status => 'O' }; $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent') ? $self->cust_main->agentnum @@ -1560,6 +1572,7 @@ TEMPLATENAME is unused? sub ftp_invoice { my $self = shift; + my $conf = $self->conf; my $template = scalar(@_) ? shift : ''; $self->send_csv( @@ -1582,6 +1595,7 @@ TEMPLATENAME is unused? sub spool_invoice { my $self = shift; + my $conf = $self->conf; my $template = scalar(@_) ? shift : ''; $self->spool_csv( @@ -2091,6 +2105,7 @@ sub realtime_lec { sub realtime_bop { my( $self, $method ) = (shift,shift); + my $conf = $self->conf; my %opt = @_; my $cust_main = $self->cust_main; @@ -2219,6 +2234,7 @@ I, if specified, overrides "Invoice" as the name of the sent docume sub print_latex { my $self = shift; + my $conf = $self->conf; my( $today, $template, %opt ); if ( ref($_[0]) ) { %opt = %{ shift() }; @@ -2284,6 +2300,7 @@ sub print_latex { SUFFIX => '.tex', UNLINK => 0, ) or die "can't open temp file: $!\n"; + binmode($fh, ':utf8'); # language support print $fh join('', @filled_in ); close $fh; @@ -2352,6 +2369,7 @@ notice_name - overrides "Invoice" as the name of the sent document (templates fr # yes: fixed width (dot matrix) text printing will be borked sub print_generic { my( $self, %params ) = @_; + my $conf = $self->conf; my $today = $params{today} ? $params{today} : time; warn "$me print_generic called on $self with suffix $params{template}\n" if $DEBUG; @@ -2635,7 +2653,11 @@ sub print_generic { 'total_pages' => 1, ); - + + #localization + my $lh = FS::L10N->get_handle($cust_main->locale); + $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) }; + my $min_sdate = 999999999999; my $max_edate = 0; foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { @@ -2770,9 +2792,9 @@ sub print_generic { if ($format eq 'latex'); } - $invoice_data{'po_line'} = + $invoice_data{'po_line'} = ( $cust_main->payby eq 'BILL' && $cust_main->payinfo ) - ? &$escape_function("Purchase Order #". $cust_main->payinfo) + ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo) : $nbsp; my %money_chars = ( 'latex' => '', @@ -2801,7 +2823,7 @@ sub print_generic { warn "$me generating sections\n" if $DEBUG > 1; - my $previous_section = { 'description' => 'Previous Charges', + my $previous_section = { 'description' => $self->mt('Previous Charges'), 'subtotal' => $other_money_char. sprintf('%.2f', $pr_total), 'summarized' => $summarypage ? 'Y' : '', @@ -2813,7 +2835,7 @@ sub print_generic { if $conf->exists('invoice_include_aging'); my $taxtotal = 0; - my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees', + my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'), 'subtotal' => $taxtotal, # adjusted below 'summarized' => $summarypage ? 'Y' : '', }; @@ -2825,7 +2847,8 @@ sub print_generic { my $adjusttotal = 0; - my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments', + my $adjust_section = { 'description' => + $self->mt('Credits, Payments, and Adjustments'), 'subtotal' => 0, # adjusted below 'summarized' => $summarypage ? 'Y' : '', }; @@ -2910,7 +2933,7 @@ sub print_generic { if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) { push @buf, ['','-----------']; - push @buf, [ 'Total Previous Balance', + push @buf, [ $self->mt('Total Previous Balance'), $money_char. sprintf("%10.2f", $pr_total) ]; push @buf, ['','']; } @@ -3066,7 +3089,7 @@ sub print_generic { if ( $taxtotal ) { my $total = {}; - $total->{'total_item'} = 'Sub-total'; + $total->{'total_item'} = $self->mt('Sub-total'); $total->{'total_amount'} = $other_money_char. sprintf('%.2f', $self->charged - $taxtotal ); @@ -3083,7 +3106,8 @@ sub print_generic { $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal); push @buf,['','-----------']; - push @buf,[( $conf->exists('disable_previous_balance') + push @buf,[$self->mt( + $conf->exists('disable_previous_balance') ? 'Total Charges' : 'Total New Charges' ), @@ -3092,7 +3116,7 @@ sub print_generic { { my $total = {}; - my $item = 'Total'; + my $item = $self->mt('Total'); $item = $conf->config('previous_balance-exclude_from_total') || 'Total New Charges' if $conf->exists('previous_balance-exclude_from_total'); @@ -3107,11 +3131,11 @@ sub print_generic { &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) ); if ( $multisection ) { if ( $adjust_section->{'sort_weight'} ) { - $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char. - sprintf("%.2f", ($self->billing_balance || 0) ); + $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '. + $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) ); } else { - $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char. - sprintf('%.2f', $self->charged ); + $adjust_section->{'pretotal'} = $self->mt('New charges total').' '. + $other_money_char. sprintf('%.2f', $self->charged ); } }else{ push @total_items, $total; @@ -3316,25 +3340,24 @@ sub print_generic { } #setup subroutine for the template - sub FS::cust_bill::_template::invoice_lines { - my $lines = shift || scalar(@FS::cust_bill::_template::buf); + #sub FS::cust_bill::_template::invoice_lines { # good god, no + $invoice_data{invoice_lines} = sub { # much better + my $lines = shift || scalar(@buf); map { - scalar(@FS::cust_bill::_template::buf) - ? shift @FS::cust_bill::_template::buf + scalar(@buf) + ? shift @buf : [ '', '' ]; } ( 1 .. $lines ); - } + }; my $lines; my @collect; while (@buf) { push @collect, split("\n", - $text_template->fill_in( HASH => \%invoice_data, - PACKAGE => 'FS::cust_bill::_template' - ) + $text_template->fill_in( HASH => \%invoice_data ) ); - $FS::cust_bill::_template::page++; + $invoice_data{'page'}++; } map "$_\n", @collect; }else{ @@ -3546,6 +3569,7 @@ sub _translate_old_latex_format { sub terms { my $self = shift; + my $conf = $self->conf; #check for an invoice-specific override return $self->invoice_terms if $self->invoice_terms; @@ -3574,10 +3598,11 @@ sub due_date2str { sub balance_due_msg { my $self = shift; - my $msg = 'Balance Due'; + my $msg = $self->mt('Balance Due'); return $msg unless $self->terms; if ( $self->due_date ) { - $msg .= ' - Please pay by '. $self->due_date2str($date_format); + $msg .= ' - ' . $self->mt('Please pay by'). ' '. + $self->due_date2str($date_format); } elsif ( $self->terms ) { $msg .= ' - '. $self->terms; } @@ -3586,6 +3611,7 @@ sub balance_due_msg { sub balance_due_date { my $self = shift; + my $conf = $self->conf; my $duedate = ''; if ( $conf->exists('invoice_default_terms') && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) { @@ -3594,7 +3620,10 @@ sub balance_due_date { $duedate; } -sub credit_balance_msg { 'Credit Balance Remaining' } +sub credit_balance_msg { + my $self = shift; + $self->mt('Credit Balance Remaining') +} =item invnum_date_pretty @@ -3605,7 +3634,7 @@ Returns a string with the invoice number and date, for example: sub invnum_date_pretty { my $self = shift; - 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')'; + $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')'; } =item _date_pretty @@ -4011,6 +4040,7 @@ sub _condensed_total_line_generator { sub _items_extra_usage_sections { my $self = shift; + my $conf = $self->conf; my $escape = shift; my $format = shift; @@ -4250,6 +4280,7 @@ sub _items_accountcode_cdr { sub _items_svc_phone_sections { my $self = shift; + my $conf = $self->conf; my $escape = shift; my $format = shift; @@ -4497,6 +4528,7 @@ sub _items { sub _items_previous { my $self = shift; + my $conf = $self->conf; my $cust_main = $self->cust_main; my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance my @b = (); @@ -4505,7 +4537,7 @@ sub _items_previous { ? 'due '. $_->due_date2str($date_format) : time2str($date_format, $_->_date); push @b, { - 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)", + 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)", #'pkgpart' => 'N/A', 'pkgnum' => 'N/A', 'amount' => sprintf("%.2f", $_->owed), @@ -4588,6 +4620,7 @@ sub _items_tax { sub _items_cust_bill_pkg { my $self = shift; + my $conf = $self->conf; my $cust_bill_pkgs = shift; my %opt = @_; @@ -4913,7 +4946,7 @@ sub _items_credits { #'description' => 'Credit ref\#'. $_->crednum. # " (". time2str("%x",$_->cust_credit->_date) .")". # $reason, - 'description' => 'Credit applied '. + 'description' => $self->mt('Credit applied').' '. time2str($date_format,$_->cust_credit->_date). $reason, 'amount' => sprintf("%.2f",$_->amount), }; @@ -4933,7 +4966,7 @@ sub _items_payments { #something more elaborate if $_->amount ne ->cust_pay->paid ? push @b, { - 'description' => "Payment received ". + 'description' => $self->mt('Payment received').' '. time2str($date_format,$_->cust_pay->_date ), 'amount' => sprintf("%.2f", $_->amount ) }; @@ -5159,6 +5192,7 @@ Currently only supported on PostgreSQL. =cut sub due_date_sql { + my $conf = new FS::Conf; 'COALESCE( SUBSTRING( COALESCE( diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 7832ecaf9..c1f95ea15 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -68,6 +68,7 @@ use FS::banned_pay; use FS::cust_main_note; use FS::cust_attachment; use FS::contact; +use FS::Locales; # 1 is mostly method/subroutine entry and options # 2 traces progress of some operations @@ -1692,6 +1693,7 @@ sub check { || $self->ut_floatn('credit_limit') || $self->ut_numbern('billday') || $self->ut_enum('edit_subject', [ '', 'Y' ] ) + || $self->ut_enum('locale', [ '', FS::Locales->locales ]) ; #barf. need message catalogs. i18n. etc. diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index e8e243f2d..d493060f0 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -538,6 +538,41 @@ sub process_email_search_result { } +=item conf + +Returns a configuration handle (L) set to the customer's locale, +if they have one. If not, returns an FS::Conf with no locale. + +=cut + +sub conf { + my $self = shift; + return $self->{_conf} if (ref $self and $self->{_conf}); + my $cust_main = $self->cust_main; + my $conf = new FS::Conf { + 'locale' => ($cust_main ? $cust_main->locale : '') + }; + $self->{_conf} = $conf if ref $self; + return $conf; +} + +=item mt TEXT [, ARGS ] + +Localizes a text string (see L) for the customer's locale, +if they have one. + +=cut + +sub mt { + my $self = shift; + return $self->{_lh}->maketext(@_) if (ref $self and $self->{_lh}); + my $cust_main = $self->cust_main; + my $locale = $cust_main ? $cust_main->locale : ''; + my $lh = FS::L10N->get_handle($locale); + $self->{_lh} = $lh if ref $self; + return $lh->maketext(@_); +} + =back =head1 BUGS diff --git a/FS/FS/part_export/shellcommands.pm b/FS/FS/part_export/shellcommands.pm index 50af45d7d..a8a57c663 100644 --- a/FS/FS/part_export/shellcommands.pm +++ b/FS/FS/part_export/shellcommands.pm @@ -193,7 +193,7 @@ old_ for replace operations):
  • $pkgnum
  • $custnum
  • All other fields in svc_acct are also available. -
  • The following fields from cust_main are also available (except during replace): company, address1, address2, city, state, zip, county, daytime, night, fax, otaker, agent_custid. When used on the command line (rather than STDIN), they will be quoted for the shell already (do not add additional quotes). +
  • The following fields from cust_main are also available (except during replace): company, address1, address2, city, state, zip, county, daytime, night, fax, otaker, agent_custid, locale. When used on the command line (rather than STDIN), they will be quoted for the shell already (do not add additional quotes). END ); @@ -263,7 +263,7 @@ sub _export_command { { no strict 'refs'; foreach my $custf (qw( company address1 address2 city state zip country - daytime night fax otaker agent_custid + daytime night fax otaker agent_custid locale )) { ${$custf} = $cust_pkg->cust_main->$custf(); @@ -343,6 +343,7 @@ sub _export_command { $fax = shell_quote $fax; $otaker = shell_quote $otaker; $agent_custid = shell_quote $agent_custid; + $locale = shell_quote $locale; my $command_string = eval(qq("$command")); my @ssh_cmd_args = ( @@ -419,6 +420,7 @@ sub _export_replace { if $error; $new_agent_custid = $new_cust_main ? $new_cust_main->agent_custid : ''; + $new_locale = $new_cust_main ? $new_cust_main->locale : ''; $old_pkgnum = $old_cust_pkg ? $old_cust_pkg->pkgnum : ''; $old_custnum = $old_cust_pkg ? $old_cust_pkg->custnum : ''; $new_pkgnum = $new_cust_pkg ? $new_cust_pkg->pkgnum : ''; @@ -432,6 +434,7 @@ sub _export_replace { $new_crypt_password = shell_quote $new_crypt_password; $new_ldap_password = shell_quote $new_ldap_password; $new_agent_custid = shell_quote $new_agent_custid; + $new_locale = shell_quote $new_locale; my $command_string = eval(qq("$command")); diff --git a/conf/invoice_html b/conf/invoice_html index 289ada1da..1d53683ad 100644 --- a/conf/invoice_html +++ b/conf/invoice_html @@ -11,6 +11,7 @@ .invoice_desc_more TD { font-weight: bold; font-size: 10pt } .invoice_extdesc TD { font-size: 8pt } .invoice_totaldesc TD { font-size: 10pt; empty-cells: show } +.allcaps { text-transform:uppercase }
    @@ -23,26 +24,26 @@ - @@ -64,7 +65,7 @@ %> <%= $ship_enable ? (' @@ -111,18 +112,13 @@ unless ($section->{'summarized'}) { $OUT .= '
    - Invoice date
    + <%= emt('Invoice date') %>
    <%= $date %>
    - Invoice #
    + <%= emt('Invoice #') %>
    <%= $invnum %>
    - Customer #
    + <%= emt('Customer #') %>
    <%= $custnum %>
      - <%= $notice_name ? substr($notice_name, 0, 1) : 'I' %><%= $notice_name ? uc(substr($notice_name, 1)) : 'NVOICE' %> + + <%= substr(emt($notice_name),0,1) %><%= substr(emt($notice_name),1) %>  
    '. - join('
    ',grep length($_), 'Service Address', + join('
    ',grep length($_), ''.emt('Service Address').'', $ship_company, $ship_address1, $ship_address2, @@ -86,7 +87,7 @@ $OUT .= qq!
    !; } %> - <%= $terms ? "Terms: $terms" : '' %>
    + <%= $terms ? emt('Terms') . ": $terms" : '' %>
    <%= $po_line %>
    ' if ( $notfirst || $section->{'pretotal'} && !$summary ); $OUT .= '
    '; - if ($section->{'description'}) { - $OUT .= - '

    '. uc(substr($section->{'description'},0,1)). - ''. uc(substr($section->{'description'},1)). + my $sectionhead = $section->{'description'} || emt('Charges'); + $OUT .= + '

    '. substr($sectionhead,0,1). + ''. substr($sectionhead,1). ''. - '

    '; - }else{ - $OUT .= - '

    CHARGES'. - '

    '; - } - $OUT .= '

    '; + '

    '. + '

    '; $OUT .= ''. @@ -133,14 +129,13 @@ $OUT .= $header; $columncount = scalar(my @array = split /<\/th>'. - ( $unitprices - ? ''. - '' - : '' - ). - ''; + $OUT .= ''. + ''. + ( $unitprices + ? ''. + '' + : '' ). + ''; } $OUT .= ''; @@ -207,7 +202,7 @@ } else { $OUT .= qq('. + $section->{'description'}. ' ' . emt('Total') . ''. qq('; } diff --git a/conf/invoice_latex b/conf/invoice_latex index 10f30cfe8..37f59d2ee 100644 --- a/conf/invoice_latex +++ b/conf/invoice_latex @@ -21,6 +21,8 @@ \usepackage{fancyhdr,lastpage,ifthen,array,fslongtable,afterpage,caption,multirow,bigstrut} \usepackage{graphicx} % required for logo graphic +\usepackage[utf8]{inputenc} % multilanguage support +\usepackage[T1]{fontenc} \addtolength{\voffset}{-0.0cm} % top margin to top of header \addtolength{\hoffset}{-0.6cm} % left margin on page @@ -125,10 +127,10 @@ \ifthenelse{\equal{\thepage}{1}} { % First page \begin{tabular}{ccc} - Invoice date & Invoice \#& Customer \#\\ + [@-- join(' & ', emt('Invoice date'), emt('Invoice #'), emt('Customer #') ) --@]\\ \vspace{0.2cm} \textbf{[@-- $date --@]} & \textbf{[@-- $invnum --@]} & \textbf{[@-- $custnum --@]} \\\hline - \rule{0pt}{5ex} &~~ \huge{\textsc{[@-- $notice_name || 'Invoice' --@]}} & \\ + \rule{0pt}{5ex} &~~ \huge{\textsc{[@-- emt($notice_name) --@]}} & \\ \vspace{-0.2cm} & & \\\hline \end{tabular} @@ -136,7 +138,7 @@ { % ... pages \small{ \begin{tabular}{lll} - Invoice date & Invoice \#& Customer\#\\ + [@-- join(' & ', emt('Invoice date'), emt('Invoice #'), emt('Customer #') ) --@]\\ \textbf{[@-- $date --@]} & \textbf{[@-- $invnum --@]} & \textbf{[@-- $custnum --@]}\\ \end{tabular} } @@ -161,19 +163,18 @@ \newcommand{\FSdescriptionlength} { [@-- $unitprices ? '8.2cm' : '12.8cm' --@] } \newcommand{\FSdescriptioncolumncount} { [@-- $unitprices ? '4' : '6' --@] } -\newcommand{\FSunitcolumns}{ [@-- $unitprices ? '\makebox[2.5cm][l]{\textbf{~~Unit Price}}&\makebox[1.4cm]{\textbf{~Quantity}}&' : '' --@] } +\newcommand{\FSunitcolumns}{ [@-- + $unitprices + ? '\makebox[2.5cm][l]{\textbf{~~'.emt('Unit Price').'}}&\makebox[1.4cm]{\textbf{~'.emt('Quantity').'}}&' + : '' --@] } \newcommand{\FShead}{ \hline \rule{0pt}{2.5ex} \makebox[1.4cm]{\textbf{Ref}} & -% \makebox[2.9cm][l]{\textbf{Description}}& -% \makebox[1.4cm][l]{}& -% \makebox[1.4cm][l]{}& -% \makebox[2.5cm][l]{}& - \multicolumn{\FSdescriptioncolumncount}{l}{\makebox[\FSdescriptionlength][l]{\textbf{Description}}}& + \multicolumn{\FSdescriptioncolumncount}{l}{\makebox[\FSdescriptionlength][l]{\textbf{[@-- emt('Description') --@]}}}& \FSunitcolumns - \makebox[1.6cm][r]{\textbf{Amount}} \\ + \makebox[1.6cm][r]{\textbf{[@-- emt('Amount') --@]}} \\ \hline } @@ -217,7 +218,7 @@ \begin{minipage}[t]{6.4cm} [@-- if ($ship_enable) { - $OUT .= '\textbf{Service Address}\\\\'; + $OUT .= '\textbf{' . emt('Service Address') . '}\\\\'; $OUT .= "\\addressline{$ship_company}"; $OUT .= "\\addressline{$ship_address1}"; $OUT .= "\\addressline{$ship_address2}"; @@ -229,7 +230,7 @@ } --@] \begin{flushright} -[@-- $terms ? "Terms: $terms" : '' --@]\\ +[@-- $terms ? emt('Terms') .": $terms" : '' --@]\\ [@-- $po_line --@]\\ \end{flushright} \end{minipage}} @@ -252,7 +253,7 @@ if $coupon; $OUT .= '\begin{longtable}{cllllllr}'; $OUT .= '\caption*{ '; - $OUT .= ($section->{'description'}) ? $section->{'description'}: 'Charges'; + $OUT .= ($section->{'description'}) ? $section->{'description'}: emt('Charges'); $OUT .= '}\\\\'; if ($section->{header_generator}) { $OUT .= &{$section->{header_generator}}(); @@ -260,14 +261,14 @@ $OUT .= '\FShead'; } $OUT .= '\endfirsthead'; - $OUT .= '\multicolumn{7}{r}{\rule{0pt}{2.5ex}Continued from previous page}\\\\'; + $OUT .= '\multicolumn{7}{r}{\rule{0pt}{2.5ex}'.emt('Continued from previous page').'}\\\\'; if ($section->{header_generator}) { $OUT .= &{$section->{header_generator}}(); } else { $OUT .= '\FShead'; } $OUT .= '\endhead'; - $OUT .= '\multicolumn{7}{r}{\rule{0pt}{2.5ex}Continued on next page...}\\\\'; + $OUT .= '\multicolumn{7}{r}{\rule{0pt}{2.5ex}'.emt('Continued on next page...').'}\\\\'; $OUT .= '\endfoot'; $OUT .= '\hline'; diff --git a/conf/invoice_template b/conf/invoice_template index ebf8ef7d0..769f043d7 100644 --- a/conf/invoice_template +++ b/conf/invoice_template @@ -1,6 +1,6 @@ - { $notice_name || 'Invoice'; } - { substr("Page $page of $total_pages ", 0, 19); } { use Date::Format; time2str("%x", $date); } Invoice #{ $invnum; } + { emt($notice_name) } + { substr(emt("Page [_1] of [_2] ", $page, $total_pages), 0, 19); } { use Date::Format; time2str("%x", $date); } { emt("Invoice #") . $invnum; } { $company_name; } diff --git a/httemplate/config/config-download.cgi b/httemplate/config/config-download.cgi index 6979246db..c071f2a6d 100644 --- a/httemplate/config/config-download.cgi +++ b/httemplate/config/config-download.cgi @@ -1,21 +1,3 @@ -% -% -%my $conf=new FS::Conf; -% -%http_header('Content-Type' => 'application/x-unknown' ); -% -%die "No configuration variable specified (bad URL)!" # umm -% unless $cgi->param('key'); -%$cgi->param('key') =~ /^([-\w.]+)$/; -%my $name = $1; -% -%my $agentnum; -%if ($cgi->param('agentnum') =~ /^(\d+)$/) { -% $agentnum = $1; -%} -% -%http_header('Content-Disposition' => "attachment; filename=$name" ); -% print $conf->config_binary($name, $agentnum); <%init> die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); @@ -25,4 +7,24 @@ if ($cgi->param('agentnum') =~ /^(\d+)$/) { $agentnum = $1; } +http_header('Content-Type' => 'application/x-unknown' ); + +die "No configuration variable specified (bad URL)!" # umm + unless $cgi->param('key'); +$cgi->param('key') =~ /^([-\w.]+)$/; +my $name = $1; + +my $agentnum; +if ($cgi->param('agentnum') =~ /^(\d+)$/) { + $agentnum = $1; +} + +my $locale = ''; +if ($cgi->param('locale') =~ /^(\w+)$/) { + $locale = $1; +} +my $conf=new FS::Conf { 'locale' => $locale }; + +http_header('Content-Disposition' => "attachment; filename=$name" ); +print $conf->config_binary($name, $agentnum); diff --git a/httemplate/config/config-image.cgi b/httemplate/config/config-image.cgi index 0de9d4278..0e04ab5bc 100644 --- a/httemplate/config/config-image.cgi +++ b/httemplate/config/config-image.cgi @@ -4,8 +4,6 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); -my $conf = new FS::Conf; - http_header( 'Content-Type' => 'image/png' ); #just png for now $cgi->param('key') =~ /^([-\w.]+)$/ or die "illegal config option"; @@ -16,6 +14,13 @@ if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { $agentnum = $1; } +my $locale = ''; +if ( $cgi->param('locale') =~ /^(\w+)$/ ) { + $locale = $1; +} + +my $conf = new FS::Conf { 'locale' => $locale }; + my $logo = $conf->config_binary($name, $agentnum); $logo = eps2png($logo) if $name =~ /\.eps$/i; diff --git a/httemplate/config/config-process.cgi b/httemplate/config/config-process.cgi index 4a84c0dd1..c62a8c784 100644 --- a/httemplate/config/config-process.cgi +++ b/httemplate/config/config-process.cgi @@ -25,7 +25,7 @@ % } % %} else { -<% header('Configuration set') %> +<& /elements/header.html, 'Configuration set' &> +invoice language options: + +<& /elements/select.html, + 'field' => 'locale', + 'options' => [ '', @locales ], + 'labels' => { map { + my %info = FS::Locales->locale_info($_); + $_ => "$info{name} ($info{country})" + } @locales }, + 'curr_value' => $locale, + 'id' => 'select-locale', + 'onchange' => 'changeLocale' + &> + ) +% $cgi->param('locale', $locale); +% } + +

    <% include('/elements/init_overlib.html') %> @@ -89,11 +121,12 @@ Click on a configuration value to change it. % if $agent && $cgi->param('showagent'); % % #indentation :/ +% my $action = 'config.cgi?key=' . $i->key . +% ";agentnum=$agentnum" . ($locale ? ";locale=$locale" : ''); % } elsif ( $type eq 'image' ) { +% my $args = 'key=' . $i->key . ";agentnum=$agentnum;locale=$locale"; @@ -141,18 +174,19 @@ Click on a configuration value to change it. % } elsif ( $type eq 'binary' ) { +% my $args = 'key=' . $i->key . ";agentnum=$agentnum;locale=$locale"; @@ -344,14 +378,37 @@ if ($cgi->param('agentnum') =~ /^(\d+)$/) { die "Agent $page_agentnum not found!" unless $page_agent; push @menubar, 'View all agents' => $p.'browse/agent.cgi'; +} + +my $conf = new FS::Conf; +my $conf_global = $conf; + +my @locales = $conf_global->config('available-locales'); + +# if this is set, we are in locale mode, so limit the displayed items +# to those with per_locale. +my $locale; +my $locale_desc; +if ( $cgi->param('locale') =~ /^\w+_\w+$/ ) { + $locale = $cgi->param('locale'); + # and set the context on $conf + $conf = new FS::Conf { 'locale' => $locale, 'localeonly' => 1 }; + my %locale_info = FS::Locales->locale_info($locale); + $locale_desc = "$locale_info{name} ($locale_info{country})"; + + $title = 'Invoice Configuration'; #for now it is only invoicing + $title .= ' for '.$page_agent->agent if $page_agent; + $title .= ', '.$locale_desc; + +} elsif ($page_agent) { $title = 'Agent Configuration for '. $page_agent->agent; + $title .= ", $locale_desc" if $locale; } else { $title = 'Global Configuration'; } -my $conf = new FS::Conf; - -my @config_items = grep { $page_agent ? $_->per_agent : 1 } +my @config_items = grep { !defined($locale) or $_->per_locale } + grep { $page_agent ? $_->per_agent : 1 } grep { $page_agent ? 1 : !$_->agentonly } $conf->config_items; diff --git a/httemplate/config/config.cgi b/httemplate/config/config.cgi index 040ed0403..6a1eaecf7 100644 --- a/httemplate/config/config.cgi +++ b/httemplate/config/config.cgi @@ -24,6 +24,7 @@ function SafeOnsubmit() { + Setting <% $key %> @@ -49,7 +50,8 @@ Setting <% $key %> <% $conf->exists($key, $agentnum) ? 'Current image
    '. '
    ' + ';agentnum='. $agentnum. + ';locale='. $locale .'">
    ' : '' %> @@ -318,10 +320,6 @@ Setting <% $key %> <%once> -my $conf = new FS::Conf; -my @config_items = $conf->config_items; -my %confitems = map { $_->key => $_ } @config_items; - my %element_types = map { $_ => 1 } qw( select-part_svc select-part_pkg select-pkg_class select-agent ); @@ -339,6 +337,15 @@ if ($cgi->param('agentnum') =~ /(\d+)$/) { $agentnum=$1; } +my $locale = ''; +if ( $cgi->param('locale') =~ /^(\w+_\w+)$/) { + $locale = $1; +} + +my $conf = new FS::Conf { 'locale' => $locale, 'localeonly' => 1 }; +my @config_items = $conf->config_items; +my %confitems = map { $_->key => $_ } @config_items; + my $agent = ''; my $title; if ($agentnum) { diff --git a/httemplate/edit/cust_main/billing.html b/httemplate/edit/cust_main/billing.html index f2d6271d0..294104b09 100644 --- a/httemplate/edit/cust_main/billing.html +++ b/httemplate/edit/cust_main/billing.html @@ -555,6 +555,24 @@ function toggle(obj) { % } +%my @available_locales = $conf->config('available-locales'); +%if ( scalar(@available_locales) ) { +% push @available_locales, ''; +% my %locale_labels = map { +% my %ll; +% my %info = FS::Locales->locale_info($_); +% $ll{$_} = $info{name} . " (" . $info{country} . ")"; +% %ll; +% } FS::Locales->locales; + <& /elements/tr-select.html, + 'label' => emt('Invoicing locale'), + 'field' => 'locale', + 'options' => \@available_locales, + 'labels' => \%locale_labels, + 'curr_value' => $cust_main->locale, + &> +% } +
    DescriptionUnit PriceQuantityAmount' . emt('Ref') . '' . emt('Description') . '' . emt('Unit Price') . '' . emt('Quantity') . '' . emt('Amount') . '
    ' : '>' ). - $section->{'description'}. ' Total ). $section->{'subtotal'}. '
    <% include('/elements/popup_link.html', - 'action' => 'config.cgi?key='. $i->key. - ';agentnum='. $agentnum, + 'action' => $action, 'width' => $width, 'height' => $height, 'actionlabel' => 'Enter configuration value', @@ -128,12 +161,12 @@ Click on a configuration value to change it.
    <% $conf->exists($i->key, $agentnum) - ? '' + ? '' : 'empty' %>
    <% $conf->exists($i->key, $agentnum) - ? qq!download! + ? 'download' : '' %>
    <% $conf->exists($i->key, $agentnum) - ? qq!download! + ? 'download' : 'empty' %>
    <% $r %> <% mt('required fields') |h %> diff --git a/httemplate/view/cust_bill-logo.cgi b/httemplate/view/cust_bill-logo.cgi index ad2ff5430..75321ef82 100755 --- a/httemplate/view/cust_bill-logo.cgi +++ b/httemplate/view/cust_bill-logo.cgi @@ -5,7 +5,7 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('View invoices') or $FS::CurrentUser::CurrentUser->access_right('Configuration'); -my $conf = new FS::Conf; +my $conf; my $templatename; my $agentnum = ''; @@ -13,6 +13,7 @@ if ( $cgi->param('invnum') ) { $templatename = $cgi->param('template') || $cgi->param('templatename'); my $cust_bill = qsearchs('cust_bill', { 'invnum' => $cgi->param('invnum') } ) or die 'unknown invnum'; + $conf = $cust_bill->conf; $agentnum = $cust_bill->cust_main->agentnum; } else { my($query) = $cgi->keywords; diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html index cf22ff701..09c0b4d1d 100644 --- a/httemplate/view/cust_main/billing.html +++ b/httemplate/view/cust_main/billing.html @@ -273,6 +273,15 @@ % } +% if ( $cust_main->locale ) { +% my %locale_info = FS::Locales->locale_info($cust_main->locale); + + <% mt('Invoicing locale') |h %> + <% $locale_info{name} . " (" . $locale_info{country} .")" %> + +% } + + <%once>