package AccTransCorrections;

use strict;

use List::Util qw(first);

use SL::DBUtils;
use SL::Taxkeys;

sub new {
  my $type = shift;

  my $self = {};

  bless $self, $type;

  $self->{taxkeys} = Taxkeys->new();

  return $self;
}

sub _fetch_transactions {
  $main::lxdebug->enter_sub();

  my $self     = shift;
  my %params   = @_;

  my $myconfig = \%main::myconfig;
  my $form     = $main::form;

  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);

  my (@where, @values) = ((), ());

  if ($params{transdate_from}) {
    push @where,  qq|at.transdate >= ?|;
    push @values, $params{transdate_from};
  }

  if ($params{transdate_to}) {
    push @where,  qq|at.transdate <= ?|;
    push @values, $params{transdate_to};
  }

  if ($params{trans_id}) {
    push @where,  qq|at.trans_id = ?|;
    push @values, $params{trans_id};
  }

  my $where = '';
  if (scalar @where) {
    $where = 'WHERE ' . join(' AND ', map { "($_)" } @where);
  }

  my $query = qq!
    SELECT at.oid, at.*,
      c.accno, c.description AS chartdescription, c.charttype, c.category AS chartcategory, c.link AS chartlink,
      COALESCE(gl.reference, COALESCE(ap.invnumber, ar.invnumber)) AS reference,
      COALESCE(ap.invoice, COALESCE(ar.invoice, FALSE)) AS invoice,
      CASE
        WHEN gl.id IS NOT NULL THEN gl.storno AND (gl.storno_id IS NOT NULL)
        WHEN ap.id IS NOT NULL THEN ap.storno AND (ap.storno_id IS NOT NULL)
        ELSE                        ar.storno AND (ar.storno_id IS NOT NULL)
      END AS is_storno,
      CASE
        WHEN gl.id IS NOT NULL THEN 'gl'
        WHEN ap.id IS NOT NULL THEN 'ap'
        ELSE                        'ar'
      END AS module

    FROM acc_trans at
    LEFT JOIN chart c ON (at.chart_id = c.id)
    LEFT JOIN gl      ON (at.trans_id = gl.id)
    LEFT JOIN ap      ON (at.trans_id = ap.id)
    LEFT JOIN ar      ON (at.trans_id = ar.id)
    $where
    ORDER BY at.trans_id, at.oid
!;

  my @transactions = ();
  my $last_trans   = undef;

  foreach my $entry (@{ selectall_hashref_query($form, $dbh, $query, @values) }) {
    if (!$last_trans || ($last_trans->[0]->{trans_id} != $entry->{trans_id})) {
      $last_trans = [];
      push @transactions, $last_trans;
    }

    push @{ $last_trans }, $entry;
  }

  $main::lxdebug->leave_sub();

  return @transactions;
}

