multi-currency, RT#21565
authorIvan Kohler <ivan@freeside.biz>
Sat, 8 Jun 2013 08:30:52 +0000 (01:30 -0700)
committerIvan Kohler <ivan@freeside.biz>
Sat, 8 Jun 2013 08:30:52 +0000 (01:30 -0700)
25 files changed:
FS/FS/Conf.pm
FS/FS/Mason.pm
FS/FS/Record.pm
FS/FS/Schema.pm
FS/FS/agent.pm
FS/FS/agent_currency.pm [new file with mode: 0644]
FS/FS/currency_exchange.pm [new file with mode: 0644]
FS/FS/part_pkg.pm
FS/FS/part_pkg_currency.pm [new file with mode: 0644]
FS/MANIFEST
FS/t/agent_currency.t [new file with mode: 0644]
FS/t/currency_exchange.t [new file with mode: 0644]
FS/t/part_pkg_currency.t [new file with mode: 0644]
httemplate/browse/agent.cgi
httemplate/config/config.cgi
httemplate/edit/agent.cgi
httemplate/edit/currency_exchange.html [new file with mode: 0755]
httemplate/edit/elements/edit.html
httemplate/edit/part_pkg.cgi
httemplate/edit/process/agent.cgi
httemplate/edit/process/currency_exchange.html [new file with mode: 0644]
httemplate/edit/process/part_pkg.cgi
httemplate/elements/checkboxes-table-name.html
httemplate/elements/checkboxes.html
httemplate/elements/menu.html

index c85e4a5..982c340 100644 (file)
@@ -5,6 +5,7 @@ use Carp;
 use IO::File;
 use File::Basename;
 use MIME::Base64;
+use Locale::Currency;
 use FS::ConfItem;
 use FS::ConfDefaults;
 use FS::Conf_compat17;
