package Support::Implementation::LicensingIncomeTemplate;
#
# This package creates Licensing Income pseudo sales files.
# If you need to create the License Income Types please
# use the 'LicenseIncomeTemplate'.
# FB11572
#
# 5/20/10 - Initial Release.
# 3/4/11 - Updated date format to include YYYY-MM-DD
# 3/14/11 - Use name of template file as the name of the license income file
# 2/18/15 - Added Excel 2007 Support
# 7/15/15 - Changed to use RSCOMMON.contract_rate_type
# 7/29/15 - Check if contract has specified license income type
# 8/6/15 - Use existing license income file if possible.  Supress duplicate
#    album exceptions if we're doing track searches
# 4/1/16 - Remove comma from amount
# 4/21/16 - Verify album_contract and track_contract relationships, if specified
# 5/16/16 - Corrected contractID bug; round 'amount' values to 4 decimal places
# 5/17/16 - Remove 'amount' from requiredColumns so that we can exception out properly
# 5/19/16 - If track is specified, then check for track contract only if there's
#   a contract and the track's album isn't attached to the contract.
# 9/15/16 - Fixed bug where album contract linkage wasn't being detected.
#   - Use license_file parameter if specified, otherwise default to input filename
# 11/30/16 - Updated debug message for contract not found. Updated blank line detection.
# 8/18/17 - Fixed date logic; MM/DD/YYYYY is no longer valid (see FB19635, FB19901)
# 2/5/18 - Removed extra "S U M M A R Y" data.
# 6/20/18 - Added check for Euro-dates
# 3/19/19 - Added duplicate track detection
# 5/7/19 - If no contract is specified, generate exception if the specified album
#   and/or track is not attached to any contract (RSD-3822).
# 5/24/19 - Allows tracks to be specified even if no track contract exists, but only
#   if there's an album contract to "catch" the license income.  Otherwise the "Track
#   not attached to any contract" will appear (RSD-4027).
# 9/6/19 - Error out if duplicate license income types are detected.
# 9/23/19 - Fixed date format regex (MM-DD-YY followed by zero or more spaces)
# 2/27/20 - Strip TABs out of contract title
# 4/29/20 - Limit new_artist_contract searches to active (deleted=0) contracts.
#
use strict;
use warnings;

use lib '/app/tools/common/lib';
use lib '/app/tools/rps/lib';


use IO::File;
use Data::Dumper;
use Date::Calc;

use Spreadsheet::ParseExcel;

#use Support::Implementation::ImplementationUtil qw(_findTrack _findAlbum report
#   appendString kAlbumNotFound kNonUniqueAlbumName kTrackNotFound);
use Support::Implementation::ImplementationUtil qw( report appendString checkMissingColumn printNull );

use Common::Util qw(clean);

use Common::Assert;
use Common::UTF8;
use Common::RSApp;
use Common::RSMath qw(round);
use Common::Util qw( clean trimspaces);
use Common::CurrencyFormat;

use RPS::DB::Item::NewArtistContract;
use RPS::DB::Item::NewArtistContractTerm;
use RPS::DB::Item::IncomeSource;
use RPS::DB::Item::ContractRateType;
use RPS::DB::Item::ReserveLiquidation;
use RPS::DB::Item::ArtistContractLicenseIncome;

use RPS::DB::Item::Album;
use RPS::DB::Item::Track;
use RPS::DB::Item::AlbumContract;
use RPS::DB::Item::TrackContract;

use RPS::DB::Item::ArtistPayee;
use RPS::DB::Item::Payor;
use RPS::DB::Item::Region;

use RPS::DB::Item::LicenseIncome;
use RPS::DB::Item::LicenseIncomeType;

use Support::Implementation::ExcelReader;
use Support::Implementation::Excel2007Reader;
use Support::Implementation::TabDelimitedReader;

use Support::Implementation::SearchUtil;

use base 'Support::Implementation::Template';


use lib '/app/tools/data_classes/lib';
use File::File;
use File::Sale;
use Client::Service;

use lib '/app/tools/sale_import/lib';

use lib '/app/tools/raptor/lib';
use Raptor::Tracker::LicenseIncome;
use Raptor::DB::Item::Sale;

use lib '/app/tools/rps/lib';
use RPS::File::Sale;


use constant kQuiet  => 0;
use constant kNormal => 1;
use constant kDebug  => 3;
my $gReportLevel = kNormal;

use constant kDuplicateTerm       => 1;
use constant kDefaultTermNotLast  => 2;

binmode STDOUT, ":utf8";

my $hasEuroDates = 0; # set if we detect any Euro dates (DD/MM/YYYY)

#use constant kClassAttributes => qw( name exec_mode client_id );

#-----------------------------------------------------------------------
# templateHeader maps column names to their default column number.
# The actual column number-header name map is stored in columnMap.
# Note(s):
# 1) Column names are case sensitive
# 2) Spaces are ignored; you must manually remove spaces from the names
#    stored in templateHeader.
# 
# TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO
#
# Should modify logic to reject template if the column names don't
# match _exactly_.  See line 78 of ExcelReader.pm (scanExcelFile); this
# is where spaces are removed from the column name.
#-----------------------------------------------------------------------
my %gTemplateHeader = (
   "*Date"              => 0,  # A
   "Units"              => 1,  # B
   "*Amount"            => 2,  # C
   "Catalog-#"          => 3,  # D
   "*Album Name"        => 4,  # E
   "ISRC Code"          => 5,  # F
   "Track Name"         => 6,  # G
   "Client Contract ID" => 7,  # H
   "Contract Title"     => 8,  # I
   "*Income Type"       => 9,  # J
   "Memo"               => 10, # K
);

#--------------------------------------------------------------
# Maps column #'s to a unique key (headername).  The reverse of
# templateHeader, but with actual column #.
#--------------------------------------------------------------
my %gColumnMap = ();
sub parseHeader {
   my $self = shift;
}

#my $app; # app singleton
my $dbo;
my $dbh;
my $cdbo;
my $clientID;
my $execMode;

my %gRegionMap;
my %gIncomeSourceMap;
my %gChannelMap;
my %gPriceLevelMap;
my %gContractRateTypeMap;
my $gCountryCode; # client country code

my $gFileName;

sub new {
   my ( $class, %args ) = @_;
   my $self = bless {}, $class;

   $self->{dbo} = $dbo;

   return $self->_init(%args);
}