sub _prepare_data {
  $main::lxdebug->enter_sub();

  my $self        = shift;
  my %params      = @_;

  my $transaction = $params{transaction};
  my $callback    = $params{callback};

  my $myconfig    = \%main::myconfig;
  my $form        = $main::form;

  my $data          = {
    'credit'        => {
      'num'         => 0,
      'sum'         => 0,
      'entries'     => [],
      'tax_sum'     => 0,
      'tax_entries' => [],
    },
    'debit'         => {
      'num'         => 0,
      'sum'         => 0,
      'entries'     => [],
      'tax_sum'     => 0,
      'tax_entries' => [],
    },
    'payments'      => [],
  };

  foreach my $entry (@{ $transaction }) {
    $entry->{chartlinks} = { map { $_ => 1 } split(m/:/, $entry->{chartlink}) };
    delete $entry->{chartlink};
  }

  # Verknüpfungen zwischen Steuerschlüsseln und zum Zeitpunkt der Transaktion
  # gültigen Steuersätze
  my %all_taxes = $self->{taxkeys}->get_full_tax_info('transdate' => $transaction->[0]->{transdate});

  my ($trans_type, $previous_non_tax_entry);
  my $sum             = 0;
  my $first_sub_trans = 1;

  my $storno_mult     = $transaction->[0]->{is_storno} ? -1 : 1;

  # Aufteilung der Buchungspositionen in Soll-, Habenseite sowie
  # getrennte Auflistung der Positionen, die auf Steuerkonten gebucht werden.
  foreach my $entry (@{ $transaction }) {
    if (!$first_sub_trans && ($entry->{chartlinks}->{AP_paid} || $entry->{chartlinks}->{AR_paid})) {
      push @{ $data->{payments} }, $entry;
      next;
    }

    my $tax_info = $all_taxes{taxkeys}->{ $entry->{taxkey} };
    if ($tax_info) {
      $entry->{taxdescription} = $tax_info->{taxdescription} . ' ' . $form->format_amount($myconfig, $tax_info->{taxrate} * 100) . ' %';
    }

    if ($entry->{chartlinks}->{AP}) {
      $trans_type = 'AP';
    } elsif ($entry->{chartlinks}->{AR}) {
      $trans_type = 'AR';
    }

    my $idx = 0 < ($entry->{amount} * $storno_mult) ? 'credit' : 'debit';

    if ($entry->{chartlinks}->{AP_tax} || $entry->{chartlinks}->{AR_tax}) {
      $data->{$idx}->{tax_sum} += $entry->{amount};
      push @{ $data->{$idx}->{tax_entries} }, $entry;

      if ($previous_non_tax_entry) {
        $previous_non_tax_entry->{tax_entry} = $entry;
        undef $previous_non_tax_entry;
      }

    } else {
      $data->{$idx}->{sum} += $entry->{amount};
      push @{ $data->{$idx}->{entries} }, $entry;

      $previous_non_tax_entry = $entry;
    }

    $sum += $entry->{amount};

    if (abs($sum) < 0.02) {
      $sum             = 0;
      $first_sub_trans = 0;
    }
  }

  # Alle Einträge entfernen, die die Gegenkonten zu Zahlungsein- und
  # -ausgängen darstellen.
  foreach my $payment (@{ $data->{payments} }) {
    my $idx = 0 < $payment->{amount} ? 'debit' : 'credit';

    foreach my $i (0 .. scalar(@{ $data->{$idx}->{entries} }) - 1) {
      my $entry = $data->{$idx}->{entries}->[$i];

      next if ((($payment->{amount} * -1) != $entry->{amount}) || ($payment->{transdate} ne $entry->{transdate}));

      splice @{ $data->{$idx}->{entries} }, $i, 1;

      last;
    }
  }

  delete $data->{payments};

  map { $data->{$_}->{num} = scalar @{ $data->{$_}->{entries} } } qw(credit debit);

  my $info   = $transaction->[0];
  my $script = ($info->{module} eq 'ar') && $info->{invoice} ? 'is'
             : ($info->{module} eq 'ap') && $info->{invoice} ? 'ir'
             :                                                 $info->{module};

  my %common_args = (
    'data'          => $data,
    'trans_type'    => $trans_type,
    'all_taxes'     => { %all_taxes },
    'transaction'   => $transaction,
    'full_analysis' => $params{full_analysis},
    'problem'       => {
      'data'        => $info,
      'link'        => $script . ".pl?action=edit${callback}&id=" . $info->{trans_id},
    },
    );

  $main::lxdebug->leave_sub();

  return %common_args;
}

sub _group_sub_transactions {
  $main::lxdebug->enter_sub();

  my $self             = shift;
  my $transaction      = shift;

  my @sub_transactions = ();
  my $sum              = 0;

  foreach my $i (0 .. scalar(@{ $transaction }) - 1) {
    my $entry = $transaction->[$i];

    if (abs($sum) <= 0.01) {
      push @sub_transactions, [];
      $sum = 0;
    }
    $sum += $entry->{amount};

    push @{ $sub_transactions[-1] }, $entry;
  }

  $main::lxdebug->leave_sub();

  return @sub_transactions;
}