@@ -1006,12 +1007,25 @@ sub reason_type_options {
   {
     'key'         => 'currency',
     'section'     => 'billing',
-    'description' => 'Currency',
+    'description' => 'Main accounting currency',
     'type'        => 'select',
     'select_enum' => [ '', qw( USD AUD CAD DKK EUR GBP ILS JPY NZD XAF ) ],
   },
 
   {
+    'key'         => 'currencies',
+    'section'     => 'billing',
+    'description' => 'Additional accepted currencies',
+    'type'        => 'select-sub',
+    'multiple'    => 1,
+    'options_sub' => sub { 
+                           map { $_ => code2currency($_) } all_currency_codes();
+                        },
+    'sort_sub'    => sub ($$) { $_[0] cmp $_[1]; },
+    'option_sub'  => sub { code2currency(shift); },
+  },
+
+  {
     'key'         => 'business-batchpayment-test_transaction',
     'section'     => 'billing',
     'description' => 'Turns on the Business::BatchPayment test_mode flag.  Note that not all gateway modules support this flag; if yours does not, using the batch gateway will fail.',
index 90ced1f..6c12e81 100644 (file)
@@ -121,6 +121,8 @@ if ( -e $addl_handler_use_file ) {
   use HTML::Widgets::SelectLayers 0.07; #should go away in favor of
                                         #selectlayers.html
   use Locale::Country;
+  use Locale::Currency;
+  use Locale::Currency::Format;
   use Business::US::USPS::WebTools::AddressStandardization;
   use Geo::GoogleEarth::Pluggable;
   use LWP::UserAgent;
@@ -341,6 +343,9 @@ if ( -e $addl_handler_use_file ) {
   use FS::part_pkg_msgcat;
   use FS::svc_cable;
   use FS::cable_device;
+  use FS::agent_currency;
+  use FS::currency_exchange;
+  use FS::part_pkg_currency;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index cdbcae0..be35521 100644 (file)
@@ -12,19 +12,19 @@ use vars qw( $AUTOLOAD @ISA @EXPORT_OK $DEBUG
 use Exporter;
 use Carp qw(carp cluck croak confess);
 use Scalar::Util qw( blessed );
+use File::Slurp qw( slurp );
 use File::CounterFile;
-use Locale::Country;
 use Text::CSV_XS;
-use File::Slurp qw( slurp );
 use DBI qw(:sql_types);
 use DBIx::DBSchema 0.38;
+use Locale::Country;
+use Locale::Currency;
+use NetAddr::IP; # for validation
 use FS::UID qw(dbh datasrc driver_name);
 use FS::CurrentUser;
 use FS::Schema qw(dbdef);
 use FS::SearchCache;
 use FS::Msgcat qw(gettext);
-use NetAddr::IP; # for validation
-use Data::Dumper;
 #use FS::Conf; #dependency loop bs, in install_callback below instead
 
 use FS::part_virtual_field;
@@ -1528,6 +1528,7 @@ csv, xls, fixedlength, xml
 
 =cut
 
+use Data::Dumper;
 sub batch_import {
   my $param = shift;
 
@@ -2129,6 +2130,41 @@ sub ut_moneyn {
   $self->ut_money($field);
 }
 
+=item ut_currencyn COLUMN
+
+Check/untaint currency indicators, such as USD or EUR.  May be null.  If there
+is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub ut_currencyn {
+  my($self, $field) = @_;
+  if ($self->getfield($field) eq '') { #can be null
+    $self->setfield($field, '');
+    return '';
+  }
+  $self->ut_currency($field);
+}
+
+=item ut_currency COLUMN
+
+Check/untaint currency indicators, such as USD or EUR.  May not be null.  If
+there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub ut_currency {
+  my($self, $field) = @_;
+  my $value = uc( $self->getfield($field) );
+  if ( code2currency($value) ) {
+    $self->setfield($value);
+  } else {
+    return "Unknown currency $value";
+  }
+
+  '';
+}
+
 =item ut_text COLUMN
 
 Check/untaint text.  Alphanumerics, spaces, and the following punctuation
index 71d84cc..0487186 100644 (file)
@@ -533,6 +533,17 @@ sub tables_hashref {
       'index' => [ ['salesnum'], ['disabled'] ],
     },
 
+    'agent_currency' => {
+      'columns' => [
+        'agentcurrencynum', 'serial', '', '', '', '',
+        'agentnum',            'int', '', '', '', '',
+        'currency',           'char', '',  3, '', '',
+      ],
+      'primary_key' => 'agentcurrencynum',
+      'unique'      => [],
+      'index'       => [ ['agentnum'] ],
+    },
+
     'cust_attachment' => {
       'columns' => [
         'attachnum', 'serial', '', '', '', '',
@@ -2054,6 +2065,31 @@ sub tables_hashref {
       'index'       => [],
     },
 
+    'part_pkg_currency' => {
+      'columns' => [
+        'pkgcurrencynum', 'serial', '',      '', '', '',
+        'pkgpart',           'int', '',      '', '', '',
+        'currency',         'char', '',       3, '', '',
+        'optionname',    'varchar', '', $char_d, '', '', 
+        'optionvalue',      'text', '',      '', '', '', 
+      ],
+      'primary_key' => 'pkgcurrencynum',
+      'unique'      => [ [ 'pkgpart', 'currency', 'optionname' ] ],
+      'index'       => [ ['pkgpart'] ],
+    },
+
+    'currency_exchange' => {
+      'columns' => [
+        'currencyratenum', 'serial', '',    '', '', '',
+        'from_currency',     'char', '',     3, '', '',
+        'to_currency',       'char', '',     3, '', '',
+        'rate',           'decimal', '', '7,6', '', '',
+      ],
+      'primary_key' => 'currencyratenum',
+      'unique'      => [ [ 'from_currency', 'to_currency' ] ],
+      'index'       => [],
+    },
+
     'part_pkg_link' => {
       'columns' => [
         'pkglinknum',  'serial',   '',      '', '', '',
index 9b32209..109343a 100644 (file)
@@ -1,19 +1,18 @@
 package FS::agent;
+use base qw( FS::m2m_Common FS::m2name_Common FS::Record );
 
 use strict;
 use vars qw( @ISA );
-#use Crypt::YAPassGen;
 use Business::CreditCard 0.28;
 use FS::Record qw( dbh qsearch qsearchs );
 use FS::cust_main;
 use FS::cust_pkg;
 use FS::agent_type;
+use FS::agent_currency;
 use FS::reg_code;
 use FS::TicketSystem;
 use FS::Conf;
 
-@ISA = qw( FS::m2m_Common FS::Record );
-
 =head1 NAME
 
 FS::agent - Object methods for agent records
@@ -177,6 +176,31 @@ sub agent_cust_main {
   qsearchs( 'cust_main', { 'custnum' => $self->agent_custnum } );
 }
 
+=item agent_currency
+
+Returns the FS::agent_currency objects (see L<FS::agent_currency>), if any, for
+this agent.
+
+=cut
+
+sub agent_currency {
+  my $self = shift;
+  qsearch('agent_currency', { 'agentnum' => $self->agentnum } );
+}
+
+=item agent_currency_hashref
+
+Returns a hash references of supported additional currencies for this agent.
+
+=cut
+
+sub agent_currency_hashref {
+  my $self = shift;
+  +{ map { $_->currency => 1 }
+       $self->agent_currency
+   };
+}
+
 =item pkgpart_hashref
 
 Returns a hash reference.  The keys of the hash are pkgparts.  The value is
diff --git a/FS/FS/agent_currency.pm b/FS/FS/agent_currency.pm
new file mode 100644 (file)
index 0000000..e387844
--- /dev/null
@@ -0,0 +1,110 @@
+package FS::agent_currency;
+use base qw( FS::Record );
+
+use strict;
+#use FS::Record qw( qsearch qsearchs );
+use FS::agent;
+
+=head1 NAME
+
+FS::agent_currency - Object methods for agent_currency records
+
+=head1 SYNOPSIS
+
+  use FS::agent_currency;
+
+  $record = new FS::agent_currency \%hash;
+  $record = new FS::agent_currency { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::agent_currency object represents an agent's ability to sell
+in a specific non-default currency.  FS::agent_currency inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item agentcurrencynum
+
+primary key
+
+=item agentnum
+
+Agent (see L<FS::agent>)
+
+=item currency
+
+3 letter currency code
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'agent_currency'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('agentcurrencynum')
+    || $self->ut_foreign_key('agentnum', 'agent', 'agentnum')
+    || $self->ut_currency('currency')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::agent>
+
+=cut
+
+1;
+
diff --git a/FS/FS/currency_exchange.pm b/FS/FS/currency_exchange.pm
new file mode 100644 (file)
index 0000000..68832b6
--- /dev/null
@@ -0,0 +1,116 @@
+package FS::currency_exchange;
+use base qw( FS::Record );
+
+use strict;
+#use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::currency_exchange - Object methods for currency_exchange records
+
+=head1 SYNOPSIS
+
+  use FS::currency_exchange;
+
+  $record = new FS::currency_exchange \%hash;
+  $record = new FS::currency_exchange { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::currency_exchange object represents an exchange rate between currencies.
+FS::currency_exchange inherits from FS::Record.  The following fields are
+currently supported:
+
+=over 4
+
+=item currencyratenum
+
+primary key
+
+=item from_currency
+
+from_currency
+
+=item to_currency
+
+to_currency
+
+=item rate
+
+rate
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new exchange rate.  To add the exchange rate to the database, see
+L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'currency_exchange'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid exchange rate.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('currencyratenum')
+    || $self->ut_currency('from_currency')
+    || $self->ut_currency('to_currency')
+    || $self->ut_float('rate') #good enough for untainting
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index 605c84f..67372ac 100644 (file)
@@ -25,6 +25,7 @@ use FS::part_pkg_link;
 use FS::part_pkg_discount;
 use FS::part_pkg_usage;
 use FS::part_pkg_vendor;
+use FS::part_pkg_currency;
 
 $DEBUG = 0;
 $setup_hack = 0;
@@ -177,6 +178,9 @@ records will be inserted.
 If I<options> is set to a hashref of options, appropriate FS::part_pkg_option
 records will be inserted.
 
+If I<part_pkg_currency> is set to a hashref of options (with the keys as
+option_CURRENCY), appropriate FS::part_pkg::currency records will be inserted.
+
 =cut
 
 sub insert {
@@ -251,6 +255,23 @@ sub insert {
     }
   }
 
+  warn "  inserting part_pkg_currency records" if $DEBUG;
+  my %part_pkg_currency = %{ $options{'part_pkg_currency'} || {} };
+  foreach my $key ( keys %part_pkg_currency ) {
+    $key =~ /^(.+)_([A-Z]{3})$/ or next;
+    my $part_pkg_currency = new FS::part_pkg_currency {
+      'pkgpart'     => $self->pkgpart,
+      'optionname'  => $1,
+      'currency'    => $2,
+      'optionvalue' => $part_pkg_currency{$key},
+    };
+    my $error = $part_pkg_currency->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
   unless ( $skip_pkg_svc_hack ) {
 
     warn "  inserting pkg_svc records" if $DEBUG;
@@ -352,6 +373,9 @@ FS::pkg_svc record will be updated.
 If I<options> is set to a hashref, the appropriate FS::part_pkg_option records
 will be replaced.
 
+If I<part_pkg_currency> is set to a hashref of options (with the keys as
+option_CURRENCY), appropriate FS::part_pkg::currency records will be replaced.
+
 =cut
 
 sub replace {
@@ -447,6 +471,34 @@ sub replace {
     }
   }
 
+  #trivial nit: not the most efficient to delete and reinsert
+  warn "  deleting old part_pkg_currency records" if $DEBUG;
+  foreach my $part_pkg_currency ( $old->part_pkg_currency ) {
+    my $error = $part_pkg_currency->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "error deleting part_pkg_currency record: $error";
+    }
+  }
+
+  warn "  inserting new part_pkg_currency records" if $DEBUG;
+  my %part_pkg_currency = %{ $options->{'part_pkg_currency'} || {} };
+  foreach my $key ( keys %part_pkg_currency ) {
+    $key =~ /^(.+)_([A-Z]{3})$/ or next;
+    my $part_pkg_currency = new FS::part_pkg_currency {
+      'pkgpart'     => $new->pkgpart,
+      'optionname'  => $1,
+      'currency'    => $2,
+      'optionvalue' => $part_pkg_currency{$key},
+    };
+    my $error = $part_pkg_currency->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "error inserting part_pkg_currency record: $error";
+    }
+  }
+
+
   warn "  replacing pkg_svc records" if $DEBUG;
   my $pkg_svc = $options->{'pkg_svc'};
   my $hidden_svc = $options->{'hidden_svc'} || {};
@@ -1191,6 +1243,33 @@ sub option {
   '';
 }
 
+=item part_pkg_currency [ CURRENCY ]
+
+Returns all currency options as FS::part_pkg_currency objects (see
+L<FS::part_pkg_currency>), or, if a currency is specified, only return the
+objects for that currency.
+
+=cut
+
+sub part_pkg_currency {
+  my $self = shift;
+  my %hash = ( 'pkgpart' => $self->pkgpart );
+  $hash{'currency'} = shift if @_;
+  qsearch('part_pkg_currency', \%hash );
+}
+
+=item part_pkg_currency_options CURRENCY
+
+Returns a list of option names and values from FS::part_pkg_currency for the
+specified currency.
+
+=cut
+
+sub part_pkg_currency_options {
+  my $self = shift;
+  map { $_->optionname => $_->optionvalue } $self->part_pkg_currency(shift);
+}
+
 =item bill_part_pkg_link
 
 Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
diff --git a/FS/FS/part_pkg_currency.pm b/FS/FS/part_pkg_currency.pm
new file mode 100644 (file)
index 0000000..246abee
--- /dev/null
@@ -0,0 +1,139 @@
+package FS::part_pkg_currency;
+use base qw( FS::Record );
+
+use strict;
+#use FS::Record qw( qsearch qsearchs );
+use FS::part_pkg;
+
+=head1 NAME
+
+FS::part_pkg_currency - Object methods for part_pkg_currency records
+
+=head1 SYNOPSIS
+
+  use FS::part_pkg_currency;
+
+  $record = new FS::part_pkg_currency \%hash;
+  $record = new FS::part_pkg_currency { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_currency object represents an example.  FS::part_pkg_currency inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item pkgcurrencynum
+
+primary key
+
+=item pkgpart
+
+Package definition (see L<FS::part_pkg>).
+
+=item currency
+
+3-letter currency code
+
+=item optionname
+
+optionname
+
+=item optionvalue
+
+optionvalue
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example.  To add the example to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'part_pkg_currency'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('pkgcurrencynum')
+    || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart')
+    || $self->ut_currency('currency')
+    || $self->ut_text('optionname')
+    || $self->ut_textn('optionvalue')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+The author forgot to customize this manpage.
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index 68b4acc..a86683d 100644 (file)
@@ -699,3 +699,9 @@ FS/cable_device.pm
 t/cable_device.t
 FS/h_svc_cable.pm
 t/h_svc_cable.t
+FS/agent_currency.pm
+t/agent_currency.t
+FS/currency_exchange.pm
+t/currency_exchange.t
+FS/part_pkg_currency.pm
+t/part_pkg_currency.t
diff --git a/FS/t/agent_currency.t b/FS/t/agent_currency.t
new file mode 100644 (file)
index 0000000..152e066
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::agent_currency;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/currency_exchange.t b/FS/t/currency_exchange.t
new file mode 100644 (file)
index 0000000..6f8ac1d
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::currency_exchange;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_pkg_currency.t b/FS/t/part_pkg_currency.t
new file mode 100644 (file)
index 0000000..b8654c7
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pkg_currency;
+$loaded=1;
+print "ok 1\n";
index fc9ce54..b9190ec 100755 (executable)
@@ -38,6 +38,10 @@ full offerings (via their type).<BR><BR>
     <TH CLASS="grid" BGCOLOR="#cccccc">Ticketing</TH>
 % } 
 
+% if ( $conf->config('currencies') ) { 
+    <TH CLASS="grid" BGCOLOR="#cccccc">Currencies</TH>
+% } 
+
   <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>Payment Gateway Overrides</FONT></TH>
   <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>Configuration Overrides</FONT></TH>
 </TR>
@@ -361,19 +365,23 @@ Unused
 
           <BR><A HREF="<%$p%>edit/prepay_credit.cgi?agentnum=<% $agent->agentnum %>">Generate cards</A>
         </TD>
-% if ( $conf->config('ticket_system') ) { 
-
 
+% if ( $conf->config('ticket_system') ) { 
           <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
-% if ( $agent->ticketing_queueid ) { 
-
-              Queue: <% $agent->ticketing_queueid %>: <% $agent->ticketing_queue %><BR>
+%         if ( $agent->ticketing_queueid ) { 
+              Queue: <% $agent->ticketing_queueid %>:
+                     <% $agent->ticketing_queue %>
+              <BR>
+%         } 
+          </TD>
 % } 
 
+% if ( $conf->config('currencies') ) { 
+          <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+            <% join('<BR>', sort keys %{ $agent->agent_currency_hashref } ) %>
           </TD>
 % } 
 
-
         <TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
           <TABLE CLASS="inv" CELLSPACING=0 CELLPADDING=0>
 % foreach my $override (
index 7960d7e..50b3eba 100644 (file)
@@ -156,7 +156,9 @@ Setting <b><% $key %></b>
 %     }
 
 %     my %options = &{$config_item->options_sub};
-%     my @options = sort { $a <=> $b } keys %options;
+%     my @options = keys %options;
+%     my $sortsub = $config_item->sort_sub || sub { $a <=> $b };
+%     @options = sort $sortsub @options;
 %     my %saw;
 %     foreach my $value ( @options ) {
 %       local($^W)=0; next if $saw{$value}++;
index b043d1e..2eddd30 100755 (executable)
 % }
 
 </TABLE>
+<BR>
+
+% if ( $conf->config('currencies') ) {
+
+    <FONT CLASS="fsinnerbox-title"><% mt('Currencies') |h %></FONT>
+    <TABLE CLASS="fsinnerbox">
+      <TR>
+        <TD>
+          <& /elements/checkboxes-table-name.html,
+               'link_table' => 'agent_currency',
+               'name_col'   => 'currency',
+               'names_list' => [ map [ $_, {label=>"$_: ".code2currency($_)} ],
+                                   $conf->config('currencies')
+                               ],
+          &>
+        </TD>
+      </TR>
+    </TABLE>
 
+% }
 
 <BR>
+
+
 <INPUT TYPE="submit" VALUE="<% $agent->agentnum ? "Apply changes" : "Add agent" %>">
 
 </FORM>
diff --git a/httemplate/edit/currency_exchange.html b/httemplate/edit/currency_exchange.html
new file mode 100755 (executable)
index 0000000..573ace5
--- /dev/null
@@ -0,0 +1,73 @@
+<& /elements/header.html, 'Exchange rates' &>
+
+<FORM METHOD="POST" ACTION="process/currency_exchange.html">
+
+<& /elements/table-grid.html &>
+% my $bgcolor1 = '#eeeeee';
+%   my $bgcolor2 = '#ffffff';
+%   my $bgcolor = '';
+
+<TR>
+  <TH CLASS="grid" BGCOLOR="#cccccc">From</TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc">Rate</TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc">To</TH>
+</TR>
+
+%foreach my $currency (@currencies) {
+%
+%  if ( $bgcolor eq $bgcolor1 ) {
+%    $bgcolor = $bgcolor2;
+%  } else {
+%    $bgcolor = $bgcolor1;
+%  }
+%
+%  my %hash = ( 'from_currency' => $currency,
+%               'to_currency'   => $to_currency,
+%             );
+%
+%  my $currency_exchange = qsearchs('currency_exchange', \%hash)
+%                         || new FS::currency_exchange   \%hash;
+%
+% $currency_exchange->rate('1.000000') if length($currency_exchange->rate) == 0;
+
+      <TR>
+
+        <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+          <% $currency %>: <% code2currency($currency) %>
+        </TD>
+
+        <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right">
+          <INPUT TYPE      = "text"
+                 NAME      = "<% "$currency-$to_currency" %>"
+                 VALUE     = "<% $currency_exchange->rate %>"
+                 SIZE      = 14
+                 MAXLENGTH = 14
+          >
+        </TD>
+
+        <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+          <% $to_currency %>: <% code2currency($to_currency) %>
+        </TD>
+
+      </TR>
+% } 
+
+    </TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Update rates">
+</FORM>
+
+<& /elements/footer.html &>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $conf = new FS::Conf;
+
+my $to_currency = $conf->config('currency') || 'USD';
+
+my @currencies = sort { $a cmp $b } $conf->config('currencies');
+
+</%init>
index 3e6bd5b..0840829 100644 (file)
@@ -282,6 +282,7 @@ Example:
 %     #text and derivitives
 %     'size'          => $f->{'size'},
 %     'maxlength'     => $f->{'maxlength'},
+%     'prefix'        => $f->{'prefix'},
 %     'postfix'       => $f->{'postfix'},
 %
 %     #textarea
index fadde35..ef9bc22 100755 (executable)
                             'setup_show_zero'  => 'Show zero setup',
                             'recur_fee'        => 'Recurring fee',
                             'recur_show_zero'  => 'Show zero recurring',
+                            ( map { ( "setup_fee_$_" => "Setup fee $_",
+                                      "recur_fee_$_" => "Recurring fee $_",
+                                    );
+                                  }
+                                $conf->config('currencies')
+                            ),
                             'discountnum'      => 'Offer discounts for longer terms',
                             'bill_dst_pkgpart' => 'Include line item(s) from package',
                             'svc_dst_pkgpart'  => 'Include services of package',
                                 value    => 'Y',
                                 disabled => sub { $setup_show_zero_disabled },
                               },
+                              ( map { +{ field => "setup_fee_$_",
+                                         type  => 'text',
+                                         prefix=> currency_symbol($_, SYM_HTML),
+                                         size  => 8,
+                                       }
+                                    }
+                                  sort $conf->config('currencies')
+                              ),
                               { field    => 'freq',
                                 type     => 'part_pkg_freq',
                                 onchange => 'freq_changed',
                                 disabled => sub { $recur_disabled },
                                 onchange => 'recur_changed',
                               },
-
                               { field    => 'recur_show_zero',
                                 type     => 'checkbox',
                                 value    => 'Y',
                                 disabled => sub { $recur_show_zero_disabled },
                               },
+                              ( map { +{ field => "recur_fee_$_",
+                                         type  => 'text',
+                                         prefix=> currency_symbol($_, SYM_HTML),
+                                         size  => 8,
+                                       }
+                                    }
+                                  sort $conf->config('currencies')
+                              ),
 
                               #price plan
                               #setup fee
@@ -460,6 +481,14 @@ my $error_callback = sub {
   $object->set($_ => scalar($cgi->param($_)) )
     foreach (qw( setup_fee recur_fee disable_line_item_date_ranges ));
 
+  foreach my $currency ( $conf->config('currencies') ) {
+    my %part_pkg_currency = $object->part_pkg_currency_options($currency);
+    foreach (qw( setup_fee recur_fee )) {
+      my $param = $_.'_'.$currency;
+      $object->set( $param, $cgi->param($param) );
+    }
+  }
+
   $pkgpart = $object->pkgpart;
 
   &$splice_locale_fields(
@@ -535,6 +564,12 @@ my $edit_callback = sub {
   $object->set($_ => $object->option($_, 1))
     foreach (qw( setup_fee recur_fee disable_line_item_date_ranges ));
 
+  foreach my $currency ( $conf->config('currencies') ) {
+    my %part_pkg_currency = $object->part_pkg_currency_options($currency);
+    $object->set( $_.'_'.$currency, $part_pkg_currency{$_} )
+      foreach keys %part_pkg_currency;
+  }
+
   $pkgpart = $object->pkgpart;
 
   &$splice_locale_fields(
@@ -599,6 +634,12 @@ my $clone_callback = sub {
   $object->set($_ => $options{$_})
     foreach (qw( setup_fee recur_fee disable_line_item_date_ranges ));
 
+  foreach my $currency ( $conf->config('currencies') ) {
+    my %part_pkg_currency = $object->part_pkg_currency_options($currency);
+    $object->set( $_.'_'.$currency, $part_pkg_currency{$_} )
+      foreach keys %part_pkg_currency;
+  }
+
   $recur_disabled = $object->freq ? 0 : 1;
 
   &$splice_locale_fields(
index 034c4cc..5549929 100755 (executable)
@@ -5,6 +5,12 @@
               'process_m2m'      => { 'link_table'   => 'access_groupagent',
                                       'target_table' => 'access_group',
                                     },
+              'process_m2name'   => {
+                      'link_table'  => 'agent_currency',
+                      'name_col'    => 'currency',
+                      'names_list'  => [ $conf->config('currencies') ],
+                      'param_style' => 'link_table.value checkboxes',
+              },
               'edit_ext'         => 'cgi',
               'noerror_callback' => $process_agent_pkg_class,
           )
@@ -14,7 +20,9 @@
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
 
-if ( FS::Conf->new->exists('disable_acl_changes') ) {
+my $conf = new FS::Conf;
+
+if ( $conf->exists('disable_acl_changes') ) {
   errorpage('ACL changes disabled in public demo.');
   die "shouldn't be reached";
 }
diff --git a/httemplate/edit/process/currency_exchange.html b/httemplate/edit/process/currency_exchange.html
new file mode 100644 (file)
index 0000000..1f68522
--- /dev/null
@@ -0,0 +1,36 @@
+%if ( $error ) {
+%  errorpage($error); #also not super ideal
+%} else { #or this
+<% include('/elements/header.html', 'Exchange rates updated') %>
+<% include('/elements/footer.html') %>
+%}
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $conf = new FS::Conf;
+
+my $to_currency = $conf->config('currency') || 'USD';
+
+my @currencies = sort { $a cmp $b } $conf->config('currencies');
+
+#in the best of all possible worlds, i would be a single database transaction
+# but here it isn't terribly important other than offending my sense of elegance
+my $error = '';
+foreach my $currency (@currencies) {
+
+  my %hash = ( 'from_currency' => $currency,
+               'to_currency'   => $to_currency,
+             );
+
+  my $currency_exchange = qsearchs('currency_exchange', \%hash)
+                         || new FS::currency_exchange   \%hash;
+
+  $currency_exchange->rate( $cgi->param("$currency-$to_currency") );
+
+  my $method = $currency_exchange->currencyratenum ? 'replace' : 'insert';
+  $error = $currency_exchange->$method() and last;
+}
+
+</%init>
index 932e33b..3b6562f 100755 (executable)
@@ -115,6 +115,19 @@ my $args_callback = sub {
   push @args, 'options' => \%options;
 
   ###
+  #part_pkg_currency
+  ###
+
+  my %part_pkg_currency = (
+    map { $_ => scalar($cgi->param($_)) }
+      #grep /._[A-Z]{3}$/, #support other options
+      grep /^(setup|recur)_fee_[A-Z]{3}$/,
+        $cgi->param
+  );
+
+  push @args, 'part_pkg_currency' => \%part_pkg_currency;
+
+  ###
   #pkg_svc
   ###
 
index 8ee2f77..957d8ef 100644 (file)
@@ -11,7 +11,7 @@ Example:
    
     'name_col' => 'name_column',
     #or
-    'name_callback' => sub { },
+    #not yet 'name_callback' => sub { },
    
     'names_list' => [ 'value',
                       'other value',
index 69ef18f..ad9d691 100644 (file)
@@ -6,7 +6,7 @@ Example:
 
     # required
    
-    #? 'name_callback' => sub { },
+    #not yet 'name_callback' => sub { },
    
     'names_list' => [ 'value',
                       'other value',
index f784d2f..53fccaf 100644 (file)
@@ -587,8 +587,10 @@ $config_billing{'Billing events'} = [ $fsurl.'browse/part_event.html', 'Billing
 if ( $curuser->access_right('Configuration') ) {
   #$config_billing{'Invoice events'}         = [ $fsurl.'browse/part_bill_event.cgi', 'Deprecated, old-style actions for overdue invoices' ];
   $config_billing{'Invoice templates'}      = [ $fsurl.'browse/invoice_template.html', 'Edit templates for HTML, plaintext and typeset invoices' ];
+  $config_billing{'separator'} = ''; #its a separator!
   $config_billing{'Prepaid cards'}          = [ $fsurl.'search/prepay_credit.html', 'View outstanding cards, generate new cards' ];
   $config_billing{'Call rates and regions'} = [ \%config_billing_rates, 'Manage rate plans, regions and prefixes for VoIP and call billing' ];
+  $config_billing{'separator2'} = ''; #its a separator!
 
   my $config_taxes_name = 'Locales and tax rates'.
                           ( $conf->exists('enable_taxproducts')
@@ -600,6 +602,12 @@ if ( $curuser->access_right('Configuration') ) {
      if $conf->exists('enable_taxproducts');
   $config_billing{'Tax classes'} = [ $fsurl. 'browse/part_pkg_taxclass.html', 'Tax classes' ];
 
+  if ( $conf->config('currencies') ) {
+    $config_billing{'separator3'} = ''; #its a separator!
+    $config_billing{'Exchange rates'} = [ $fsurl.'edit/currency_exchange.html', 'Currency exchange rates' ];
+  }
+
+  $config_billing{'separator4'} = ''; #its a separator!
   $config_billing{'Credit reasons'}  = [ $fsurl.'browse/reason.html?class=R', 'Credit reasons explain why a credit was issued.' ];
   $config_billing{'Credit reason types'}  = [ $fsurl.'browse/reason_type.html?class=R', 'Credit reason types define groups of reasons.' ];
 }