sub _init {
   my ($self, %args) = @_;

   $self->SUPER::_init(%args);

   $execMode = $self->exec_mode if ( $self->exec_mode );

   $clientID = $self->client_id;

   my $app = Common::RSApp->new( clientID => $clientID );

   $dbo = Common::RSApp::GetClientDB();
   $dbh = $dbo->DBH;

   $cdbo = Common::RSApp::GetCommonDB();


   #------------------------------
   # Build a list of valid regions
   #------------------------------
   my $sql = "SELECT region_id, name FROM region";
   my $sth = $dbo->DoCmd($sql);
   while( my($id,$name) = $sth->fetchrow_array() ) {
      $gRegionMap{ lc $name } = $id;
   }

   #-------------------------------------
   # Build a list of valid income sources
   #-------------------------------------
   $sql = "SELECT income_source_id, format, name FROM income_source";
   $sth = $cdbo->DoCmd($sql); # RSCOMMON
   while( my($id,$format,$name) = $sth->fetchrow_array() ) {
      my $val = join("\t",$format,$id);
      $gIncomeSourceMap{ lc $name } = $val;
   }

   #-------------------------------
   # Build a list of valid channels
   #-------------------------------
   $sql = "SELECT channel_id, name FROM channel";
   $sth = $dbo->DoCmd($sql);
   while( my($id,$name) = $sth->fetchrow_array() ) {
      $gChannelMap{ lc $name } = $id;
   }
   $gChannelMap{all} = 0;

   #-----------------------------------
   # Build a list of valid price levels
   #-----------------------------------
   $sql = "SELECT price_level_id, name FROM price_level";
   $sth = $cdbo->DoCmd($sql); # RSCOMMON
   while( my($id,$name) = $sth->fetchrow_array() ) {
      $gPriceLevelMap{ lc $name } = $id;
      $gPriceLevelMap{album} = $id if ( $name =~ m/^album/ );
      $gPriceLevelMap{track} = $id if ( $name =~ m/^track/ );
   }
   $gPriceLevelMap{all} = 0;

   #------------------------------------------
   # Build a list of valid contract rate types
   #------------------------------------------
   $sql = "SELECT contract_rate_type_id, name FROM contract_rate_type";
   $sth = $cdbo->DoCmd($sql); # RSCOMMON
   while( my($id,$name) = $sth->fetchrow_array() ) {
      $gContractRateTypeMap{ lc $name } = $id;

      # store synonyms that are used by the template specification 1.1
      $gContractRateTypeMap{ retail } = $id if ( $name =~ m/retail/i );
      $gContractRateTypeMap{ wholesale } = $id if ( $name =~ m/wholesale/i );
      $gContractRateTypeMap{ revenue } = $id if ( $name =~ m/revenue/i );
      $gContractRateTypeMap{ "net revenue" } = $id if ( $name =~ m/revenue/i );
      $gContractRateTypeMap{ fixed } = $id if ( $name =~ m/fixed/i );
   }

   #------------------------------
   # Get the client's country code
   #------------------------------
   $sql = "SELECT country_code FROM client WHERE client_id=$clientID";
   $sth = $cdbo->DoCmd($sql);
   ($gCountryCode) = $sth->fetchrow_array();

   return $self;
}

sub getHeader {
   my $self = shift;
   #shift->{gTemplateHeader};
   return %gTemplateHeader;
}

sub getColumnMap {
   my $self = shift;
   return %gColumnMap;
}

sub getDbo {
   my $self = shift;
   return $dbo;
}

# Reference to SearchUtil object
my $gSearchObj;

my $gClientID;

#--------------------------------------------------------------
# Read-in all of the template data into memory.  Once it's been
# read, try to parse it and
#--------------------------------------------------------------
sub loadMemory {
   my $self = shift;

   my $clientID = $self->client_id;
   $gClientID = $clientID;
   my $app = Common::RSApp->new( clientID => $clientID );
#
   $dbo = Common::RSApp::GetClientDB();
   $dbh = $dbo->DBH;
#
   $cdbo = Common::RSApp::GetCommonDB();

   $gSearchObj = Support::Implementation::SearchUtil->new(
      clientID => $clientID,
   );


   my $fileName = $self->name; # excel input file name

   if( $self->license_file )
   {
      $gFileName = $self->license_file; # license file name
      print("LicensingIncomeTemplate::loadMemory -- using license_file '$gFileName'\n");
   }
   else
   {
      $gFileName = $fileName; # license file name
      print("LicensingIncomeTemplate::loadMemory -- using input_file '$gFileName'\n");
   }

   if( $self->isExcel2003( $fileName ) ) {
      print("LicensingIncomeTemplate::loadMemory -- loading Excel2k3 $fileName into memory\n");

      #--------------------------
      # Read in the 1st worksheet
      #--------------------------
      my %data;
      my $reader = Support::Implementation::ExcelReader->new(
         filename => $fileName,
         data => \%data,
         header => \%gTemplateHeader,
         columnmap => \%gColumnMap,
         tab => 1,
      );
      $reader->scanExcelFile();

      #---------------
      # Parse the data
      #---------------
      _processData(\%data);

      #return %data;
   }
   elsif( $self->isExcel2007( $fileName ) ) {
      print("LicensingIncomeTemplate::loadMemory -- loading Excel2k7 $fileName into memory\n");

      #--------------------------
      # Read in the 1st worksheet
      #--------------------------
      my %data;
      my $reader = Support::Implementation::Excel2007Reader->new(
         filename => $fileName,
         data => \%data,
         header => \%gTemplateHeader,
         columnmap => \%gColumnMap,
         tab => 1,
      );
      $reader->scanExcelFile();

      #---------------
      # Parse the data
      #---------------
      _processData(\%data);

      #return %data;
   } elsif( $self->isTabDelimited( $fileName ) ) {
      report("LicensingIncomeTemplate::loadMemory -- processing tab-delimited file");

      my %data;
      my $reader = Support::Implementation::TabDelimitedReader->new(
         filename => $fileName,
         data => \%data,
         header => \%gTemplateHeader,
         columnmap => \%gColumnMap
      );

      $reader->scanTabbedFile;
      _processData(\%data);

   }
}