# Problemfall: Verkaufsrechnungen, bei denen Buchungen auf Warenbestandskonten
# mit Steuerschlüssel != 0 durchgeführt wurden. Richtig wäre, dass alle
# Steuerschlüssel für solche Warenbestandsbuchungen 0 sind.
sub _check_trans_invoices_inventory_with_taxkeys {
  $main::lxdebug->enter_sub();

  my $self   = shift;
  my %params = @_;

  if (!$params{transaction}->[0]->{invoice}) {
    $main::lxdebug->leave_sub();
    return 0;
  }

  my @sub_transactions = $self->_group_sub_transactions($params{transaction});

  foreach my $sub_transaction (@sub_transactions) {
    my $is_cogs = first { $_->{chartlinks}->{IC_cogs} } @{ $sub_transaction };
    next unless ($is_cogs);

    my $needs_fixing = first { $_->{taxkey} != 0 } @{ $sub_transaction };
    next unless ($needs_fixing);

    $params{problem}->{type} = 'invoice_inventory_with_taxkeys';
    push @{ $self->{invoice_inventory_taxkey_problems} }, $params{problem};

    $main::lxdebug->leave_sub();

    return 1;
  }

  $main::lxdebug->leave_sub();

  return 0;
}

# Problemfall: Kreditorenbuchungen, bei denen mit Umsatzsteuerschlüsseln
# gebucht wurde und Debitorenbuchungen, bei denen mit Vorsteuerschlüsseln
# gebucht wurde.
sub _check_trans_ap_ar_wrong_taxkeys {
  $main::lxdebug->enter_sub();

  my $self   = shift;
  my %params = @_;

  my $retval = 0;

  if (!$params{transaction}->[0]->{invoice}
      && ((   ($params{transaction}->[0]->{module} eq 'ap')
          && (first { my $taxkey = $_->{taxkey}; first { $taxkey == $_ } (2, 3, 12, 13) } @{ $params{transaction} }))
         ||
         (   ($params{transaction}->[0]->{module} eq 'ar')
          && (first { my $taxkey = $_->{taxkey}; first { $taxkey == $_ } (8, 9, 18, 19) } @{ $params{transaction} })))) {
    $params{problem}->{type} = 'ap_ar_wrong_taxkeys';
    push @{ $self->{ap_ar_taxkey_problems} }, $params{problem};

    $retval = 1;
  }

  $main::lxdebug->leave_sub();

  return $retval;
}

# Problemfall: Splitbuchungen, die mehrere Haben- und Sollkonten ansprechen.
# Aber nur für Debitoren- und Kreditorenbuchungen, weil das bei Einkaufs- und
# Verkaufsrechnungen hingegen völlig normal ist.
sub _check_trans_split_multiple_credit_and_debit {
  $main::lxdebug->enter_sub();

  my $self   = shift;
  my %params = @_;

  my $retval = 0;

  if (   !$params{transaction}->[0]->{invoice}
      && (1 < $params{data}->{credit}->{num})
      && (1 < $params{data}->{debit}->{num})) {
    $params{problem}->{type} = 'split_multiple_credit_and_debit';
    push @{ $self->{problems} }, $params{problem};

    $retval = 1;
  }

  $main::lxdebug->leave_sub();

  return $retval;
}

