[freeside-commits] branch master updated. a2c63a59734369fdda2073349c164a2649af8e10

Ivan ivan at 420.am
Sat Jun 8 01:50:00 PDT 2013


The branch, master has been updated
       via  a2c63a59734369fdda2073349c164a2649af8e10 (commit)
       via  e96a2a6fd3a8885b0fb035ecc55bdf50dbe5a4aa (commit)
      from  0f21021fea8f99d28b4507c3cffa55cbdd6f110d (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit a2c63a59734369fdda2073349c164a2649af8e10
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Sat Jun 8 01:31:53 2013 -0700

    multi-currency, RT#21565

diff --git a/FS/FS.pm b/FS/FS.pm
index 4b2e527..076f80b 100644
--- a/FS/FS.pm
+++ b/FS/FS.pm
@@ -237,6 +237,10 @@ L<FS::part_pkg> - Package definition class
 
 L<FS::part_pkg_msgcat> - Package definition localization class
 
+L<FS::part_pkg_currency> - Package definition local currency prices
+
+L<FS::currency_exchange> - Currency exchange rates
+
 L<FS::part_pkg_link> - Package definition link class
 
 L<FS::part_pkg_taxclass> - Tax class class
@@ -276,6 +280,8 @@ L<FS::sales> - Sales person class
 
 L<FS::agent> - Agent (reseller) class
 
+L<FS::agent_currency> - Agent (reseller) currency class
+
 L<FS::agent_pkg_class> - Agent (reseller) package class commission class
 
 L<FS::agent_type> - Agent type class

commit e96a2a6fd3a8885b0fb035ecc55bdf50dbe5a4aa
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Sat Jun 8 01:30:52 2013 -0700

    multi-currency, RT#21565

diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index c85e4a5..982c340 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -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.',
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 90ced1f..6c12e81 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -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 ) {
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index cdbcae0..be35521 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -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
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 71d84cc..0487186 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -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',   '',      '', '', '',
diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm
index 9b32209..109343a 100644
--- a/FS/FS/agent.pm
+++ b/FS/FS/agent.pm
@@ -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;
 
- at 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
index 0000000..e387844
--- /dev/null
+++ b/FS/FS/agent_currency.pm
@@ -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
index 0000000..68832b6
--- /dev/null
+++ b/FS/FS/currency_exchange.pm
@@ -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;
+
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 605c84f..67372ac 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -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
index 0000000..246abee
--- /dev/null
+++ b/FS/FS/part_pkg_currency.pm
@@ -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;
+
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 68b4acc..a86683d 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -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
index 0000000..152e066
--- /dev/null
+++ b/FS/t/agent_currency.t
@@ -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
index 0000000..6f8ac1d
--- /dev/null
+++ b/FS/t/currency_exchange.t
@@ -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
index 0000000..b8654c7
--- /dev/null
+++ b/FS/t/part_pkg_currency.t
@@ -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";
diff --git a/httemplate/browse/agent.cgi b/httemplate/browse/agent.cgi
index fc9ce54..b9190ec 100755
--- a/httemplate/browse/agent.cgi
+++ b/httemplate/browse/agent.cgi
@@ -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 (
diff --git a/httemplate/config/config.cgi b/httemplate/config/config.cgi
index 7960d7e..50b3eba 100644
--- a/httemplate/config/config.cgi
+++ b/httemplate/config/config.cgi
@@ -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}++;
diff --git a/httemplate/edit/agent.cgi b/httemplate/edit/agent.cgi
index b043d1e..2eddd30 100755
--- a/httemplate/edit/agent.cgi
+++ b/httemplate/edit/agent.cgi
@@ -170,9 +170,30 @@
 % }
 
 </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
index 0000000..573ace5
--- /dev/null
+++ b/httemplate/edit/currency_exchange.html
@@ -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>
diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html
index 3e6bd5b..0840829 100644
--- a/httemplate/edit/elements/edit.html
+++ b/httemplate/edit/elements/edit.html
@@ -282,6 +282,7 @@ Example:
 %     #text and derivitives
 %     'size'          => $f->{'size'},
 %     'maxlength'     => $f->{'maxlength'},
+%     'prefix'        => $f->{'prefix'},
 %     'postfix'       => $f->{'postfix'},
 %
 %     #textarea
diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi
index fadde35..ef9bc22 100755
--- a/httemplate/edit/part_pkg.cgi
+++ b/httemplate/edit/part_pkg.cgi
@@ -51,6 +51,12 @@
                             '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',
@@ -118,6 +124,14 @@
                                 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',
@@ -127,12 +141,19 @@
                                 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(
diff --git a/httemplate/edit/process/agent.cgi b/httemplate/edit/process/agent.cgi
index 034c4cc..5549929 100755
--- a/httemplate/edit/process/agent.cgi
+++ b/httemplate/edit/process/agent.cgi
@@ -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
index 0000000..1f68522
--- /dev/null
+++ b/httemplate/edit/process/currency_exchange.html
@@ -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>
diff --git a/httemplate/edit/process/part_pkg.cgi b/httemplate/edit/process/part_pkg.cgi
index 932e33b..3b6562f 100755
--- a/httemplate/edit/process/part_pkg.cgi
+++ b/httemplate/edit/process/part_pkg.cgi
@@ -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
   ###
 
diff --git a/httemplate/elements/checkboxes-table-name.html b/httemplate/elements/checkboxes-table-name.html
index 8ee2f77..957d8ef 100644
--- a/httemplate/elements/checkboxes-table-name.html
+++ b/httemplate/elements/checkboxes-table-name.html
@@ -11,7 +11,7 @@ Example:
    
     'name_col' => 'name_column',
     #or
-    'name_callback' => sub { },
+    #not yet 'name_callback' => sub { },
    
     'names_list' => [ 'value',
                       'other value',
diff --git a/httemplate/elements/checkboxes.html b/httemplate/elements/checkboxes.html
index 69ef18f..ad9d691 100644
--- a/httemplate/elements/checkboxes.html
+++ b/httemplate/elements/checkboxes.html
@@ -6,7 +6,7 @@ Example:
 
     # required
    
-    #? 'name_callback' => sub { },
+    #not yet 'name_callback' => sub { },
    
     'names_list' => [ 'value',
                       'other value',
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index f784d2f..53fccaf 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -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.' ];
 }

-----------------------------------------------------------------------

Summary of changes:
 FS/FS.pm                                       |    6 +
 FS/FS/Conf.pm                                  |   16 +++-
 FS/FS/Mason.pm                                 |    5 +
 FS/FS/Record.pm                                |   44 +++++++-
 FS/FS/Schema.pm                                |   36 ++++++
 FS/FS/agent.pm                                 |   30 +++++-
 FS/FS/agent_currency.pm                        |  110 +++++++++++++++++++
 FS/FS/currency_exchange.pm                     |  116 ++++++++++++++++++++
 FS/FS/part_pkg.pm                              |   79 ++++++++++++++
 FS/FS/part_pkg_currency.pm                     |  139 ++++++++++++++++++++++++
 FS/MANIFEST                                    |    6 +
 FS/t/agent_currency.t                          |    5 +
 FS/t/currency_exchange.t                       |    5 +
 FS/t/part_pkg_currency.t                       |    5 +
 httemplate/browse/agent.cgi                    |   20 +++-
 httemplate/config/config.cgi                   |    4 +-
 httemplate/edit/agent.cgi                      |   21 ++++
 httemplate/edit/currency_exchange.html         |   73 +++++++++++++
 httemplate/edit/elements/edit.html             |    1 +
 httemplate/edit/part_pkg.cgi                   |   43 +++++++-
 httemplate/edit/process/agent.cgi              |   10 ++-
 httemplate/edit/process/currency_exchange.html |   36 ++++++
 httemplate/edit/process/part_pkg.cgi           |   13 +++
 httemplate/elements/checkboxes-table-name.html |    2 +-
 httemplate/elements/checkboxes.html            |    2 +-
 httemplate/elements/menu.html                  |    8 ++
 26 files changed, 816 insertions(+), 19 deletions(-)
 create mode 100644 FS/FS/agent_currency.pm
 create mode 100644 FS/FS/currency_exchange.pm
 create mode 100644 FS/FS/part_pkg_currency.pm
 create mode 100644 FS/t/agent_currency.t
 create mode 100644 FS/t/currency_exchange.t
 create mode 100644 FS/t/part_pkg_currency.t
 create mode 100755 httemplate/edit/currency_exchange.html
 create mode 100644 httemplate/edit/process/currency_exchange.html




More information about the freeside-commits mailing list