#-------------------------------------------------------------------
# _processData is where the real work is done.  It takes the generic
# information stored in the supplied array of hashes and decodes it.
# In this case, it assumes that the supplied data contains contract
# data.
#-------------------------------------------------------------------
sub _processData {

   my($data) = @_;
   my $rows = $data->{rows};

   #-----------------------------------------------------------------------
   # Hash of all contracts seen.  We'll use this hash to detect if template
   # lines aren't grouped together properly.
   #-----------------------------------------------------------------------
   my %seenContractMap = ();

   my %excount = (   # XXX - update
      contract_not_found  => 0,
      album_not_found     => 0,
      track_not_found     => 0,
   );

   #--------------------------------------
   # rowcount keeps track of line counters
   #--------------------------------------
   my %rowcount = (
      rows_failed => 0,
      total       => 0,
   );

   #----------------------------------------------------------
   # entities: if we create any RPS entities, we'll keep track
   # of the totals in this hash
   #----------------------------------------------------------
   my %entities = (
      album_contract => 0,
      track_contract => 0,
   );

   #================================================================
   # We'll use the following variables to keep track of the contract
   # whose terms we are processing.
   #================================================================

   # Keep track of the number of contracts processed
   my $contractCounter = 0;

   #--------------------------------------------------------------------
   # errorCode will hold one or more error messages for the current line
   #--------------------------------------------------------------------
   my $errorCode;

   use constant kColumnDate             => '*Date';
   use constant kColumnUnits            => 'Units';
   use constant kColumnAmount           => '*Amount';

   use constant kColumnRSAlbumID        => 'RS Album ID';
   use constant kColumnCatalogNumber    => 'Catalog-#';
   use constant kColumnAlbumName        => '*Album Name';

   use constant kColumnRSTrackID        => 'RS Track ID';
   use constant kColumnTrackName        => 'Track Name';
   use constant kColumnISRC             => 'ISRC Code';

   use constant kColumnRSContractID     => 'RS Contract ID';
   use constant kColumnContractName     => 'Contract Title';
   use constant kColumnClientContractID => 'Client Contract ID';

   use constant kColumnIncomeType       => '*Income Type';
   use constant kColumnMemo             => 'Memo';

   my @requiredColumns;


   #---------------------------------------------------------------------------
   # If a column is added to requiredColumns, then all rows must have something
   # in that column otherwise the importer will exception out and die.  If you
   # want to be able to catch the error and generate an exception message then
   # don't use # 'requiredColumns' (you'll have to manually check for the
   # missing column).
   #---------------------------------------------------------------------------

   #push @requiredColumns, kColumnPeriodName;
   #push @requiredColumns, kColumnFileName;
   push @requiredColumns, kColumnDate;
#   push @requiredColumns, kColumnAmount;
   #push @requiredColumns, kColumnAlbumName;
   #push @requiredColumns, kColumnIncomeType;


   #---------------------------------
   # Get the client's currency format
   #---------------------------------
   my $sql = "SELECT country_code FROM client WHERE client_id=$clientID";
   my $sth = $cdbo->DoCmd($sql);
   my($cCode) = $sth->fetchrow_array();
   my $currencyFormat = new Common::CurrencyFormat( countryCode => $cCode );
   my $denomination = $currencyFormat->currencyCode();
   #my $conversionRate = $currencyFormat->conversionRate();

   if ( !$denomination ) {
      die("ERROR: Unable to find currency denomination for client($clientID)");
   }

   my $totalRevenue = 0;  # running total
   my $totalUnits   = 0;  # running total
   my $numRecords   = 0;

   #=========================
   #
   # Check if filename exists
   #
   #=========================

   #----------------------------------------------------------------
   # fileObj - reference to a file we create.  This will get created
   # once we start analyzing the template lines.  It will also be
   # referenced _after_ we've processed all of the sales lines.
   #----------------------------------------------------------------
   my $fileObj;

   report("_processData: fileName($gFileName)");
   if ( $gFileName  and '' ne $gFileName ) {

      #------------------------------------------
      # Check if the licensing income file exists
      #------------------------------------------
      my $sql = "SELECT file_id FROM file "
         . "WHERE orig_file_name= ? "
         . "AND period_id = 0 ";
      my $sth = $dbh->prepare($sql);
      $sth->execute($gFileName);

      if ( $sth->rows > 0 ) {
         report("INFO: file '$gFileName' already exists\n");  # TESTING ONLY

         # If the file exists in the open period, then we'll append data to it.
         #
         my($fileID) = $sth->fetchrow_array();
         #$fileObj = RPS::File::File->Lookup( file_id => $fileID );

         $fileObj = RPS::File::File->new( client_id => $clientID, file_id => $fileID );

         die("FATAL ERROR: Unable to get fileobject for existing file $fileID") if( !$fileObj );

         print STDERR "D: Using existing fileID $fileID - ". $fileObj->OrigFileName() . "\n"; # XXX
         report("INFO: Using existing fileID $fileID - ". $fileObj->OrigFileName() );

         # Grab the units, total revenue, and number of records from the existing file;
         # we'll add to these values.
         #
         my $sql2 = "SELECT SUM(units), SUM(total_revenue), COUNT(*) FROM sale WHERE file_id=$fileID";
         my $sth2 = $dbo->DoCmd($sql2);
         ($totalUnits, $totalRevenue, $numRecords) = $sth2->fetchrow_array();
         report("INFO:  FileID $fileID: Current units($totalUnits), revenue($totalRevenue), records($numRecords)");


      } else {
         report("\n>> ------------------------------------------------------------------------------\n>>");
         report(">> WARNING: license income file '$gFileName' does not exist !!!");
         report(">>\n>> ------------------------------------------------------------------------------");

         print STDERR "\n>> ------------------------------------------------------------------------------\n>>\n";
         print STDERR ">> WARNING: license income file '$gFileName' does not exist !!!\n";
         print STDERR ">>\n>> ------------------------------------------------------------------------------\n";

      }

   } else {
      die("FATAL ERROR: Missing License Income filename!!");
   }


   
   #---------------------------------------------------------------------------
   # Loop over each row containing license income information.
   #---------------------------------------------------------------------------


   foreach my $row (@$rows) {


      #-------------------------------------------------------
      # Get all of the template variables.
      # Note: the keys listed are defined in %gTemplateHeader.
      #-------------------------------------------------------
      my $rowid            = $row->{rowid};

      my $date             = $row->{ kColumnDate()             };
      my $units            = $row->{ kColumnUnits()            };
      my $amount           = $row->{ kColumnAmount()           };

      $amount = $row->{ 'Amount' } if( !$amount );

      my $rsAlbumID        = $row->{ kColumnRSAlbumID()        }; # OPTIONAL
      my $catalogNumber    = $row->{ kColumnCatalogNumber()    };
      my $albumName        = $row->{ kColumnAlbumName()        };

      my $rsTrackID        = $row->{ kColumnRSTrackID()        }; # OPTIONAL
      my $trackName        = $row->{ kColumnTrackName()        };
      my $isrc             = $row->{ kColumnISRC()             };

      my $rsContractID     = $row->{ kColumnRSContractID()     }; # OPTIONAL
      my $contractName     = $row->{ kColumnContractName()     };
      my $clientContractID = $row->{ kColumnClientContractID() };

      my $incomeType       = $row->{ kColumnIncomeType()       };
      my $memo             = $row->{ kColumnMemo()             };

      undef $errorCode;


      ++$rowcount{total};

      # Strip trailing spaces
      $catalogNumber    =~ s/\s*$//g if ( $catalogNumber );
      $albumName        =~ s/\s*$//g if ( $albumName );
      $trackName        =~ s/\s*$//g if ( $trackName );
      $isrc             =~ s/\s*$//g if ( $isrc );
      $contractName     =~ s/\s*$//g if ( $contractName );
      $contractName     =~ s/\t//g   if ( $contractName );
      $clientContractID =~ s/\s*$//g if ( $clientContractID );
      $incomeType       =~ s/\s*$//g;  # shouldn't be blank.. ever
      $memo             =~ s/\s*$//g if ( $memo );

      $amount           =~ s/,// if( $amount );
      if( $amount )
      {
         if( $amount =~ /\(/ && $amount =~ /\)/ ) {
            print "*** Detected negative value wrapped in parentheses... fixing...\n"; # XXX
            $amount =~ s/\(//;
            $amount =~ s/\)//;
            $amount *= -1 if( $amount > 0 );
         }
         $amount = trimspaces($amount);
         $amount = round($amount, 4);    # Round to 4 decimal places (this matches the DB schema for license_income.revenue)
         $row->{ kColumnAmount() } = $amount;
      }

      if( (!defined $contractName || '' eq $contractName) &&
          (!defined $albumName    || '' eq $albumName) &&
          (!defined $trackName    || '' eq $trackName) )
      {
          print "Line $rowid is blank -- skipping !!!\n";
          ++$rowcount{skipped};
          next;
      }

      report("#### [$rowid]  contract name(". printNull($contractName) .") "
         . "album(". printNull($albumName) . ") "
         . "cat#(". printNull($catalogNumber) .") : " . Dumper( \%$row ));



      #-----------------------------------
      # Make sure all columns are in place
      #-----------------------------------
      my $s;
      foreach my $c (@requiredColumns) {
         $s = checkMissingColumn( $c, $row, \%excount );
         appendString( $errorCode, $s ) if ( $s );
      }


      if ( $errorCode ) {
         die("ERROR: template is missing the following column(s):\n$errorCode\n");
      }


      #--------------------
      # Validate the amount
      #--------------------

      # NOTE: As of 5/19/10, the UI will allow you to enter negative income,
      # so we don't check for that.  We also don't check if the user entered
      # "too many" decimals.
      if( !$amount or '' eq $amount ) {
         appendString( $errorCode, "Amount missing" );
         ++$excount{amount_missing};
      } elsif ( $amount !~ m/[+-]?(\d+\.\d+|\d+\.|\d.\d+|\d)/ ) {
         appendString( $errorCode, "Invalid amount format" );
         ++$excount{invalid_amount_format};
      }

      

      #-------------------------------------------------------------
      # Validate the date.  It's supposed to be in MM/DD/YYYY format
      #-------------------------------------------------------------
      if ( $date =~ m/\d+\/\d+\/\d\d\d\d$/ )
      {
         my($month,$day,$year) = split("/",$date);
         report("  DEBUG: month($month) day($day) year($year)\n");

         if( !$hasEuroDates ) {
             if( $month > 12 ) {  # first field must be day, not month...
                 $hasEuroDates = 1;
             }
         } else {
             if( $month <= 12 ) {
                 appendString( $errorCode, "Date Format Conflict" ); # can't mix Euro and non-Euro in same file...
                 ++$excount{date_format_conflict};
             }
         }

#         # Convert to SQL-style date YYYY-MM-DD
#         $date = join("-", $year, $month, $day);

         if( !$hasEuroDates ) {
             # Convert to SQL-style date YYYY-MM-DD
             $date = join("-", $year, $month, $day);
         } else {
             # Convert to SQL-style date YYYY-MM-DD
             $date = join("-", $year, $day, $month);
         }

      }
      elsif( $date =~ m/\d\d\d\d-\d+-\d+/ )
      {
         # date already in correct format; do nothing
      }
      elsif( $date =~ /^(\d{1,2})-(\d{1,2})-(\d{2})\s*$/ ||
             $date =~ /^(\d{1,2})\/(\d{1,2})\/(\d{2})\s*$/ ) # MM-DD-YY
      {
         my $_date = sprintf("%04d-%02d-%02d", ( $3 + 2000), $1, $2);
         report("  Converting $date to $_date");
         $date = $_date;
      }
      elsif ( $date =~ /^\d{5}$/ ) {
         my $_dt = Spreadsheet::ParseExcel::Utility::ExcelFmt( "yyyy-mm-dd", $date );
         report("  Converted msft date $date to $_dt");
         $date = $_dt;
      }

      else
      {
         appendString( $errorCode, "Invalid date format" );
         ++$excount{invalid_date_format};
      }

      $row->{ kColumnDate() } = $date;


      #--------------------------------
      # Validate the units (if present)
      #--------------------------------
      if ( $units and $units !~ /^[+-]?\d+$/ ) {
         appendString( $errorCode, "Invalid units" );
         ++$excount{invalid_units};
      }

      #------------------------------------------------------
      # Check for following dependencies:
      #  - if no contract then album must be specified
      #  - if no album/track then contract must be specified
      #
      # TODO: do we need to include track?
      #------------------------------------------------------
      if ( !$rsContractID and !$contractName and !$clientContractID ) {
         if ( !$rsAlbumID and !$catalogNumber and !$albumName
           # and !$rsTrackID and !$trackName and !$isrc
         ) {
            appendString( $errorCode, "No contract, album required");
            ++$excount{no_contract_album_required};
         }
      } elsif ( !$rsAlbumID and !$catalogNumber and !$albumName
            #and !$rsTrackID and !$trackName and !$isrc
         ) {
         if ( !$rsContractID and !$contractName and !$clientContractID ) {
            appendString( $errorCode, "No album, contract required");
            ++$excount{no_album_contract_required};
         }
      }


      #-----------------------------
      #
      # Find the license income type
      #
      #-----------------------------

      my $incomeTypeID; # set to license_income_type.license_income_type_id if found

      if ( !$incomeType or '' eq $incomeType ) {
         appendString( $errorCode, "Missing income type");
         ++$excount{missing_income_type};
      } else {

         # Find income type.  It's possible to have dupes to we query directly vs. using LicenseIncomeType->Lookup
         #
         my $sql = "SELECT license_income_type_id, name FROM license_income_type WHERE name = ?";
         my $sth = $dbh->prepare($sql);
         $sth->execute($incomeType);
         if ( $sth->rows == 1 ) {
            ($incomeTypeID) = $sth->fetchrow_array();
         } elsif( $sth->rows > 1 ) {
            report("ERROR: Duplicate income Type '$incomeType'");
            appendString( $errorCode, "Duplicate license income type");
            ++$excount{duplicate_licensing_income_type_detected};
         } else {
            report("ERROR: Income Type '$incomeType' not found");
            appendString( $errorCode, "Income type not found");
            ++$excount{incometype_not_found};
         }
      }

      #----------------------------------------------------
      #
      # Find the contract if any contract info was provided
      #
      #----------------------------------------------------

      my $artistContractID;  # This will contain the RPS contract ID if contract exists
      my %cArgs;

      if ( $contractName or $clientContractID or $rsContractID ) {
         $cArgs{title} = $contractName if( $contractName and '' ne $contractName );
         $cArgs{client_contract_id} = $clientContractID  if( $clientContractID and '' ne $clientContractID );
         $cArgs{artist_contract_id} = $rsContractID  if( $rsContractID and '' ne $rsContractID );
         $cArgs{deleted}            = 0; # active contracts only


         if ( !$rsContractID ) {

            #--------------------------------------------------------------
            # If the contract title (and optionally the client contract ID)
            # is specified then only one contract must be located.
            #--------------------------------------------------------------
            
            if ( $contractName and '' ne $contractName ) {
               my $sql = "SELECT artist_contract_id FROM new_artist_contract "
                  . "WHERE title=? ";
               $sql .= "AND client_contract_id=? " if ( $clientContractID );
               $sql .= "AND deleted=0"; # active contracts only
               my $sth = $dbh->prepare($sql);
               if ( $clientContractID ) {
                  $sth->execute( $contractName, $clientContractID );
               } else {
                  $sth->execute( $contractName );
               }

               if ( $sth->rows > 1 ) {
                  appendString( $errorCode, "Duplicate contract");
                  ++$excount{duplicate_contract};
               }
            }
         }


         report("DEBUG: looking for contract: ".Dumper(\%cArgs) );

         my $cObj = RPS::DB::Item::NewArtistContract->Lookup( %cArgs );

         if ( !$cObj ) {


            if ( $clientID == 182 ) {


               #---------------------------------------------------------------------
               # 5/21/10 - Because the reserve export didn't generate a template with
               # actual RS contract titles and client contract IDs (it used the CP
               # title and contract code), we won't be able to find the contract if
               # it was split coming into RoyaltyShare.  Per discussion with CB, we
               # can just pick one of the split contracts and attach the reserve
               # there.
               #---------------------------------------------------------------------
               report("ERROR: Contract '$contractName' not found -- time for some fun...");

               die("ERROR: Can't do anything for MOS without client_contract_id ...")
                  if ( !$clientContractID );


               #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
               #
               # FYI - notice the double escape in SQL below; MySQL needs to have
               # the underscore escaped otherwise it treats it as a wildcard for an
               # alphanumeric character.  In this case, we really want to find an
               # underscore, not an alphanum character.
               #
               #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
               my $sql = "SELECT artist_contract_id, client_contract_id "
                  . "FROM new_artist_contract "
                  #. "WHERE title=? "
                  . "WHERE client_contract_id LIKE '$clientContractID\\_%' ";
               $sql .= "AND deleted=0"; # active contracts only
               my $sth = $dbh->prepare($sql);
               #$sth->execute( $contractName );
               $sth->execute();
               if ( $sth->rows > 1 ) {

                  # Pick one

                  my $_clientContractID;

                  ($artistContractID, $_clientContractID) = $sth->fetchrow_array();
                  report("  MOS: Didn't find '$clientContractID', but Found split "
                     . "contract '$_clientContractID' (id=$artistContractID)");

                  #appendString( $errorCode, "Duplicate contract");
                  #++$excount{duplicate_contract};

               } elsif( $sth->rows == 1 ) {
                  die("FATAL_ERROR: Should have found single contract above..");
               } elsif( $sth->rows == 0 ) {
                  die("FATAL_ERROR: Can't find _any_ contracts with title($contractName), "
                     . "clientID($clientContractID), sql = $sql");
               }


            } else {
               my $c = (defined $clientContractID) ? $clientContractID : "";
               report("ERROR: Contract '$contractName' clientContractID($c) not found");
               appendString( $errorCode, "Contract not found");
               ++$excount{contract_not_found};
            }
         } else {
            $artistContractID = $cObj->artist_contract_id;
            report("   Found contractID($artistContractID)");
         }


         # Make sure the license income type is on the specified contract
         #
         if( $artistContractID && $incomeTypeID )
         {
             my $o = RPS::DB::Item::ArtistContractLicenseIncome->Lookup(
                 artist_contract_id => $artistContractID,
                 license_income_type_id => $incomeTypeID,
             );
             if( !$o )
             {
                report("ERROR: Contract '$contractName' (id=$artistContractID) does not have income type '$incomeType'");
                appendString( $errorCode, "Contract not configured with type");
                ++$excount{contract_not_configured_with_type};
             }
         }
      }

      my $albumID;  # set only if valid album info was provided
      my $trackID;  # set only if valid track info was provided

      my $errFlag;  # set only if album/track error is detected

      #-----------------------------------------------------
      #
      # Find the album if any album information was provided
      #
      #-----------------------------------------------------
      if ( ($albumName && '' ne $albumName ) or
           ($catalogNumber && '' ne $catalogNumber) or
           ($rsAlbumID && '' ne $rsAlbumID) ) {

report("Looking for album... "
   . "albumName(" . printNull($albumName) . ") "
   . "catalogNumber(" . printNull($catalogNumber) . ") "
   . "rsAlbumID(" . printNull($rsAlbumID) . ") ");

         if ( $rsAlbumID ) {
            my $aObj = RPS::DB::Item::Album->Lookup(
               album_id => $rsAlbumID,
            );
            if ( $aObj ) {

               $albumID = $aObj->album_id;

               $albumName = $aObj->title;

            } else {
               appendString( $errorCode, "Invalid albumID");
               ++$excount{albumid_not_found};
            }
         } else {

            my %aArgs = (
               album_name => $albumName,
            );
            $aArgs{catalog_number} = $catalogNumber if ( $catalogNumber && '' ne $catalogNumber );

            ($albumID, $errFlag) = $gSearchObj->findAlbum( %aArgs );

            if ( $errFlag ) {
               report("   errFlag($errFlag) ***");
               if ( $errFlag == Support::Implementation::SearchUtil::kAlbumNotFound ) {
                  report("ERROR: findAlbum returned album not found : ". Dumper(\%aArgs) );
                  appendString( $errorCode, "Album not found");
                  ++$excount{album_not_found};
               } elsif ( $errFlag == Support::Implementation::SearchUtil::kNonUniqueAlbumName ) {
                  appendString( $errorCode, "Non-unique album name");
                  ++$excount{nonunique_album_name};
               } else {
                  die("ERROR: _findAlbum returned unknown errorFlag($errFlag)\n");
               }
            } else {
               report("   albumID($albumID)");
            }
         }
      }

report("### Found albumID($albumID)") if ( $albumID );

      
      if( $albumID )
      {
         #-----------------------------------------------------
         #
         # Find the track if any track information was provided
         #
         #-----------------------------------------------------
         my $trackWasSpecified; # set if any track info in template

         if ( ($trackName && '' ne $trackName ) or
              ($isrc && '' ne $isrc) or
              ($rsTrackID && '' ne $rsTrackID) )
         {

   report("Looking for track... "
      . "trackName(" . printNull($trackName) . ") "
      . "isrc(" . printNull($isrc) . ") "
      . "rsTrackID(" . printNull($rsTrackID) . ") ");

            $trackWasSpecified = 1;

            #---------------
            # Find the track
            #---------------
            if ( $rsTrackID ) {
               my $tObj = RPS::DB::Item::Track->Lookup(
                  track_id => $rsTrackID,
               );
               if ( $tObj ) {
                  $trackID = $tObj->track_id;
               } else {
                  appendString( $errorCode, "Invalid trackID");
                  ++$excount{trackid_not_found};
               }
            } else {

               my %tArgs = (
                  track_name => $trackName,
                  album_name => $albumName,
               );
               $tArgs{catalog_number} = $catalogNumber if ( $catalogNumber && '' ne $catalogNumber );
               $tArgs{isrc} = $isrc if ( $isrc && '' ne $isrc );

               ($trackID, $errFlag) = $gSearchObj->findTrack( %tArgs );

               if ( $errFlag ) {
                  report("   errFlag($errFlag) ***");
                  if ( $errFlag == Support::Implementation::SearchUtil::kTrackNotFound ) {
                     report("ERROR: findTrack returned track not found : ". Dumper(\%tArgs) );
                     appendString( $errorCode, "Track not found");
                     ++$excount{track_not_found};
                  } elsif ( $errFlag == Support::Implementation::SearchUtil::kDuplicateTrack ) {
                     report("ERROR: findTrack returned duplicate track: ". Dumper(\%tArgs) );
                     appendString( $errorCode, "Duplicate track");
                     ++$excount{duplicate_track};
                  } elsif ( $errFlag == Support::Implementation::SearchUtil::kAlbumNotFound ) {
                     report("ERROR: findTrack returned album not found -- ignoring error");
                  } elsif ( $errFlag == Support::Implementation::SearchUtil::kNonUniqueAlbumName ) {
                     report("ERROR: findTrack returned non-unique album name -- ignoring error");
                  } else {
                     die("ERROR: _findTrack returned unknown errorFlag($errFlag)\n");
                  }
               } else {
                  report("   trackID($trackID)");
               }
            }
         }#trackID

         report("### Found trackID($trackID)") if ( $trackID );


         # If a contract was specified, check for contract linkages at the album or track level.
         #
         if( $artistContractID )
         {
            # Check for album linkage
            #
            my $aObj = RPS::DB::Item::AlbumContract->Lookup(
               album_id => $albumID,
               artist_contract_id => $artistContractID,
            );

            # Check for track linkage if we don't have an album linkage
            #
            if( $trackID )
            {
               if( !$aObj )
               {
                  my $tObj = RPS::DB::Item::TrackContract->Lookup(
                     track_id => $trackID,
                     artist_contract_id => $artistContractID,
                  );

                  if( !$tObj )
                  {
                     report("ERROR: Neither album $albumID or track $trackID attached to contract $artistContractID" );
                     appendString( $errorCode, "Album/Track not attached to contract");
                     ++$excount{no_album_or_track_contract};
                  }
               }

               # XXX - Is having an album linkage sufficient if track info was specified?

            }
            else
            {
               if( !$trackWasSpecified )
               {
                  if( !$aObj )
                  {
                     report("ERROR: album $albumID not attached to contract $artistContractID" );
                     appendString( $errorCode, "Album not attached to contract");
                     ++$excount{no_album_contract};
                  }
               }
               else
               {
                  # Track was in the template, but wasn't found
                  report("ERROR: Neither album $albumID or track (not found) attached to contract $artistContractID" );
                  appendString( $errorCode, "Album/Track not attached to contract");
                  ++$excount{no_album_or_track_contract__track_not_found};
               }
            }
         } else {
            # If no contract was specified; make sure we have album and/or track linkages to _something_.
            # If we don't have any linkages, then this will causes problems when trying to edit the
            # license income file through the UI.  See RSD-3822.
            #
            my $numAlbumContracts;
            my $numTrackContracts;

            if ( $albumID ) {
                print ">> Checking for album contracts ...\n";
                my $sql = "SELECT album_contract_id, artist_contract_id FROM album_contract WHERE album_id=$albumID";
                my $sth = $dbo->DoCmd($sql);
                $numAlbumContracts = $sth->rows;

                print "  >> Found $numAlbumContracts album contract(s) ...\n";

                # Check all contracts and make sure at least one is attached to the license income type (incomeTypeID)

                my $hasIncomeType;

                while( my($acID, $contractID) = $sth->fetchrow_array() ) {

                    my $o = RPS::DB::Item::ArtistContractLicenseIncome->Lookup(
                        artist_contract_id     => $contractID,
                        license_income_type_id => $incomeTypeID,
                    );

                    $hasIncomeType = 1 if ( $o );
                }

                if ( !$hasIncomeType ) {
                   # Generate error if no track contracts to check, otherwise
                   # wait till we check the track contracts
                   if ( !$trackID ) {
                       report("ERROR: None of the contracts attached to albumID $albumID has income type $incomeTypeID");
                       appendString( $errorCode, "No matching album contracts with income type");
                       ++$excount{no_album_contracts_with_income_type};
                   }
                }


            }

            if ( $trackID ) {
               print ">> Checking for track contracts ...\n";
               my $sql = "SELECT track_contract_id, artist_contract_id FROM track_contract WHERE track_id=$trackID";
               my $sth = $dbo->DoCmd($sql);
               $numTrackContracts = $sth->rows;

               if ( $numTrackContracts == 0 ) {
                  print "  >> No track contracts found ...\n";
                  # This is only an issue if we don't have an album contract; the UI allows tracks to be entered
                  # on license income even if there are no track contracts (must have an album contract, though).
                  # Addresses exceptions noticed in RSD-4027.
                  #
                  if ( $numAlbumContracts == 0 ) {
                      report("ERROR: Track '$trackName' (trackID $trackID) not attached to any contract, and "
                         . "albumID $albumID has no album contract(s)");
                      appendString( $errorCode, "Album and Track not attached to any contract");
                      ++$excount{album_and_track_not_attached_to_any_contract};
                  } else {
                      report("INFO: Track '$trackName' (trackID $trackID) not attached to any contract, "
                         . "albumID $albumID has $numAlbumContracts album contract(s)");
                  }
               } else {
                   print "  >> Found $numTrackContracts track contract(s) ...\n";

                   # Check all contracts and make sure at least one is attached to the license income type (incomeTypeID)

                   my $hasIncomeType;

                   while( my($tcID, $contractID) = $sth->fetchrow_array() ) {

                       my $o = RPS::DB::Item::ArtistContractLicenseIncome->Lookup(
                           artist_contract_id     => $contractID,
                           license_income_type_id => $incomeTypeID,
                       );

                       $hasIncomeType = 1 if ( $o );
                   }

                   if ( !$hasIncomeType ) {
                       report("ERROR: None of the contracts attached to trackID $trackID has income type $incomeTypeID");
                       appendString( $errorCode, "No matching track contracts with income type");
                       ++$excount{no_track_contracts_with_income_type};
                   } else {
                       print "    >> Found at least one track contract attached to license income !!!\n";
                   }

               }

            } else {

               if ( $numAlbumContracts == 0 ) {
                  report("ERROR: Album $albumID not attached to any contract");
                  appendString( $errorCode, "Album not attached to any contract");
                  ++$excount{album_not_attached_to_any_contract};
               }

            }
         }
      }#albumID



      #----------------------------------------
      # Stop processing if there are any errors
      #----------------------------------------
      if ( $errorCode && '' ne $errorCode ) {
         report("DEBUG: skipping row with error: $errorCode");
         $row->{'error-code'} = $errorCode if ( $errorCode && '' ne $errorCode );
         ++$rowcount{rows_failed};
         next;
      }

      if ( $execMode ) {


         my $nativeCurrency = Common::Client::Current()->Locale()->currencyFormat()->currencyCode();

         #---------------------------------------------------------
         # fileID will be the file_id of the file representing the
         # current License Income File.  The fileID will be created
         # only once regardless of the number of sales lines in the
         # template.  Once created, subsequent sales line will just
         # refer to the newly-created file_id rather than creating
         # a new one.
         #---------------------------------------------------------
         my $fileID;

         if ( !$fileObj ) {
            report("#### Creating license income file '$gFileName'\n");

            $fileObj = RPS::File::File->new( client_id => $clientID );
            $fileObj->PeriodID(0);
            $fileObj->TypeID( File::File::FILETYPE_LICENSE_INCOME );     # License Income

            $fileObj->FileStatus( File::File::STATUS_OPEN ); # Open

            $fileObj->Physical(1);   # Physical

            $fileObj->CurrencyCode($nativeCurrency);

            $fileObj->OrigFileName($gFileName);

            $fileObj->Save() or die "failed to save to db\n";

            $fileID = $fileObj->FileID;
            report("  Created file $fileID (name='$gFileName')");
#
         } else {
            $fileID = $fileObj->FileID;
            report("  #### Using existing License Income File $fileID");
         }

         #----------------
         # Create the sale
         #----------------
         #report("  ## Create the sale..");


         my $saleID;  # will contain the sale_id that we create
         my %saleArgs = (
            currency_code => $nativeCurrency,
            date_begin    => $date,
            date_end      => $date,
            file_id       => $fileID,
            product_type  => RPS::File::Sale::TYPE_LICENSE_INCOME,
            #units         => $units,
            total_revenue => $amount,
            #product_type  => 'L',
            country_code  => $gCountryCode,
            #conversion_rate => 1,
         );
#
         $saleArgs{units} = $units if ( $units );

         #my $saleObj = File::Sale->new( \%saleArgs );
         #$saleObj->Save() or die("failed to save sale to db");
         #my $saleID = $saleObj->sale_id;
         #report("    Created sale $saleID :". Dumper(\%saleArgs));

         #my $sale = Raptor::DB::Item::Sale->Create( %saleArgs );
         #my $sale = Raptor::DB::Item::Sale->Create();

         my $sale = RPS::File::Sale->new( dbo => $dbo );
         $sale->CurrencyCode( $saleArgs{$nativeCurrency} );
         $sale->DateBegin(    $saleArgs{date_begin} );
         $sale->DateEnd(      $saleArgs{date_end} );
         $sale->FileID(       $saleArgs{file_id} );
         $sale->ProductType(  $saleArgs{product_type} );
         $sale->Units(        $saleArgs{units} ) if ( $saleArgs{units} );
         $sale->TotalRevenue( $saleArgs{total_revenue} );
         $sale->CountryCode(  $saleArgs{country_code} );

         #$sale->currency_code($denomination);
         #$sale->country_code($gCountryCode);
         ##$sale->conversion_rate(1);  -- default is 1
         die("unable to create sale?") if ( !$sale );
         $sale->Save();

         $saleID = $sale->SaleID();
         report("  ## Created sale $saleID : ".Dumper(\%saleArgs));


         #--------------------------------
         # Create the LicenseIncome record
         #--------------------------------
         #report("TODO: Create the LicenseIncome object");

         #------------------------------
         # Build the license income file
         #------------------------------
         my %args = (
            file_id                => $fileID,
            sale_id                => $saleID,
            date                   => $date,
            #units                  => $units,
            revenue                => $amount,
            contract_id            => $artistContractID,
            license_income_type_id => $incomeTypeID,
            memo                   => $memo,
         );
         $args{units}    = $units if ( $units );
         $args{track_id} = $trackID if ( $trackID );
         $args{album_id} = $albumID if ( $albumID );

         my $o = RPS::DB::Item::LicenseIncome->Create( %args );
         $o->save();
         my $incomeID = $o->license_income_id;
         report("  ## Created license_income $incomeID : ". Dumper(\%args));
      }


      $totalUnits += $units if ( $units );
      $totalRevenue += $amount;

      report("    Added amount($amount), totalRevenue = $totalRevenue");
      report("    Added units($units), totalUnits = $totalUnits ") if ( $units );
      ++$numRecords;

      #----------------------------------------------------------------------
      # If any errors occurred during the album/track creation, log them here
      #----------------------------------------------------------------------
      if ( $errorCode && '' ne $errorCode ) {
         $row->{'error-code'} = $errorCode if ( $errorCode && '' ne $errorCode );
         ++$rowcount{rows_failed};
      }

      
      ++$contractCounter;
   }# template row loop

   # Update totals on file..
   report("DEBUG: totalUnits($totalUnits) totalRevenue($totalRevenue) #records($numRecords)");
   if ( $fileObj ) {
      if ( $execMode ) {

         #-------------------------------------
         # Update the totals on the file record
         #-------------------------------------
         $fileObj->Units($totalUnits);
         $fileObj->Revenue($totalRevenue);
         $fileObj->Records($numRecords);
         $fileObj->Save();



      } else {
         report("  Non-exec mode, skipping summary information for file");
      }
      
   }

   report("DONE reading template rows...");

   _showExceptions( $rows );

   #-----------------------
   # Show import statistics
   #-----------------------
   report("##### S U M M A R Y #####");
#   print STDERR "##### S U M M A R Y #####\n";
   report("Entities Created:");
#   print STDERR "Entities Created:\n";
   foreach my $c (keys %entities) {
      my $v = $entities{$c};
      printf("%30s %6d\n", $c, $v);
#      printf(STDERR "%30s %6d\n", $c, $v);
   }
   report("Exceptions:");
   print STDERR "Exceptions:\n";
   my $totalExceptions = 0;
   my $totalRows = (scalar @$rows);

   foreach my $c (keys %excount) {
      my $v = $excount{$c};
      printf("%30s %6d\n", $c, $v);
      $totalExceptions += $v;
   }
   printf("%30s %s\n", " ", "-------" );
   printf("%30s %6d\n", "Total Exceptions", $totalExceptions );
   printf("%30s %6d\n", "Total Rows", $totalRows );
   report(" ");

   report("Row Processing Summary:");
   foreach my $c (sort{ $a cmp $b } keys %rowcount) {
      my $v = $rowcount{$c};
      printf("%30s %6d\n", $c, $v);
   }

   print STDERR ">>>\n";
   if (!$execMode)
   {
       print STDERR ">>> Test complete.  Run 'make import' to commit changes\n";
   }
   else
   {
       print STDERR ">>> Import complete.  Attach exceptions report to FogBugz case.\n";
   }
#   print STDERR ">>>\n\n";

}#_processData