# Problemfall: Buchungen, bei denen Steuersummen nicht mit den Summen
# übereinstimmen, die nach ausgewähltem Steuerschlüssel hätten auftreten müssen.
sub _check_trans_wrong_taxkeys {
  $main::lxdebug->enter_sub();

  my $self        = shift;
  my %params      = @_;

  my $form        = $main::form;

  my %data        = %{ $params{data} };
  my $transaction = $params{transaction};

  if (   $transaction->[0]->{invoice}
      || $transaction->[0]->{ob_transaction}
      || $transaction->[0]->{cb_transaction}
      || (!scalar @{ $data{credit}->{entries} } && !scalar @{ $data{debit}->{entries} })
      || (   ($transaction->[0]->{module} eq 'gl')
          && (!scalar @{ $data{credit}->{entries} } || !scalar @{ $data{debit}->{entries} }))) {
    $main::lxdebug->leave_sub();
    return 0;
  }

  my $retval = 0;

  my ($side, $other_side);
  if (   (grep { $_->{taxkey} * 1 } @{ $data{credit}->{entries} })
      || (scalar @{ $data{credit}->{tax_entries} })) {
    $side       = 'credit';
    $other_side = 'debit';

  } elsif (   (grep { $_->{taxkey} * 1 } @{ $data{debit}->{entries} })
           || (scalar @{ $data{debit}->{tax_entries} })) {
    $side       = 'debit';
    $other_side = 'credit';
  }

  if (!$side) {
    $main::lxdebug->leave_sub();
    return 0;
  }

  my $expected_tax          = 0;
  my %num_entries_per_chart = ();
  my $num_taxed_entries     = 0;

  foreach my $entry (@{ $data{$side}->{entries} }) {
    my $taxinfo             = $params{all_taxes}->{taxkeys}->{$entry->{taxkey}} || { };
    $entry->{expected_tax}  = $entry->{amount} * $taxinfo->{taxrate};
    $expected_tax          += $entry->{expected_tax};

    $num_taxed_entries++ if ($taxinfo->{taxrate} * 1);

    my $chart_key = $entry->{chart_id} . "-" . $entry->{taxkey};
    $num_entries_per_chart{$chart_key} ||= 0;
    $num_entries_per_chart{$chart_key}++;
  }

#   $main::lxdebug->message(0, "side $side trans_id $transaction->[0]->{trans_id} expected tax $expected_tax actual tax $data{$side}->{tax_sum}");

  if (abs($expected_tax - $data{$side}->{tax_sum}) >= (0.01 * ($num_taxed_entries + 1))) {
    if ($params{full_analysis}) {
      my $storno_mult = $data{$side}->{entries}->[0]->{is_storno} ? -1 : 1;

      foreach my $entry (@{ $data{$other_side}->{entries} }) {
        $entry->{display_amount} = $form->round_amount(abs($entry->{amount}) * $storno_mult, 2);
      }

      foreach my $entry (@{ $data{$side}->{entries} }) {
        $entry->{actual_tax}     = $form->round_amount(abs($entry->{tax_entry} ? $entry->{tax_entry}->{amount} : 0), 2);
        $entry->{expected_tax}   = $form->round_amount(abs($entry->{expected_tax}), 2);
        $entry->{taxkey_error}   =    ( $entry->{taxkey} && !$entry->{tax_entry})
                                   || (!$entry->{taxkey} &&  $entry->{tax_entry})
                                   || (abs($entry->{expected_tax} - $entry->{actual_tax}) >= 0.02);
        $entry->{tax_entry_oid}  = $entry->{tax_entry}->{oid};
        delete $entry->{tax_entry};

        $entry->{display_amount}       = $form->round_amount(abs($entry->{amount}) * $storno_mult, 2);
        $entry->{display_actual_tax}   = $entry->{actual_tax}   * $storno_mult;
        $entry->{display_expected_tax} = $entry->{expected_tax} * $storno_mult;

        if ($entry->{taxkey_error}) {
          $self->{negative_taxkey_filter} ||= {
            'ar' => { map { $_ => 1 } (   8, 9, 18, 19) },
            'ap' => { map { $_ => 1 } (1, 2, 3, 12, 13) },
            'gl' => { },
          };

          $entry->{correct_taxkeys} = [];

          my %all_taxes = $self->{taxkeys}->get_full_tax_info('transdate' => $entry->{transdate});

          foreach my $taxkey (sort { $a <=> $b } keys %{ $all_taxes{taxkeys} }) {
            next if ($self->{negative_taxkey_filter}->{ $entry->{module} }->{$taxkey});

            my $tax_info = $all_taxes{taxkeys}->{$taxkey};

            next if ((!$tax_info || (0 == $tax_info->{taxrate} * 1)) && $entry->{tax_entry_oid});

            push @{ $entry->{correct_taxkeys} }, {
              'taxkey'      => $taxkey,
              'tax'         => $form->round_amount(abs($entry->{amount}) * $tax_info->{taxrate}, 2),
              'description' => sprintf("%s %d%%", $tax_info->{taxdescription}, int($tax_info->{taxrate} * 100)),
            };
          }
        }
      }
    }

    if (first { $_ > 1 } values %num_entries_per_chart) {
      $params{problem}->{type} = 'wrong_taxkeys';
    } else {
      $params{problem}->{type} = 'wrong_taxes';
    }

    $params{problem}->{acc_trans} = { %data };
    push @{ $self->{problems} }, $params{problem};

    $retval = 1;
  }

  $main::lxdebug->leave_sub();

  return $retval;
}

