multi-currency, RT#21565
authorIvan Kohler <ivan@freeside.biz>
Tue, 11 Jun 2013 06:15:03 +0000 (23:15 -0700)
committerIvan Kohler <ivan@freeside.biz>
Tue, 11 Jun 2013 06:15:03 +0000 (23:15 -0700)
FS/FS/Schema.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_main.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_pkg.pm
FS/FS/part_pkg/currency_fixed.pm
FS/FS/part_pkg/flat.pm
FS/FS/part_pkg/recur_Common.pm
httemplate/edit/cust_main/billing.html
httemplate/search/cust_bill_pkg.cgi
httemplate/view/cust_main/billing.html

index f1cc09c..da3ddab 100644 (file)
@@ -768,24 +768,26 @@ sub tables_hashref {
 
     'cust_bill_pkg' => {
       'columns' => [
-        'billpkgnum',        'serial',     '',      '', '', '', 
-        'invnum',               'int',     '',      '', '', '', 
-        'pkgnum',               'int',     '',      '', '', '', 
-        'pkgpart_override',     'int', 'NULL',      '', '', '', 
-        'setup',               @money_type,             '', '', 
-        'recur',               @money_type,             '', '', 
-        #XXX a currency for a line item?  or just one for the entire invoice
-        #'currency',            'char', 'NULL',       3, '', '',
-        'sdate',               @date_type,              '', '', 
-        'edate',               @date_type,              '', '', 
-        'itemdesc',         'varchar', 'NULL', $char_d, '', '', 
-        'itemcomment',      'varchar', 'NULL', $char_d, '', '', 
-        'section',          'varchar', 'NULL', $char_d, '', '', 
-        'freq',             'varchar', 'NULL', $char_d, '', '',
-        'quantity',             'int', 'NULL',      '', '', '',
-        'unitsetup',           @money_typen,            '', '', 
-        'unitrecur',           @money_typen,            '', '', 
-        'hidden',              'char', 'NULL',       1, '', '',
+        'billpkgnum',          'serial',     '',      '', '', '', 
+        'invnum',                 'int',     '',      '', '', '', 
+        'pkgnum',                 'int',     '',      '', '', '', 
+        'pkgpart_override',       'int', 'NULL',      '', '', '', 
+        'setup',                 @money_type,             '', '', 
+        'unitsetup',             @money_typen,            '', '', 
+        'setup_billed_currency', 'char', 'NULL',       3, '', '',
+        'setup_billed_amount',   @money_typen,            '', '',
+        'recur',                 @money_type,             '', '', 
+        'unitrecur',             @money_typen,            '', '', 
+        'recur_billed_currency', 'char', 'NULL',       3, '', '',
+        'recur_billed_amount',   @money_typen,            '', '',
+        'sdate',                 @date_type,              '', '', 
+        'edate',                 @date_type,              '', '', 
+        'itemdesc',           'varchar', 'NULL', $char_d, '', '', 
+        'itemcomment',        'varchar', 'NULL', $char_d, '', '', 
+        'section',            'varchar', 'NULL', $char_d, '', '', 
+        'freq',               'varchar', 'NULL', $char_d, '', '',
+        'quantity',               'int', 'NULL',      '', '', '',
+        'hidden',                'char', 'NULL',       1, '', '',
       ],
       'primary_key' => 'billpkgnum',
       'unique' => [],
index 0c8c0bb..572fe79 100644 (file)
@@ -434,7 +434,13 @@ sub check {
       || $self->ut_snumber('pkgnum')
       || $self->ut_number('invnum')
       || $self->ut_money('setup')
+      || $self->ut_moneyn('unitsetup')
+      || $self->ut_currencyn('setup_billed_currency')
+      || $self->ut_moneyn('setup_billed_amount')
       || $self->ut_money('recur')
+      || $self->ut_moneyn('unitrecur')
+      || $self->ut_currencyn('recur_billed_currency')
+      || $self->ut_moneyn('recur_billed_amount')
       || $self->ut_numbern('sdate')
       || $self->ut_numbern('edate')
       || $self->ut_textn('itemdesc')
index 3fbee15..f3122aa 100644 (file)
@@ -1757,7 +1757,7 @@ sub check {
     || $self->ut_flag('invoice_noemail')
     || $self->ut_flag('message_noemail')
     || $self->ut_enum('locale', [ '', FS::Locales->locales ])
-    || $self->ut_currency('currency')
+    || $self->ut_currencyn('currency')
   ;
 
   my $company = $self->company;
index 814802b..220f66a 100644 (file)
@@ -951,6 +951,8 @@ sub _make_lines {
   my $unitsetup = 0;
   my @setup_discounts = ();
   my %setup_param = ( 'discounts' => \@setup_discounts );
+  my $setup_billed_currency = '';
+  my $setup_billed_amount = 0;
   if (     ! $options{recurring_only}
        and ! $options{cancel}
        and ( $options{'resetup'}
@@ -977,7 +979,13 @@ sub _make_lines {
         return "$@ running calc_setup for $cust_pkg\n"
           if $@;
 
-        $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
+        $unitsetup = $cust_pkg->base_setup()
+                       || $setup; #XXX uuh
+
+        if ( $setup_param{'billed_currency'} ) {
+          $setup_billed_currency = delete $setup_param{'billed_currency'};
+          $setup_billed_amount   = delete $setup_param{'billed_amount'};
+        }
     }
 
     $cust_pkg->setfield('setup', $time)
@@ -997,6 +1005,8 @@ sub _make_lines {
   my $recur = 0;
   my $unitrecur = 0;
   my @recur_discounts = ();
+  my $recur_billed_currency = '';
+  my $recur_billed_amount = 0;
   my $sdate;
   if (     ! $cust_pkg->start_date
        and ( ! $cust_pkg->susp || $cust_pkg->option('suspend_bill',1)
@@ -1058,6 +1068,11 @@ sub _make_lines {
     #base_cancel???
     $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better
 
+    if ( $param{'billed_currency'} ) {
+      $recur_billed_currency = delete $param{'billed_currency'};
+      $recur_billed_amount   = delete $param{'billed_amount'};
+    }
+
     if ( $increment_next_bill ) {
 
       my $next_bill;
@@ -1173,16 +1188,20 @@ sub _make_lines {
       push @details, @cust_pkg_detail;
 
       my $cust_bill_pkg = new FS::cust_bill_pkg {
-        'pkgnum'    => $cust_pkg->pkgnum,
-        'setup'     => $setup,
-        'unitsetup' => $unitsetup,
-        'recur'     => $recur,
-        'unitrecur' => $unitrecur,
-        'quantity'  => $cust_pkg->quantity,
-        'details'   => \@details,
-        'discounts' => [ @setup_discounts, @recur_discounts ],
-        'hidden'    => $part_pkg->hidden,
-        'freq'      => $part_pkg->freq,
+        'pkgnum'                => $cust_pkg->pkgnum,
+        'setup'                 => $setup,
+        'unitsetup'             => $unitsetup,
+        'setup_billed_currency' => $setup_billed_currency,
+        'setup_billed_amount'   => $setup_billed_amount,
+        'recur'                 => $recur,
+        'unitrecur'             => $unitrecur,
+        'recur_billed_currency' => $recur_billed_currency,
+        'recur_billed_amount'   => $recur_billed_amount,
+        'quantity'              => $cust_pkg->quantity,
+        'details'               => \@details,
+        'discounts'             => [ @setup_discounts, @recur_discounts ],
+        'hidden'                => $part_pkg->hidden,
+        'freq'                  => $part_pkg->freq,
       };
 
       if ( $part_pkg->option('prorate_defer_bill',1) 
@@ -1194,7 +1213,7 @@ sub _make_lines {
         $cust_bill_pkg->sdate( $hash{last_bill} );
         $cust_bill_pkg->edate( $sdate - 86399   ); #60s*60m*24h-1
         $cust_bill_pkg->edate( $time ) if $options{cancel};
-      } else { #if ( $part_pkg->recur_temporality eq 'upcoming' ) {
+      } else { #if ( $part_pkg->recur_temporality eq 'upcoming' )
         $cust_bill_pkg->sdate( $sdate );
         $cust_bill_pkg->edate( $cust_pkg->bill );
         #$cust_bill_pkg->edate( $time ) if $options{cancel};
index 8357386..3d24ea5 100644 (file)
@@ -2191,6 +2191,18 @@ sub calc_recur {
   $self->part_pkg->calc_recur($self, @_);
 }
 
+=item base_setup
+
+Calls the I<base_setup> of the FS::part_pkg object associated with this billing
+item.
+
+=cut
+
+sub base_setup {
+  my $self = shift;
+  $self->part_pkg->base_setup($self, @_);
+}
+
 =item base_recur
 
 Calls the I<base_recur> of the FS::part_pkg object associated with this billing
@@ -2345,9 +2357,11 @@ sub num_cust_event {
 
 =item part_pkg_currency_option OPTIONNAME
 
-Returns the option value for the given name and the currency of this customer
-(see L<FS::part_pkg_currency>).  If this customer has no currency, returns
-the regular option value for the given name (see L<FS::part_pkg_option>).
+Returns a two item list consisting of the currency of this customer, if any,
+and a value for the provided option.  If the customer has a currency, the value
+is the option value the given name and the currency (see
+L<FS::part_pkg_currency>).  Otherwise, if the customer has no currency, is the
+regular option value for the given name (see L<FS::part_pkg_option>).
 
 =cut
 
@@ -2355,9 +2369,9 @@ sub part_pkg_currency_option {
   my( $self, $optionname ) = @_;
   my $part_pkg = $self->part_pkg;
   if ( my $currency = $self->cust_main->currency ) {
-    $part_pkg->part_pkg_currency_option($currency, $optionname);
+    ($currency, $part_pkg->part_pkg_currency_option($currency, $optionname) );
   } else {
-    $part_pkg->option($optionname);
+    ('', $part_pkg->option($optionname) );
   }
 }
 
index bee6c5b..ce71452 100644 (file)
@@ -5,7 +5,8 @@ use base qw( FS::part_pkg::recur_Common );
 
 use strict;
 use vars qw( %info );
-#use FS::Record qw(qsearch qsearchs);
+use FS::Record qw(qsearchs); # qsearch qsearchs);
+use FS::currency_exchange;
 
 %info = (
   'name' => 'Per-currency pricing from package definitions',
@@ -26,7 +27,7 @@ use vars qw( %info );
   },
   'fieldorder' => [qw( recur_method cutoff_day ),
                    FS::part_pkg::prorate_Mixin::fieldorder,
-                  )],
+                  ],
   'weight' => '59',
 );
 
@@ -37,19 +38,44 @@ sub price_info {
     $str;
 }
 
-#some false laziness w/recur_Common, could have been better about it.. pry when
-# we do discounting
+sub base_setup {
+  my($self, $cust_pkg, $sdate, $details, $param ) = @_;
+
+  $self->calc_currency_option('setup_fee', $cust_pkg, $sdate, $details, $param);
+}
+
 sub calc_setup {
   my($self, $cust_pkg, $sdate, $details, $param) = @_;
 
   return 0 if $self->prorate_setup($cust_pkg, $sdate);
 
-  sprintf('%.2f', $cust_pkg->part_pkg_currency_option('setup_fee') );
+  $self->base_setup($cust_pkg, $sdate, $details, $param);
+}
+
+use FS::Conf;
+sub calc_currency_option {
+  my($self, $optionname, $cust_pkg, $sdate, $details, $param) = @_;
+
+  my($currency, $amount) = $cust_pkg->part_pkg_currency_option($optionname);
+  return sprintf('%.2f', $amount ) unless $currency;
+
+  $param->{'billed_currency'} = $currency;
+  $param->{'billed_amount'}   = $amount;
+
+  my $currency_exchange = qsearchs('currency_exchange', {
+    'from_currency' => $currency,
+    'to_currency'   => ( FS::Conf->new->config('currency') || 'USD' ),
+  }) or die "No exchange rate from $currency\n";
+
+  #XXX do we want the rounding here to work differently?
+  #my $recognized_amount =
+  sprintf('%.2f', $amount * $currency_exchange->rate);
 }
 
 sub base_recur {
-  my( $self, $cust_pkg ) = @_;
-  sprintf('%.2f', $cust_pkg->part_pkg_currency_option('recur_fee') );
+  my( $self, $cust_pkg, $sdate, $details, $param ) = @_;
+  $param ||= {};
+  $self->calc_currency_option('recur_fee', $cust_pkg, $sdate, $details, $param);
 }
 
 sub can_discount { 0; } #can't discount yet (percentage would work, but amount?)
index 22eb698..9737a94 100644 (file)
@@ -122,7 +122,7 @@ sub calc_setup {
 
   my $quantity = $cust_pkg->quantity || 1;
 
-  my $charge = $quantity * $self->unit_setup($cust_pkg, $sdate, $details);
+  my $charge = $quantity * $self->base_setup($cust_pkg, $sdate, $details);
 
   my $discount = 0;
   if ( $charge > 0 ) {
@@ -134,7 +134,7 @@ sub calc_setup {
   sprintf('%.2f', $charge - $discount);
 }
 
-sub unit_setup {
+sub base_setup {
   my($self, $cust_pkg, $sdate, $details ) = @_;
 
   $self->option('setup_fee') || 0;
index 03d5c2c..ebf8869 100644 (file)
@@ -61,7 +61,7 @@ sub calc_recur_Common {
     my $recur_method = $self->option('recur_method', 1) || 'anniversary';
     my @cutoff_day = $self->cutoff_day($cust_pkg);
 
-    $charges = $self->base_recur($cust_pkg);
+    $charges = $self->base_recur($cust_pkg, $sdate, $details, $param);
     $charges += $param->{'override_charges'} if $param->{'override_charges'};
 
     if ( $recur_method eq 'prorate' ) {
index 5a66f0a..da5f0f2 100644 (file)
@@ -615,6 +615,23 @@ function toggle(obj) {
       <INPUT TYPE="hidden" NAME="cdr_termination_percentage" VALUE="<% $cust_main->cdr_termination_percentage %>">
 % }
 
+%my @currencies = $conf->config('currencies');
+%if ( scalar(@currencies) ) {
+%  unshift @currencies, ''; #default
+%  my %currency_labels = map { $_ => "$_: ". code2currency($_) } @currencies;
+%  $currency_labels{''} =
+%    'Default: '. code2currency( $conf->config('currency') || 'USD' );
+
+    <& /elements/tr-select.html, 
+         'label'         => emt('Invoicing currency'),
+         'field'         => 'currency',
+         'options'       => \@currencies,
+         'labels'        => \%currency_labels,
+         'curr_value'    => $cust_main->currency,
+    &>
+% }
+
+
 %my @available_locales = $conf->config('available-locales');
 %if ( scalar(@available_locales) ) {
 %  push @available_locales, ''
index 1830511..b2ff45b 100644 (file)
@@ -10,6 +10,7 @@
                    emt('Description'),
                    @post_desc_header,
                    @peritem_desc,
+                   @currency_desc,
                    emt('Invoice'),
                    emt('Date'),
                    emt('Paid'),
@@ -32,6 +33,7 @@
                    #strikethrough or "N/A ($amount)" or something these when
                    # they're not applicable to pkg_tax search
                    @peritem_sub,
+                   @currency_sub,
                    'invnum',
                    sub { time2str('%b %d %Y', shift->_date ) },
                    sub { sprintf($money_char.'%.2f', shift->get('pay_amount')) },
@@ -44,6 +46,7 @@
                    '',
                    @post_desc_null,
                    @peritem,
+                   @currency,
                    'invnum',
                    '_date',
                    #'pay_amount',
@@ -55,6 +58,7 @@
                    '',
                    @post_desc_null,
                    @peritem_null,
+                   @currency_null,
                    $ilink,
                    $ilink,
                    $pay_link,
@@ -68,6 +72,7 @@
                             'rl'.
                             $post_desc_align.
                             $peritem_align.
+                            $currency_align.
                             'rcrr'.
                             FS::UI::Web::cust_aligns(),
                  'color' => [ 
@@ -76,6 +81,7 @@
                               '',
                               @post_desc_null,
                               @peritem_null,
+                              @currency_null,
                               '',
                               '',
                               '',
@@ -88,6 +94,7 @@
                               '',
                               @post_desc_null,
                               @peritem_null,
+                              @currency_null,
                               '',
                               '',
                               '',
@@ -196,6 +203,23 @@ my @total_desc = ( $money_char.'%.2f total' ); # sprintf strings
 my @peritem = ( 'setup', 'recur' );
 my @peritem_desc = ( 'Setup charge', 'Recurring charge' );
 
+my @currency_desc = ();
+my @currency_sub = ();
+my @currency = ();
+if ( $conf->config('currencies') ) {
+  @currency_desc = ( 'Setup billed', 'Recurring billed' );
+  @currency_sub = (
+    map {
+      my $what = $_;
+      sub { my $currency = $_[0]->get($what.'_billed_currency');
+            $currency. ' '. currency_symbol($currency, SYM_HTML).
+              $_[0]->get($what.'_billed_amount');
+          };
+    } qw( setup recur )
+  );
+  @currency = ( 'setup_billed_amount', 'recur_billed_amount' ); #for sorting
+}
+
 my @pkgnum_header = ();
 my @pkgnum = ();
 my @pkgnum_null;
@@ -672,6 +696,10 @@ my @peritem_sub = map {
 my @peritem_null = map { '' } @peritem; # placeholders
 my $peritem_align = 'r' x scalar(@peritem);
 
+@currency_desc = map {emt($_)} @currency_desc;
+my @currency_null = map { '' } @currency; # placeholders
+my $currency_align = 'r' x scalar(@currency);
+
 my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ];
 my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
 
index b863a73..e286305 100644 (file)
   </TR>
 % }
 
+% if ( $cust_main->currency ) {
+  <TR>
+    <TD ALIGN="right"><% mt('Invoicing currency') |h %></TD>
+    <TD BGCOLOR="#ffffff"><% $cust_main->currency. ': '. code2currency($cust_main->currency) %></TD>
+  </TR>
+% }
+
 % if ( $cust_main->locale ) {
 % my %locale_info = FS::Locales->locale_info($cust_main->locale);
   <TR>