Neue Methoden um Debitorenbuchungen zu erstellen
authorG. Richardson <information@kivitendo-premium.de>
Mon, 29 Feb 2016 10:56:43 +0000 (11:56 +0100)
committerG. Richardson <information@kivitendo-premium.de>
Mon, 29 Feb 2016 15:18:46 +0000 (16:18 +0100)
Vorbereitung für Debitorenbuchungsimport, neue Methoden für SL::DB::Invoice
Objekte:

add_ar_amount_row - Erlösbuchungen hinzufügen, mit Steuerschlüssel
create_ar_row -  acc-trans für Forderung hinzufügen
validate_acc_trans - Prüfen ob alle acc_trans-Einträge aufgehen
recalculate_amount - anhand acc_trans-Zeilen amount und netamount berechnen

SL/DB/Invoice.pm
t/ar/ar.t [new file with mode: 0644]

index 70291ad..2d6ab07 100644 (file)
@@ -3,9 +3,9 @@ package SL::DB::Invoice;
 use strict;
 
 use Carp;
-use List::Util qw(first);
+use List::Util qw(first sum);
 
-use Rose::DB::Object::Helpers ();
+use Rose::DB::Object::Helpers qw(has_loaded_related);
 use SL::DB::MetaSetup::Invoice;
 use SL::DB::Manager::Invoice;
 use SL::DB::Helper::Payment qw(:ALL);
@@ -282,6 +282,163 @@ sub _post_add_acctrans {
   }
 }
 