# Inaktiver Code für das Erraten möglicher Verteilungen von
# Steuerschlüsseln. Deaktiviert, weil er exponentiell Zeit
# benötigt.

#       if (abs($expected_tax - $data{$side}->{tax_sum}) >= 0.02) {
#         my @potential_taxkeys = $trans_type eq 'AP' ? (0, 8, 9) : (0, 1, 2, 3);

#         $main::lxdebug->dump(0, "pota", \@potential_taxkeys);

#         # Über alle Kombinationen aus Buchungssätzen und potenziellen Steuerschlüsseln
#         # iterieren und jeweils die Summe ermitteln.
#         my $num_entries    = scalar @{ $data{$side}->{entries} };
#         my @taxkey_indices = (0) x $num_entries;

#         my @solutions      = ();

#         my $start_time     = time();

#         $main::lxdebug->message(0, "num_entries $num_entries");

#         while ($num_entries == scalar @taxkey_indices) {
#           my @tax_cache = ();

#           # Berechnen der Steuersumme für die aktuell angenommenen Steuerschlüssel.
#           my $tax_sum = 0;
#           foreach my $i (0 .. $num_entries - 1) {
#             my $taxkey      = $potential_taxkeys[$taxkey_indices[$i]];
#             my $entry       = $data{$side}->{entries}->[$i];
#             my $taxinfo     = $all_taxes{taxkeys}->{ $taxkey } || { };
#             $tax_cache[$i]  = $entry->{amount} * $taxinfo->{taxrate};
#             $tax_sum       += $tax_cache[$i];
#           }

#           # Entspricht die Steuersumme mit den aktuell angenommenen Steuerschlüsseln
#           # der verbuchten Steuersumme? Wenn ja, dann ist das eine potenzielle
#           # Lösung.
#           if (abs($tax_sum - $data{$side}->{tax_sum}) < 0.02) {
#             push @solutions, {
#               'taxkeys' => [ @potential_taxkeys[@taxkey_indices] ],
#               'taxes'   => [ @tax_cache ],
#             }
#           }

#           # Weiterzählen der Steuerschlüsselindices zum Interieren über
#           # alle möglichen Kombinationen.
#           my $i = 0;
#           while (1) {
#             $taxkey_indices[$i]++;
#             last if ($taxkey_indices[$i] < scalar @potential_taxkeys);

#             $taxkey_indices[$i] = 0;
#             $i++;
#           }

#           my $now = time();
#           if (($now - $start_time) >= 5) {
#             $main::lxdebug->message(0, "  " . join("", @taxkey_indices));
#             $start_time = $now;
#           }
#         }

#         foreach my $solution (@solutions) {
#           $solution->{rows}    = [];
#           $solution->{changes} = [];
#           my $error            = 0;

#           foreach my $i (0 .. $num_entries - 1) {
#             if ($solution->{taxes}->[$i]) {
#               my $tax_rounded          = $form->round_amount($solution->{taxes}->[$i] + $error, 2);
#               $error                   = $solution->{taxes}->[$i] + $error - $tax_rounded;
#               $solution->{taxes}->[$i] = $tax_rounded;
#             }

#             my $entry     = $data{$side}->{entries}->[$i];
#             my $tax_entry = $all_taxes{taxkeys}->{ $solution->{taxkeys}->[$i] };

#             push @{ $solution->{rows} }, {
#               %{ $entry },
#               %{ $tax_entry },
#               'taxamount' => $solution->{taxes}->[$i],
#             };

#             $solution->{rows}->[$i]->{taxdescription} .= ' ' . $form->format_amount(\%myconfig, $tax_entry->{taxrate} * 100) . ' %';

#             push @{ $solution->{changes} }, {
#               'oid'    => $entry->{oid},
#               'taxkey' => $solution->{taxkeys}->[$i],
#             };
#           }

#           push @{ $solution->{rows} }, @{ $data{$other_side}->{entries} };

#           delete @{ $solution }{ qw(taxes taxkeys) };
#         }