sub _showExceptions {
   my ( $rows ) = @_;

   #---------------------------
   # Build the exception header
   #---------------------------
   my @header;
   for( my $i = 0; $i < (keys %gColumnMap); $i++ ) {
      # Get the key at the specified column
      my $v = $gColumnMap{$i};
      next if ( !$v ); # strip blank columns
      #----------------------------------------------------------
      # Do _not_ push the "Error Code" or "import-status" columns
      # if they were in the original template.  We'll re-create
      # them on the fly.
      #----------------------------------------------------------
      next if ( ("Error Code" eq $v) || ("import-status") eq $v );
      #
      push @header, $v;
   }
   #
   report("STATUS:\t".join("\t", @header, "Error Code", "import-status"));
   #--------------------------------------
   # Now dump out the rows that had errors
   #--------------------------------------
   foreach my $row (@$rows) {
      my $rowid = $row->{rowid};
      my $errorCode = $row->{'error-code'};
      my $warningCode = $row->{'warning-code'};

      my $albumContractID = $row->{'rs-albumcontract-id'};
      my $trackContractID = $row->{'rs-trackcontract-id'};
      #my $licenseID = $row->{'rs-license-id'} || '---';
   
      #----------------------------------------
      # Output the row data in the proper order
      #----------------------------------------
      my @obuf;
      for( my $i = 0; $i < (keys %gColumnMap); $i++ ) {
         # Get the key at the specified column
         my $v = $gColumnMap{$i};
         next if ( !$v ); # strip blank columns
         my $val = ($row->{$v}) ? $row->{$v} : '';
         #----------------------------------------------------------
         # Do _not_ push the "Error Code" or "import-status" columns
         # if they were in the original template.  We'll re-create
         # these columns below.
         #----------------------------------------------------------
         next if ( ("Error Code" eq $v) || ("import-status") eq $v );
   
         push @obuf, $val;
      }
   
      my $importStatus;
      my $ecString;

      if ( $errorCode ) {
         $importStatus = "__FAIL__";
         $ecString = ($errorCode) ? $errorCode : '';
      } else {
         $importStatus = "___XXX___";
         $importStatus = "albumcontract($albumContractID)" if ( $albumContractID );
         $importStatus = "trackcontract($trackContractID)" if ( $trackContractID );
         $ecString = ($warningCode) ? $warningCode : '';
      }

      my $zzbuf = join("\t", "STATUS:", @obuf, $ecString, $importStatus);
      report($zzbuf);
   }
} #_showExceptions

1;