+sub add_ar_amount_row {
+  my ($self, %params ) = @_;
+
+  # only allow this method for ar invoices (Debitorenbuchung)
+  die "not an ar invoice" if $self->invoice and not $self->customer_id;
+
+  die "add_ar_amount_row needs a chart object as chart param" unless $params{chart} && $params{chart}->isa('SL::DB::Chart');
+  die unless $params{chart}->link =~ /AR_amount/;
+
+  my $acc_trans = [];
+
+  my $roundplaces = 2;
+  my ($netamount,$taxamount);
+
+  $netamount = $params{amount} * 1;
+  my $tax = SL::DB::Manager::Tax->find_by(id => $params{tax_id}) || die "Can't find tax with id " . $params{tax_id};
+
+  if ( $tax and $tax->rate != 0 ) {
+    ($netamount, $taxamount) = Form->calculate_tax($params{amount}, $tax->rate, $self->taxincluded, $roundplaces);
+  };
+  next unless $netamount; # netamount mustn't be zero
+
+  my $sign = $self->customer_id ? 1 : -1;
+  my $acc = SL::DB::AccTransaction->new(
+    amount     => $netamount * $sign,
+    chart_id   => $params{chart}->id,
+    chart_link => $params{chart}->link,
+    transdate  => $self->transdate,
+    taxkey     => $tax->taxkey,
+    tax_id     => $tax->id,
+    project_id => $params{project_id},
+  );
+
+  $self->add_transactions( $acc );
+  push( @$acc_trans, $acc );
+
+  if ( $taxamount ) {
+     my $acc = SL::DB::AccTransaction->new(
+       amount     => $taxamount * $sign,
+       chart_id   => $tax->chart_id,
+       chart_link => $tax->chart->link,
+       transdate  => $self->transdate,
+       taxkey     => $tax->taxkey,
+       tax_id     => $tax->id,
+     );
+     $self->add_transactions( $acc );
+     push( @$acc_trans, $acc );
+  };
+  return $acc_trans;
+};
+
+sub create_ar_row {
+  my ($self, %params) = @_;
+  # to be called after adding all AR_amount rows, adds an AR row
+
+  # only allow this method for ar invoices (Debitorenbuchung)
+  die if $self->invoice and not $self->customer_id;
+  die "create_ar_row needs a chart object as a parameter" unless $params{chart} and ref($params{chart}) eq 'SL::DB::Chart';
+
+  my @transactions = @{$self->transactions};
+  # die "invoice has no acc_transactions" unless scalar @transactions > 0;
+  return 0 unless scalar @transactions > 0;
+
+  my $chart = $params{chart} || SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ar_chart_id);
+  die "illegal chart in create_ar_row" unless $chart;
+
+  die "receivables chart must have link 'AR'" unless $chart->link eq 'AR';
+
+  my $acc_trans = [];
+
+  # hardcoded entry for no tax: tax_id and taxkey should be 0
+  my $tax = SL::DB::Manager::Tax->find_by(id => 0, taxkey => 0) || die "Can't find tax with id 0 and taxkey 0";
+
+  my $sign = $self->customer_id ? -1 : 1;
+  my $acc = SL::DB::AccTransaction->new(
+    amount     => $self->amount * $sign,
+    chart_id   => $params{chart}->id,
+    chart_link => $params{chart}->link,
+    transdate  => $self->transdate,
+    taxkey     => $tax->taxkey,
+    tax_id     => $tax->id,
+  );
+  $self->add_transactions( $acc );
+  push( @$acc_trans, $acc );
+  return $acc_trans;
+};
+
+sub validate_acc_trans {
+  my ($self, %params) = @_;
+  # should be able to check unsaved invoice objects with several acc_trans lines
+
+  die "validate_acc_trans can't check invoice object with empty transactions" unless $self->transactions;
+
+  my @transactions = @{$self->transactions};
+  # die "invoice has no acc_transactions" unless scalar @transactions > 0;
+  return 0 unless scalar @transactions > 0;
+  return 0 unless $self->has_loaded_related('transactions');
+  if ( $params{debug} ) {
+    printf("starting validatation of invoice %s with trans_id %s and taxincluded %s\n", $self->invnumber, $self->id, $self->taxincluded);
+    foreach my $acc ( @transactions ) {
+      printf("chart: %s  amount: %s   tax_id: %s  link: %s\n", $acc->chart->accno, $acc->amount, $acc->tax_id, $acc->chart->link);
+    };
+  };
+
+  my $acc_trans_sum = sum map { $_->amount } @transactions;
+
+  unless ( $::form->round_amount($acc_trans_sum, 10) == 0 ) {
+    my $string = "sum of acc_transactions isn't 0: $acc_trans_sum\n";
+
+    if ( $params{debug} ) {
+      foreach my $trans ( @transactions ) {
+          $string .= sprintf("  %s %s %s\n", $trans->chart->accno, $trans->taxkey, $trans->amount);
+      };
+    };
+    return 0;
+  };
+
+  # only use the first AR entry, so it also works for paid invoices
+  my @ar_transactions = map { $_->amount } grep { $_->chart_link eq 'AR' } @transactions;
+  my $ar_sum = $ar_transactions[0];
+  # my $ar_sum = sum map { $_->amount } grep { $_->chart_link eq 'AR' } @transactions;
+
+  unless ( $::form->round_amount($ar_sum * -1,2) == $::form->round_amount($self->amount,2) ) {
+    if ( $params{debug} ) {
+      printf("debug: (ar_sum) %s = %s (amount)\n",  $::form->round_amount($ar_sum * -1,2) , $::form->round_amount($self->amount, 2) );
+      foreach my $trans ( @transactions ) {
+        printf("  %s %s %s %s\n", $trans->chart->accno, $trans->taxkey, $trans->amount, $trans->chart->link);
+      };
+    };
+    die sprintf("sum of ar (%s) isn't equal to invoice amount (%s)", $::form->round_amount($ar_sum * -1,2), $::form->round_amount($self->amount,2));
+  };
+
+  return 1;
+};
+
+sub recalculate_amounts {
+  my ($self, %params) = @_;
+  # calculate and set amount and netamount from acc_trans objects
+
+  croak ("Can only recalculate amounts for ar transactions") if $self->invoice;
+
+  return undef unless $self->has_loaded_related('transactions');
+
+  my ($netamount, $taxamount);
+
+  my @transactions = @{$self->transactions};
+
+  foreach my $acc ( @transactions ) {
+    $netamount += $acc->amount if $acc->chart->link =~ /AR_amount/;
+    $taxamount += $acc->amount if $acc->chart->link =~ /AR_tax/;
+  };
+
+  $self->amount($netamount+$taxamount);
+  $self->netamount($netamount);
+};
+
+
 sub _post_create_assemblyitem_entries {
   my ($self, $assembly_entries) = @_;
 
@@ -502,6 +659,63 @@ active.
 
 See L<SL::DB::Object::basic_info>.
 
+=item C<recalculate_amounts %params>
+
+Calculate and set amount and netamount from acc_trans objects by summing up the
+values of acc_trans objects with AR_amount and AR_tax link charts.
+amount and netamount are set to the calculated values.
+
+=item C<validate_acc_trans>
+
+Checks if the sum of all associated acc_trans objects is 0 and checks whether
+the amount of the AR acc_transaction matches the AR amount. Only the first AR
+line is checked, because the sum of all AR lines is 0 for paid invoices.
+
+Returns 0 or 1.
+
+Can be called with a debug parameter which writes debug info to STDOUT, which is
+useful in console mode or while writing tests.
+
+ my $ar = SL::DB::Manager::Invoice->get_first();
+ $ar->validate_acc_trans(debug => 1);
+
+=item C<create_ar_row %params>
+
+Creates a new acc_trans entry for the receivable (AR) entry of an existing AR
+invoice object, which already has some income and tax acc_trans entries.
+
+The acc_trans entry is also returned inside an array ref.
+
+Mandatory params are
+
+=over 2
+
+=item * chart as an RDBO object, e.g. for bank. Must be a 'paid' chart.
+
+=back
+
+Currently the amount of the invoice object is used for the acc_trans amount.
+Use C<recalculate_amounts> before calling this mehtod if amount it isn't known
+yet or you didn't set it manually.
+
+=item C<add_ar_amount_row %params>
+
+Add a new entry for an existing AR invoice object. Creates an acc_trans entry,
+and also adds an acc_trans tax entry, if the tax has an associated tax chart.
+Also all acc_trans entries that were created are returned inside an array ref.
+
+Mandatory params are
+
+=over 2
+
+=item * chart as an RDBO object, should be an income chart (link = AR_amount)
+
+=item * tax_id
+
+=item * amount
+
+=back
+
 =back
 
 =head1 TODO
diff --git a/t/ar/ar.t b/t/ar/ar.t
new file mode 100644 (file)
index 0000000..79fcc0a
--- /dev/null
+++ b/t/ar/ar.t
@@ -0,0 +1,207 @@
+use strict;
+use Test::More;
+
+use lib 't';
+use Support::TestSetup;
+use Carp;
+use Test::Exception;
+use SL::DB::TaxZone;
+use SL::DB::Buchungsgruppe;
+use SL::DB::Currency;
+use SL::DB::Customer;
+use SL::DB::Employee;
+use SL::DB::Invoice;
+use SL::DATEV qw(:CONSTANTS);
+use Data::Dumper;
+
+
+my ($i, $customer, $vendor, $currency_id, @parts, $buchungsgruppe, $buchungsgruppe7, $unit, $employee, $ar_tax_19, $ar_tax_7,$ar_tax_0, $taxzone);
+my ($ar_chart,$bank,$ar_amount_chart);
+my $config = {};
+$config->{numberformat} = '1.000,00';
+
+sub reset_state {
+  my %params = @_;
+
+  $params{$_} ||= {} for qw(buchungsgruppe vendor customer ar_tax_19 ar_tax_7 ar_tax_0 );
+
+  clear_up();
+
+  $employee        = SL::DB::Manager::Employee->current                                                || croak "No employee";
+  $taxzone         = SL::DB::Manager::TaxZone->find_by( description => 'Inland')                       || croak "No taxzone"; # only needed for setting customer/vendor
+  $ar_tax_19       = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19, %{ $params{ar_tax_19} }) || croak "No 19% tax";
+  $ar_tax_7        = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07, %{ $params{ar_tax_7} })  || croak "No 7% tax";
+  $ar_tax_0        = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00, %{ $params{ar_tax_0} })  || croak "No 0% tax";
+  $currency_id     = $::instance_conf->get_currency_id;
+
+  $customer   = SL::DB::Customer->new(
+    name        => 'Test Customer foo',
+    currency_id => $currency_id,
+    taxzone_id  => $taxzone->id,
+  )->save;
+
+  $ar_chart        = SL::DB::Manager::Chart->find_by( accno => '1400' ); # Forderungen
+  $bank            = SL::DB::Manager::Chart->find_by( accno => '1200' ); # Bank
+  $ar_amount_chart = SL::DB::Manager::Chart->find_by( accno => '8590' ); # verrechn., eigentlich Anzahlungen
+
+};
+
+sub ar {
+  reset_state;
+  my %params = @_;
+
+  my $amount = $params{amount};
+  my $customer = $params{customer};
+  my $date = $params{date} || DateTime->today;
+  my $with_payment = $params{with_payment} || 0;
+
+  # SL::DB::Invoice has a _before_save_set_invnumber hook, so we don't need to pass invnumber
+  my $invoice = SL::DB::Invoice->new(
+      invoice          => 0,
+      amount           => $amount,
+      netamount        => $amount,
+      transdate        => $date,
+      taxincluded      => 'f',
+      customer_id      => $customer->id,
+      taxzone_id       => $customer->taxzone_id,
+      currency_id      => $customer->currency_id,
+      globalproject_id => $params{project},
+      notes            => $params{notes},
+      transactions     => [],
+  );
+
+  my $db = $invoice->db;
+
+  $db->do_transaction( sub {
+
+  my $tax = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0);
+
+  $invoice->add_ar_amount_row(
+    amount     => $amount / 2,
+    chart      => $ar_amount_chart,
+    tax_id     => $tax->id,
+  );
+  $invoice->add_ar_amount_row(
+    amount     => $amount / 2,
+    chart      => $ar_amount_chart,
+    tax_id     => $tax->id,
+  );
+
+  $invoice->create_ar_row( chart => $ar_chart );
+
+  _save_and_pay_and_check(invoice => $invoice, bank => $bank, pay => 1, check => 1);
+
+  }) || die "something went wrong: " . $db->error;
+  return $invoice->invnumber;
+};
+
+sub ar_with_tax {
+  my %params = @_;
+
+  my $amount       = $params{amount};
+  my $customer     = $params{customer};
+  my $date         = $params{date} || DateTime->today;
+  my $with_payment = $params{with_payment} || 0;
+
+  my $invoice = SL::DB::Invoice->new(
+      invoice          => 0,
+      amount           => $amount,
+      netamount        => $amount,
+      transdate        => $date,
+      taxincluded      => 'f',
+      customer_id      => $customer->id,
+      taxzone_id       => $customer->taxzone_id,
+      currency_id      => $customer->currency_id,
+      globalproject_id => $params{project},
+      notes            => $params{notes},
+      transactions     => [],
+  );
+
+  my $db = $invoice->db;
+
+  $db->do_transaction( sub {
+
+  # TODO: check for currency and exchange rate
+
+  my $tax = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19 );
+  my $tax_id = $tax->id or die "can't find tax";
+
+  $invoice->add_ar_amount_row(
+    amount     => $amount / 2,
+    chart      => $ar_amount_chart,
+    tax_id     => $tax_id,
+  );
+  $invoice->add_ar_amount_row(
+    amount     => $amount / 2,
+    chart      => $ar_amount_chart,
+    tax_id     => $tax_id,
+  );
+
+  $invoice->create_ar_row( chart => $ar_chart );
+  _save_and_pay_and_check(invoice => $invoice, bank => $bank, pay => 1, check => 1);
+
+  }) || die "something went wrong: " . $db->error;
+  return $invoice->invnumber;
+};
+
+Support::TestSetup::login();
+
+reset_state();
+
+# check ar without tax
+my $invnumber  = ar(customer => $customer, amount => 100, with_payment => 0 , notes => 'ar without tax');
+my $inv = SL::DB::Manager::Invoice->find_by(invnumber => $invnumber);
+my $number_of_acc_trans = scalar @{ $inv->transactions };
+is($::form->round_amount($inv->amount), 100,  "invoice_amount = 100");
+is($number_of_acc_trans, 5,  "number of transactions");
+is($inv->datepaid->to_kivitendo, DateTime->today->to_kivitendo,  "datepaid");
+is($inv->amount - $inv->paid, 0 ,  "paid = amount ");
+
+# check ar with tax
+my $invnumber2 = ar_with_tax(customer => $customer, amount => 200, with_payment => 0, notes => 'ar with taxincluded');
+my $inv_with_tax = SL::DB::Manager::Invoice->find_by(invnumber => $invnumber2);
+die unless $inv_with_tax;
+is(scalar @{ $inv_with_tax->transactions } , 7,  "number of transactions for inv_with_tax");
+
+# general checks
+is(SL::DB::Manager::Invoice->get_all_count(), 2,  "total number of invoices created is 2");
+done_testing;
+
+clear_up();
+
+1;
+
+sub clear_up {
+  SL::DB::Manager::AccTransaction->delete_all(all => 1);
+  SL::DB::Manager::Invoice->delete_all(       all => 1);
+  SL::DB::Manager::Customer->delete_all(      all => 1);
+};
+
+sub _save_and_pay_and_check {
+  my %params = @_;
+  my $invoice = $params{invoice};
+  my $datev_check = 1;
+
+  my $return = $invoice->save;
+
+  $invoice->pay_invoice(chart_id     => $params{bank}->id,
+                        amount       => $invoice->amount,
+                        transdate    => $invoice->transdate->to_kivitendo,
+                        payment_type => 'without_skonto',  # default if not specified
+                       ) if $params{pay};
+
+  if ($datev_check) {
+    my $datev = SL::DATEV->new(
+      exporttype => DATEV_ET_BUCHUNGEN,
+      format     => DATEV_FORMAT_KNE,
+      dbh        => $invoice->db->dbh,
+      trans_id   => $invoice->id,
+    );
+
+    $datev->export;
+    if ($datev->errors) {
+      $invoice->db->dbh->rollback;
+      die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
+    }
+  };
+};