#         $problem->{type}      = 'wrong_taxkeys';
#         $problem->{solutions} = [ @solutions ];
#         $problem->{acc_trans} = { %data };
#         push @problems, $problem;

#         next;
#       }

sub analyze {
  $main::lxdebug->enter_sub();

  my $self         = shift;
  my %params       = @_;

  my $myconfig     = \%main::myconfig;
  my $form         = $main::form;

  my $dbh          = $params{dbh} || $form->get_standard_dbh($myconfig);

  my @transactions = $self->_fetch_transactions(%params, 'dbh' => $dbh);

  if (!scalar @transactions) {
    $main::lxdebug->leave_sub();
    return ();
  }

  my $callback = $params{callback} ? '&callback=' . $params{callback} : '';

  $self->{problems}                          = [];
  $self->{ap_ar_taxkey_problems}             = [];
  $self->{invoice_inventory_taxkey_problems} = [];

  foreach my $transaction (@transactions) {
    my %common_args = $self->_prepare_data('transaction' => $transaction, 'callback' => $callback, 'full_analysis' => $params{full_analysis});

    next if ($self->_check_trans_ap_ar_wrong_taxkeys(%common_args));
    next if ($self->_check_trans_invoices_inventory_with_taxkeys(%common_args));
    next if ($self->_check_trans_split_multiple_credit_and_debit(%common_args));
    next if ($self->_check_trans_wrong_taxkeys(%common_args));
  }

  my @problems = @{ $self->{problems} };

  if (0 != scalar @{ $self->{ap_ar_taxkey_problems} }) {
    my $problem = {
      'type'        => 'ap_ar_wrong_taxkeys',
      'ap_problems' => [ grep { $_->{data}->{module} eq 'ap' } @{ $self->{ap_ar_taxkey_problems} } ],
      'ar_problems' => [ grep { $_->{data}->{module} eq 'ar' } @{ $self->{ap_ar_taxkey_problems} } ],
    };
    unshift @problems, $problem;
  }

  if (0 != scalar @{ $self->{invoice_inventory_taxkey_problems} }) {
    my $problem = {
      'type'        => 'invoice_inventory_with_taxkeys',
      'ap_problems' => [ grep { $_->{data}->{module} eq 'ap' } @{ $self->{invoice_inventory_taxkey_problems} } ],
      'ar_problems' => [ grep { $_->{data}->{module} eq 'ar' } @{ $self->{invoice_inventory_taxkey_problems} } ],
    };
    unshift @problems, $problem;
  }

  $main::lxdebug->leave_sub();


  return @problems;
}

