From 4f43ec85816b8f5c9031ff35adfa9e0d33c4306f Mon Sep 17 00:00:00 2001 From: "G. Richardson" Date: Mon, 29 Feb 2016 11:56:43 +0100 Subject: [PATCH] Neue Methoden um Debitorenbuchungen zu erstellen MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit 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 | 218 ++++++++++++++++++++++++++++++++++++++++++++++- t/ar/ar.t | 207 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 423 insertions(+), 2 deletions(-) create mode 100644 t/ar/ar.t diff --git a/SL/DB/Invoice.pm b/SL/DB/Invoice.pm index 70291ad61..2d6ab0734 100644 --- a/SL/DB/Invoice.pm +++ b/SL/DB/Invoice.pm @@ -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. +=item C + +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 + +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 + +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 before calling this mehtod if amount it isn't known +yet or you didn't set it manually. + +=item C + +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 index 000000000..79fcc0a4a --- /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; + } + }; +}; -- 2.20.1