package FS::Record;
+use base qw( Exporter );
use strict;
-use vars qw( $AUTOLOAD @ISA @EXPORT_OK $DEBUG
- %virtual_fields_cache
- $conf $conf_encryption $money_char $lat_lower $lon_upper
- $me
- $nowarn_identical $nowarn_classload
- $no_update_diff $no_check_foreign
- @encrypt_payby
+use vars qw( $AUTOLOAD
+ %virtual_fields_cache %fk_method_cache
+ $money_char $lat_lower $lon_upper
);
-use Exporter;
use Carp qw(carp cluck croak confess);
use Scalar::Util qw( blessed );
use File::Slurp qw( slurp );
use File::CounterFile;
use Text::CSV_XS;
use DBI qw(:sql_types);
-use DBIx::DBSchema 0.38;
+use DBIx::DBSchema 0.43; #0.43 for foreign keys
use Locale::Country;
use Locale::Currency;
use NetAddr::IP; # for validation
use Tie::IxHash;
-@ISA = qw(Exporter);
-
-@encrypt_payby = qw( CARD DCRD CHEK DCHK );
+our @encrypt_payby = qw( CARD DCRD CHEK DCHK );
#export dbdef for now... everything else expects to find it here
-@EXPORT_OK = qw(
+our @EXPORT_OK = qw(
dbh fields hfields qsearch qsearchs dbdef jsearch
str2time_sql str2time_sql_closing regexp_sql not_regexp_sql concat_sql
midnight_sql
);
-$DEBUG = 0;
-$me = '[FS::Record]';
+our $DEBUG = 0;
+our $me = '[FS::Record]';
+
+our $nowarn_identical = 0;
+our $nowarn_classload = 0;
+our $no_update_diff = 0;
+our $no_history = 0;
-$nowarn_identical = 0;
-$nowarn_classload = 0;
-$no_update_diff = 0;
-$no_check_foreign = 0;
+our $no_check_foreign = 1; #well, not inefficiently in perl by default anymore
my $rsa_module;
my $rsa_loaded;
my $rsa_encrypt;
my $rsa_decrypt;
-$conf = '';
-$conf_encryption = '';
+our $conf = '';
+our $conf_encryption = '';
FS::UID->install_callback( sub {
eval "use FS::Conf;";
eval "sub PG_BYTEA { die 'guru meditation #9: calling PG_BYTEA when not running Pg?'; }";
}
+ foreach my $table ( dbdef->tables ) {
+ $fk_method_cache{$table} = fk_methods($table);
+ }
+
} );
=head1 NAME
$error = $record->ut_floatn('column');
$error = $record->ut_number('column');
$error = $record->ut_numbern('column');
+ $error = $record->ut_decimal('column');
+ $error = $record->ut_decimaln('column');
$error = $record->ut_snumber('column');
$error = $record->ut_snumbern('column');
$error = $record->ut_money('column');
my @bind_type = ();
my $dbh = dbh;
foreach my $stable ( @stable ) {
+
+ carp '->qsearch on cust_main called' if $stable eq 'cust_main' && $DEBUG;
+
#stop altering the caller's hashref
my $record = { %{ shift(@record) || {} } };#and be liberal in receipt
my $select = shift @select;
# Check for encrypted fields and decrypt them.
## only in the local copy, not the cached object
if ( $conf_encryption
- && eval 'defined(@FS::'. $table . '::encrypted_fields)' ) {
+ && eval '@FS::'. $table . '::encrypted_fields' ) {
foreach my $record (@return) {
foreach my $field (eval '@FS::'. $table . '::encrypted_fields') {
next if $field eq 'payinfo'
# Check for encrypted fields and decrypt them.
## only in the local copy, not the cached object
if ( $conf_encryption
- && eval 'defined(@FS::'. $table . '::encrypted_fields)' ) {
+ && eval '@FS::'. $table . '::encrypted_fields' ) {
foreach my $record (@return) {
foreach my $field (eval '@FS::'. $table . '::encrypted_fields') {
next if $field eq 'payinfo'
$record->column('value') is a synonym for $record->set('column','value');
+$record->foreign_table_name calls qsearchs and returns a single
+FS::foreign_table record (for tables referenced by a column of this table) or
+qsearch and returns an array of FS::foreign_table records (for tables
+referenced by a column in the foreign table).
+
=cut
# readable/safe
my($self,$value)=@_;
my($field)=$AUTOLOAD;
$field =~ s/.*://;
+
+ confess "errant AUTOLOAD $field for $self (arg $value)"
+ unless blessed($self) && $self->can('setfield');
+
+ #$fk_method_cache{$self->table} ||= fk_methods($self->table);
+ if ( exists($fk_method_cache{$self->table}->{$field}) ) {
+
+ my $fk_info = $fk_method_cache{$self->table}->{$field};
+ my $method = $fk_info->{method} || 'qsearchs';
+ my $table = $fk_info->{table} || $field;
+ my $column = $fk_info->{column};
+ my $foreign_column = $fk_info->{references} || $column;
+
+ eval "use FS::$table";
+ die $@ if $@;
+
+ carp '->cust_main called' if $table eq 'cust_main' && $DEBUG;
+
+ my $pkey_value = $self->$column();
+ my %search = ( $foreign_column => $pkey_value );
+
+ # FS::Record->$method() ? they're actually just subs :/
+ if ( $method eq 'qsearchs' ) {
+ return $pkey_value ? qsearchs( $table, \%search ) : '';
+ } elsif ( $method eq 'qsearch' ) {
+ return $pkey_value ? qsearch( $table, \%search ) : ();
+ } else {
+ die "unknown method $method";
+ }
+
+ }
+
if ( defined($value) ) {
- confess "errant AUTOLOAD $field for $self (arg $value)"
- unless blessed($self) && $self->can('setfield');
$self->setfield($field,$value);
} else {
- confess "errant AUTOLOAD $field for $self (no args)"
- unless blessed($self) && $self->can('getfield');
$self->getfield($field);
}
}
-# efficient
+# efficient (also, old, doesn't support FK stuff)
#sub AUTOLOAD {
# my $field = $AUTOLOAD;
# $field =~ s/.*://;
# }
#}
+sub fk_methods {
+ my $table = shift;
+
+ my %hash = ();
+
+ # foreign keys we reference in other tables
+ foreach my $fk (dbdef->table($table)->foreign_keys) {
+
+ my $method = '';
+ if ( scalar( @{$fk->columns} ) == 1 ) {
+ if ( ! defined($fk->references)
+ || ! @{$fk->references}
+ || $fk->columns->[0] eq $fk->references->[0]
+ ) {
+ $method = $fk->table;
+ } else {
+ #some sort of hint in the table.pm or schema for methods not named
+ # after their foreign table (well, not a whole lot different than
+ # just providing a small subroutine...)
+ }
+
+ if ( $method ) {
+ $hash{$method} = { #fk_info
+ 'method' => 'qsearchs',
+ 'column' => $fk->columns->[0],
+ #'references' => $fk->references->[0],
+ };
+ }
+
+ }
+
+ }
+
+ # foreign keys referenced in other tables to us
+ # (alas. why we're cached. still, might this loop better be done once at
+ # schema load time insetad of every time we AUTOLOAD a method on a new
+ # class?)
+ foreach my $f_table ( dbdef->tables ) {
+ foreach my $fk (dbdef->table($f_table)->foreign_keys) {
+
+ next unless $fk->table eq $table;
+
+ my $method = '';
+ if ( scalar( @{$fk->columns} ) == 1 ) {
+ if ( ! defined($fk->references)
+ || ! @{$fk->references}
+ || $fk->columns->[0] eq $fk->references->[0]
+ ) {
+ $method = $f_table;
+ } else {
+ #some sort of hint in the table.pm or schema for methods not named
+ # after their foreign table (well, not a whole lot different than
+ # just providing a small subroutine...)
+ }
+
+ if ( $method ) {
+ $hash{$method} = { #fk_info
+ 'method' => 'qsearch',
+ 'column' => $fk->columns->[0], #references||column
+ #'references' => $fk->column->[0],
+ };
+ }
+
+ }
+
+ }
+
+ }
+
+ \%hash;
+}
+
=item hash
Returns a list of the column/value pairs, usually for assigning to a new hash.
$self->{'Hash'};
}
+#fallbacks/generics
+
+sub API_getinfo {
+ my $self = shift;
+ +{ ( map { $_=>$self->$_ } $self->fields ),
+ };
+}
+
+sub API_insert {
+ my( $class, %opt ) = @_;
+ my $table = $class->table;
+ my $self = $class->new( { map { $_ => $opt{$_} } fields($table) } );
+ my $error = $self->insert;
+ return +{ 'error' => $error } if $error;
+ my $pkey = $self->pkey;
+ return +{ 'error' => '',
+ 'primary_key' => $pkey,
+ $pkey => $self->$pkey,
+ };
+}
+
=item modified
Returns true if any of this object's values have been modified with set (or via
}
my $h_sth;
- if ( defined dbdef->table('h_'. $table) ) {
+ if ( defined( dbdef->table('h_'. $table) ) && ! $no_history ) {
my $h_statement = $self->_h_statement('insert');
warn "[debug]$me $h_statement\n" if $DEBUG > 2;
$h_sth = dbh->prepare($h_statement) or do {
=item check
Checks custom fields. Subclasses should still provide a check method to validate
-non-custom fields, foreign keys, etc., and call this method via $self->SUPER::check.
+non-custom fields, etc., and call this method via $self->SUPER::check.
=cut
my $file = $param->{file};
my $params = $param->{params} || {};
+ my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
+ my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
+
my( $type, $header, $sep_char,
$fixedlength_format, $xml_format, $asn_format,
- $row_callback, @fields );
+ $parser_opt, $row_callback, @fields );
my $postinsert_callback = '';
$postinsert_callback = $param->{'postinsert_callback'}
? $param->{'format_fixedlength_formats'}{ $param->{'format'} }
: '';
+ $parser_opt =
+ $param->{'format_parser_opts'}
+ ? $param->{'format_parser_opts'}{ $param->{'format'} }
+ : {};
+
$xml_format =
$param->{'format_xml_formats'}
? $param->{'format_xml_formats'}{ $param->{'format'} }
if ( $type eq 'csv' ) {
- my %attr = ( 'binary' => 1, );
- $attr{sep_char} = $sep_char if $sep_char;
- $parser = new Text::CSV_XS \%attr;
+ $parser_opt->{'binary'} = 1;
+ $parser_opt->{'sep_char'} = $sep_char if $sep_char;
+ $parser = Text::CSV_XS->new($parser_opt);
} elsif ( $type eq 'fixedlength' ) {
eval "use Parse::FixedLength;";
die $@ if $@;
- $parser = Parse::FixedLength->new($fixedlength_format);
+ $parser = Parse::FixedLength->new($fixedlength_format, $parser_opt);
- }
- else {
+ } else {
die "Unknown file type $type\n";
}
}
+ if ( $custnum_prefix && $hash{custnum} =~ /^$custnum_prefix(0*([1-9]\d*))$/
+ && length($1) == $custnum_length ) {
+ $hash{custnum} = $2;
+ }
+
#my $table = $param->{table};
my $class = "FS::$table";
'';
}
+=item ut_decimal COLUMN[, DIGITS]
+
+Check/untaint decimal numbers (up to DIGITS decimal places. If there is an
+error, returns the error, otherwise returns false.
+
+=item ut_decimaln COLUMN[, DIGITS]
+
+Check/untaint decimal numbers. May be null. If there is an error, returns
+the error, otherwise returns false.
+
+=cut
+
+sub ut_decimal {
+ my($self, $field, $digits) = @_;
+ $digits ||= '';
+ $self->getfield($field) =~ /^\s*(\d+(\.\d{0,$digits})?)\s*$/
+ or return "Illegal or empty (decimal) $field: ".$self->getfield($field);
+ $self->setfield($field, $1);
+ '';
+}
+
+sub ut_decimaln {
+ my($self, $field, $digits) = @_;
+ $self->getfield($field) =~ /^\s*(\d*(\.\d{0,$digits})?)\s*$/
+ or return "Illegal (decimal) $field: ".$self->getfield($field);
+ $self->setfield($field, $1);
+ '';
+}
+
=item ut_money COLUMN
Check/untaint monetary numbers. May be negative. Set to 0 if null. If there
#warn "msgcat ". \&msgcat. "\n";
#warn "notexist ". \¬exist. "\n";
#warn "AUTOLOAD ". \&AUTOLOAD. "\n";
+ # \p{Word} = alphanumerics, marks (diacritics), and connectors
+ # see perldoc perluniprops
$self->getfield($field)
- =~ /^([\wô \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]\<\>$money_char]+)$/
+ =~ /^([\p{Word} \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]\<\>$money_char]+)$/
or return gettext('illegal_or_empty_text'). " $field: ".
$self->getfield($field);
$self->setfield($field,$1);
sub ut_name {
my( $self, $field ) = @_;
# warn "ut_name allowed alphanumerics: +(sort grep /\w/, map { chr() } 0..255), "\n";
- $self->getfield($field) =~ /^([\w \,\.\-\']+)$/
+ $self->getfield($field) =~ /^([\p{Word} \,\.\-\']+)$/
or return gettext('illegal_name'). " $field: ". $self->getfield($field);
my $name = $1;
$name =~ s/^\s+//;
sub encrypt {
my ($self, $value) = @_;
- my $encrypted;
+ my $encrypted = $value;
if ($conf->exists('encryption')) {
if ($self->is_encrypted($value)) {
sub is_encrypted {
my ($self, $value) = @_;
- # Possible Bug - Some work may be required here....
-
- if ($value =~ /^M/ && length($value) > 80) {
- return 1;
- } else {
- return 0;
- }
+ # could be more precise about it, but this will do for now
+ $value =~ /^M/ && length($value) > 80;
}
=item decrypt($value)
my $column_type = $column_obj->type;
my $nullable = $column_obj->null;
+ utf8::upgrade($value);
+
warn " $table.$column: $value ($column_type".
( $nullable ? ' NULL' : ' NOT NULL' ).
")\n" if $DEBUG > 2;