sub fix_ap_ar_wrong_taxkeys {
  $main::lxdebug->enter_sub();

  my $self     = shift;
  my %params   = @_;

  my $myconfig = \%main::myconfig;
  my $form     = $main::form;

  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);

  my $query    = qq|SELECT 'ap' AS module,
                      at.oid, at.trans_id, at.chart_id, at.amount, at.taxkey, at.transdate,
                      c.link
                    FROM acc_trans at
                    LEFT JOIN chart c ON (at.chart_id = c.id)
                    WHERE (trans_id IN (SELECT id FROM ap WHERE NOT invoice))
                      AND (taxkey IN (2, 3, 12, 13))

                    UNION

                    SELECT 'ar' AS module,
                      at.oid, at.trans_id, at.chart_id, at.amount, at.taxkey, at.transdate,
                      c.link
                    FROM acc_trans at
                    LEFT JOIN chart c ON (at.chart_id = c.id)
                    WHERE (trans_id IN (SELECT id FROM ar WHERE NOT invoice))
                      AND (taxkey IN (8, 9, 18, 19))

                    ORDER BY trans_id, oid|;

  my $sth      = prepare_execute_query($form, $dbh, $query);
  my @transactions;

  while (my $ref = $sth->fetchrow_hashref()) {
    if ((!scalar @transactions) || ($ref->{trans_id} != $transactions[-1]->[0]->{trans_id})) {
      push @transactions, [];
    }

    push @{ $transactions[-1] }, $ref;
  }

  $sth->finish();

  @transactions = grep { (scalar(@transactions) % 2) == 0 } @transactions;

  my %taxkey_replacements = (
     2 =>  8,
     3 =>  9,
     8 =>  2,
     9 =>  3,
    12 => 18,
    13 => 19,
    18 => 12,
    19 => 13,
    );

  my %bad_taxkeys = (
    'ap' => { map { $_ => 1 } (2, 3, 12, 13) },
    'ar' => { map { $_ => 1 } (8, 9, 18, 19) },
    );

  my @corrections = ();

  foreach my $transaction (@transactions) {

    for (my $i = 0; $i < scalar @{ $transaction }; $i += 2) {
      my ($non_tax_idx, $tax_idx) = abs($transaction->[$i]->{amount}) > abs($transaction->[$i + 1]->{amount}) ? ($i, $i + 1) : ($i + 1, $i);
      my ($non_tax,     $tax)     = @{ $transaction }[$non_tax_idx, $tax_idx];

      last if ($non_tax->{link} =~ m/(:?AP|AR)_tax(:?$|:)/);
      last if ($tax->{link}     !~ m/(:?AP|AR)_tax(:?$|:)/);

      next if (!$bad_taxkeys{ $non_tax->{module} }->{ $non_tax->{taxkey} });

      my %all_taxes = $self->{taxkeys}->get_full_tax_info('transdate' => $non_tax->{transdate});

      push @corrections, ({ 'oid'      => $non_tax->{oid},
                            'taxkey'   => $taxkey_replacements{$non_tax->{taxkey}},
                          },
                          {
                            'oid'      => $tax->{oid},
                            'taxkey'   => $taxkey_replacements{$non_tax->{taxkey}},
                            'chart_id' => $all_taxes{taxkeys}->{ $taxkey_replacements{$non_tax->{taxkey}} }->{taxchart_id},
                          });
    }
  }

  if (scalar @corrections) {
    my $q_taxkey_only     = qq|UPDATE acc_trans SET taxkey = ? WHERE oid = ?|;
    my $h_taxkey_only     = prepare_query($form, $dbh, $q_taxkey_only);

    my $q_taxkey_chart_id = qq|UPDATE acc_trans SET taxkey = ?, chart_id = ? WHERE oid = ?|;
    my $h_taxkey_chart_id = prepare_query($form, $dbh, $q_taxkey_chart_id);

    foreach my $entry (@corrections) {
      if ($entry->{chart_id}) {
        do_statement($form, $h_taxkey_chart_id, $q_taxkey_chart_id, $entry->{taxkey}, $entry->{chart_id}, $entry->{oid});
      } else {
        do_statement($form, $h_taxkey_only, $q_taxkey_only, $entry->{taxkey}, $entry->{oid});
      }
    }

    $h_taxkey_only->finish();
    $h_taxkey_chart_id->finish();

    $dbh->commit() unless ($params{dbh});
  }

  $main::lxdebug->leave_sub();
}

sub fix_invoice_inventory_with_taxkeys {
  $main::lxdebug->enter_sub();

  my $self     = shift;
  my %params   = @_;

  my $myconfig = \%main::myconfig;
  my $form     = $main::form;

  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);

  my $query    = qq|SELECT at.oid, at.*, c.link
                    FROM acc_trans at
                    LEFT JOIN ar      ON (at.trans_id = ar.id)
                    LEFT JOIN chart c ON (at.chart_id = c.id)
                    WHERE (ar.invoice)

                    UNION

                    SELECT at.oid, at.*, c.link
                    FROM acc_trans at
                    LEFT JOIN ap      ON (at.trans_id = ap.id)
                    LEFT JOIN chart c ON (at.chart_id = c.id)
                    WHERE (ap.invoice)

                    ORDER BY trans_id, oid|;

  my $sth      = prepare_execute_query($form, $dbh, $query);
  my @transactions;

  while (my $ref = $sth->fetchrow_hashref()) {
    if ((!scalar @transactions) || ($ref->{trans_id} != $transactions[-1]->[0]->{trans_id})) {
      push @transactions, [];
    }

    push @{ $transactions[-1] }, $ref;
  }

  $sth->finish();

  my @corrections = ();

  foreach my $transaction (@transactions) {
    my @sub_transactions = $self->_group_sub_transactions($transaction);

    foreach my $sub_transaction (@sub_transactions) {
      my $is_cogs = first { $_->{link} =~ m/IC_cogs/ } @{ $sub_transaction };
      next unless ($is_cogs);

      foreach my $entry (@{ $sub_transaction }) {
        next if ($entry->{taxkey} == 0);
        push @corrections, $entry->{oid};
      }
    }
  }

  if (@corrections) {
    $query = qq|UPDATE acc_trans SET taxkey = 0 WHERE oid = ?|;
    $sth   = prepare_query($form, $dbh, $query);

    foreach my $oid (@corrections) {
      do_statement($form, $sth, $query, $oid);
    }

    $sth->finish();

    $dbh->commit() unless ($params{dbh});
#     $dbh->rollback();
  }

  $main::lxdebug->leave_sub();
}

