+sub save_report {
+  my ($self, %params) = @_;
+
+  if ($self->worker->is_multiplexed) {
+    return $self->save_report_multi(%params);
+  } else {
+    return $self->save_report_single(%params);
+  }
+}
+
+sub save_report_single {
+  my ($self, %params) = @_;
+
+  $self->track_progress(phase => 'building report', progress => 0);
+
+  my $clone_profile = $self->profile->clone_and_reset_deep;
+  $clone_profile->save; # weird bug. if this isn't saved before adding it to the report, it will default back to the last profile.
+
+  my $report = SL::DB::CsvImportReport->new(
+    session_id => $params{session_id},
+    profile    => $clone_profile,
+    type       => $self->type,
+    file       => '',
+    numrows    => scalar @{ $self->data },
+    numheaders => 1,
+    test_mode  => $params{test} ? 1 : 0,
+  );
+
+  $report->save(cascade => 1) or die $report->db->error;
+
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
+
+    my $query  = 'INSERT INTO csv_import_report_rows (csv_import_report_id, col, row, value) VALUES (?, ?, ?, ?)';
+    my $query2 = 'INSERT INTO csv_import_report_status (csv_import_report_id, row, type, value) VALUES (?, ?, ?, ?)';
+
+    my $sth = $dbh->prepare($query);
+    my $sth2 = $dbh->prepare($query2);
+
+    # save headers
+    my (@headers, @info_methods, @raw_methods, @methods);
+
+    for my $i (0 .. $#{ $self->info_headers->{headers} }) {
+      next unless         $self->info_headers->{used}->{ $self->info_headers->{methods}->[$i] };
+      push @headers,      $self->info_headers->{headers}->[$i];
+      push @info_methods, $self->info_headers->{methods}->[$i];
+    }
+    for my $i (0 .. $#{ $self->headers->{headers} }) {
+      next unless         $self->headers->{used}->{ $self->headers->{headers}->[$i] };
+      push @headers,      $self->headers->{headers}->[$i];
+      push @methods,      $self->headers->{methods}->[$i];
+    }
+    for my $i (0 .. $#{ $self->raw_data_headers->{headers} }) {
+      next unless         $self->raw_data_headers->{used}->{ $self->raw_data_headers->{headers}->[$i] };
+      push @headers,      $self->raw_data_headers->{headers}->[$i];
+      push @raw_methods,  $self->raw_data_headers->{headers}->[$i];
+    }
+
+    do_statement($::form, $sth, $query, $report->id, $_, 0, $headers[$_]) for 0 .. $#headers;
+
+    # col offsets
+    my $o1 =       @info_methods;
+    my $o2 = $o1 + @methods;
+
+    for my $row (0 .. $#{ $self->data }) {
+      $self->track_progress(progress => $row / @{ $self->data } * 100) if $row % 1000 == 0;
+      my $data_row = $self->{data}[$row];
+
+      do_statement($::form, $sth, $query, $report->id,       $_, $row + 1, $data_row->{info_data}{ $info_methods[$_] }) for 0 .. $#info_methods;
+      do_statement($::form, $sth, $query, $report->id, $o1 + $_, $row + 1, $data_row->{object}->${ \ $methods[$_] })    for 0 .. $#methods;
+      do_statement($::form, $sth, $query, $report->id, $o2 + $_, $row + 1, $data_row->{raw_data}{ $raw_methods[$_] })   for 0 .. $#raw_methods;
+
+      do_statement($::form, $sth2, $query2, $report->id, $row + 1, 'information', $_) for @{ $data_row->{information} || [] };
+      do_statement($::form, $sth2, $query2, $report->id, $row + 1, 'errors', $_)      for @{ $data_row->{errors}      || [] };
+    }
+    1;
+  }) or do { die SL::DB->client->error };
+
+  return $report->id;
+}
+
+sub save_report_multi {
+  my ($self, %params) = @_;
+
+  $self->track_progress(phase => 'building report', progress => 0);
+
+  my $clone_profile = $self->profile->clone_and_reset_deep;
+  $clone_profile->save; # weird bug. if this isn't saved before adding it to the report, it will default back to the last profile.
+
+  my $report = SL::DB::CsvImportReport->new(
+    session_id => $params{session_id},
+    profile    => $clone_profile,
+    type       => $self->type,
+    file       => '',
+    numrows    => scalar @{ $self->data },
+    numheaders => scalar @{ $self->worker->profile },
+    test_mode  => $params{test} ? 1 : 0,
+  );
+
+  $report->save(cascade => 1) or die $report->db->error;
+
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
+
+    my $query  = 'INSERT INTO csv_import_report_rows (csv_import_report_id, col, row, value) VALUES (?, ?, ?, ?)';
+    my $query2 = 'INSERT INTO csv_import_report_status (csv_import_report_id, row, type, value) VALUES (?, ?, ?, ?)';
+
+    my $sth = $dbh->prepare($query);
+    my $sth2 = $dbh->prepare($query2);
+
+    # save headers
+    my ($headers, $info_methods, $raw_methods, $methods);
+
+    for my $i (0 .. $#{ $self->worker->profile }) {
+      my $row_ident = $self->worker->profile->[$i]->{row_ident};
+
+      for my $i (0 .. $#{ $self->info_headers->{$row_ident}->{headers} }) {
+        next unless                            $self->info_headers->{$row_ident}->{used}->{ $self->info_headers->{$row_ident}->{methods}->[$i] };
+        push @{ $headers->{$row_ident} },      $self->info_headers->{$row_ident}->{headers}->[$i];
+        push @{ $info_methods->{$row_ident} }, $self->info_headers->{$row_ident}->{methods}->[$i];
+      }
+      for my $i (0 .. $#{ $self->headers->{$row_ident}->{headers} }) {
+        next unless                       $self->headers->{$row_ident}->{used}->{ $self->headers->{$row_ident}->{headers}->[$i] };
+        push @{ $headers->{$row_ident} }, $self->headers->{$row_ident}->{headers}->[$i];
+        push @{ $methods->{$row_ident} }, $self->headers->{$row_ident}->{methods}->[$i];
+      }
+
+      for my $i (0 .. $#{ $self->raw_data_headers->{$row_ident}->{headers} }) {
+      next unless                           $self->raw_data_headers->{$row_ident}->{used}->{ $self->raw_data_headers->{$row_ident}->{headers}->[$i] };
+      push @{ $headers->{$row_ident} },     $self->raw_data_headers->{$row_ident}->{headers}->[$i];
+      push @{ $raw_methods->{$row_ident} }, $self->raw_data_headers->{$row_ident}->{headers}->[$i];
+    }
+
+    }
+
+    for my $i (0 .. $#{ $self->worker->profile }) {
+      my $row_ident = $self->worker->profile->[$i]->{row_ident};
+      do_statement($::form, $sth, $query, $report->id, $_, $i, $headers->{$row_ident}->[$_]) for 0 .. $#{ $headers->{$row_ident} };
+    }
+
+    # col offsets
+    my ($off1, $off2);
+    for my $i (0 .. $#{ $self->worker->profile }) {
+      my $row_ident = $self->worker->profile->[$i]->{row_ident};
+      my $n_info_methods = $info_methods->{$row_ident} ? scalar @{ $info_methods->{$row_ident} } : 0;
+      my $n_methods      = $methods->{$row_ident} ?      scalar @{ $methods->{$row_ident} }      : 0;
+
+      $off1->{$row_ident} = $n_info_methods;
+      $off2->{$row_ident} = $off1->{$row_ident} + $n_methods;
+    }
+
+    my $n_header_rows = scalar @{ $self->worker->profile };
+
+    for my $row (0 .. $#{ $self->data }) {
+      $self->track_progress(progress => $row / @{ $self->data } * 100) if $row % 1000 == 0;
+      my $data_row = $self->{data}[$row];
+      my $row_ident = $data_row->{raw_data}{datatype};
+
+      my $o1 = $off1->{$row_ident};
+      my $o2 = $off2->{$row_ident};
+
+      do_statement($::form, $sth, $query, $report->id,       $_, $row + $n_header_rows, $data_row->{info_data}{ $info_methods->{$row_ident}->[$_] }) for 0 .. $#{ $info_methods->{$row_ident} };
+      do_statement($::form, $sth, $query, $report->id, $o1 + $_, $row + $n_header_rows, $data_row->{object}->${ \ $methods->{$row_ident}->[$_] })    for 0 .. $#{ $methods->{$row_ident} };
+      do_statement($::form, $sth, $query, $report->id, $o2 + $_, $row + $n_header_rows, $data_row->{raw_data}{ $raw_methods->{$row_ident}->[$_] })   for 0 .. $#{ $raw_methods->{$row_ident} };
+
+      do_statement($::form, $sth2, $query2, $report->id, $row + $n_header_rows, 'information', $_) for @{ $data_row->{information} || [] };
+      do_statement($::form, $sth2, $query2, $report->id, $row + $n_header_rows, 'errors', $_)      for @{ $data_row->{errors}      || [] };
+    }
+    1;
+  }) or do { die SL::DB->client->error };
+
+  return $report->id;
+}
+
+sub csv_file_name {
+  my ($self) = @_;
+  return "csv-import-" . $self->type . ".csv";
+}
+
+sub init_worker {
+  my $self = shift;
+
+  my @args = (controller => $self);
+
+  if ( $self->file() ) {
+    push(@args, file => $self->file());
+  }
+
+  return $self->{type} eq 'customers_vendors' ? SL::Controller::CsvImport::CustomerVendor->new(@args)
+       : $self->{type} eq 'contacts'          ? SL::Controller::CsvImport::Contact->new(@args)
+       : $self->{type} eq 'addresses'         ? SL::Controller::CsvImport::Shipto->new(@args)
+       : $self->{type} eq 'parts'             ? SL::Controller::CsvImport::Part->new(@args)
+       : $self->{type} eq 'inventories'       ? SL::Controller::CsvImport::Inventory->new(@args)
+       : $self->{type} eq 'projects'          ? SL::Controller::CsvImport::Project->new(@args)
+       : $self->{type} eq 'orders'            ? SL::Controller::CsvImport::Order->new(@args)
+       : $self->{type} eq 'bank_transactions' ? SL::Controller::CsvImport::BankTransaction->new(@args)
+       : $self->{type} eq 'ar_transactions'   ? SL::Controller::CsvImport::ARTransaction->new(@args)
+       :                                        die "Program logic error";
+}
+
+sub init_num_imported { 0 }
+
+sub setup_help {
+  my ($self) = @_;
+
+  $self->worker->setup_displayable_columns;
+}
+
+sub track_progress {
+  my ($self, %params) = @_;
+
+  for my $tracker ($self->progress_tracker) {
+    $tracker->track_progress(%params);
+  }
+}
+
+sub init_task_server {
+  SL::System::TaskServer->new;
+}
+
+sub cleanup_reports {
+  SL::DB::Manager::CsvImportReport->cleanup;
+}
+
+sub check_task_server {
+  return 1 if $_[0]->task_server->is_running;
+
+  flash('info', t8('The task server is not running at the moment but needed for this module'));
+
+  1;
+}
+
+sub mappings_for_profile {
+  +{ map { $_->{from} => $_->{to} } @{ $_[0]->mappings } }
+}
+
+sub init_mappings {
+  [ grep { $_->{from} } @{ $::form->{mappings} || [] } ]
+}
+
+sub setup_render_inputs_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Preview'),
+        submit    => [ '#form', { action => 'CsvImport/test' } ],
+        accesskey => 'enter',
+        not_if    => ($self->profile && $self->profile->get('dont_edit_profile')),
+      ],
+      action => [
+        t8('Import'),
+        submit    => [ '#form', { action => 'CsvImport/import' } ],
+        disabled  => t8('The test import has not been executed yet.'),
+        id        => 'action_import',
+      ],
+    );
+  }
+}
+