sub fix_wrong_taxkeys {
  $main::lxdebug->enter_sub();

  my $self     = shift;
  my %params   = @_;

  Common::check_params(\%params, qw(fixes));

  my $myconfig = \%main::myconfig;
  my $form     = $main::form;

  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);

  my $q_taxkey_only  = qq|UPDATE acc_trans SET taxkey = ? WHERE oid = ?|;
  my $h_taxkey_only  = prepare_query($form, $dbh, $q_taxkey_only);

  my $q_taxkey_chart = qq|UPDATE acc_trans SET taxkey = ?, chart_id = ? WHERE oid = ?|;
  my $h_taxkey_chart = prepare_query($form, $dbh, $q_taxkey_chart);

  my $q_transdate    = qq|SELECT transdate FROM acc_trans WHERE oid = ?|;
  my $h_transdate    = prepare_query($form, $dbh, $q_transdate);

  foreach my $fix (@{ $params{fixes} }) {
    next unless ($fix->{oid});

    do_statement($form, $h_taxkey_only, $q_taxkey_only, conv_i($fix->{taxkey}), conv_i($fix->{oid}));

    next unless ($fix->{tax_entry_oid});

    do_statement($form, $h_transdate, $q_transdate, conv_i($fix->{tax_entry_oid}));
    my ($transdate) = $h_transdate->fetchrow_array();

    my %all_taxes = $self->{taxkeys}->get_full_tax_info('transdate' => $transdate);
    my $tax_info  = $all_taxes{taxkeys}->{ $fix->{taxkey} };

    next unless ($tax_info);

    do_statement($form, $h_taxkey_chart, $q_taxkey_chart, conv_i($fix->{taxkey}), conv_i($tax_info->{taxchart_id}), conv_i($fix->{tax_entry_oid}));
  }

  $h_taxkey_only->finish();
  $h_taxkey_chart->finish();
  $h_transdate->finish();

#   $dbh->rollback();
  $dbh->commit() unless ($params{dbh});

  $main::lxdebug->leave_sub();
}

sub delete_transaction {
  $main::lxdebug->enter_sub();

  my $self     = shift;
  my %params   = @_;

  Common::check_params(\%params, qw(trans_id));

  my $myconfig = \%main::myconfig;
  my $form     = $main::form;

  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);

  do_query($form, $dbh, qq|UPDATE ar SET storno_id = NULL WHERE storno_id = ?|, conv_i($params{trans_id}));
  do_query($form, $dbh, qq|UPDATE ap SET storno_id = NULL WHERE storno_id = ?|, conv_i($params{trans_id}));
  do_query($form, $dbh, qq|UPDATE gl SET storno_id = NULL WHERE storno_id = ?|, conv_i($params{trans_id}));

  do_query($form, $dbh, qq|DELETE FROM ar        WHERE id       = ?|, conv_i($params{trans_id}));
  do_query($form, $dbh, qq|DELETE FROM ap        WHERE id       = ?|, conv_i($params{trans_id}));
  do_query($form, $dbh, qq|DELETE FROM gl        WHERE id       = ?|, conv_i($params{trans_id}));
  do_query($form, $dbh, qq|DELETE FROM acc_trans WHERE trans_id = ?|, conv_i($params{trans_id}));

#   $dbh->rollback();
  $dbh->commit() unless ($params{dbh});

  $main::lxdebug->leave_sub();
}

1;
