Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 4f43ec85

Von Kivitendo Admin vor fast 9 Jahren hinzugefügt

  • ID 4f43ec85816b8f5c9031ff35adfa9e0d33c4306f
  • Vorgänger 6418adee
  • Nachfolger 3f924c0f

Neue Methoden um Debitorenbuchungen zu erstellen

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

Unterschiede anzeigen:

SL/DB/Invoice.pm
3 3
use strict;
4 4

  
5 5
use Carp;
6
use List::Util qw(first);
6
use List::Util qw(first sum);
7 7

  
8
use Rose::DB::Object::Helpers ();
8
use Rose::DB::Object::Helpers qw(has_loaded_related);
9 9
use SL::DB::MetaSetup::Invoice;
10 10
use SL::DB::Manager::Invoice;
11 11
use SL::DB::Helper::Payment qw(:ALL);
......
282 282
  }
283 283
}
284 284

  
285
sub add_ar_amount_row {
286
  my ($self, %params ) = @_;
287

  
288
  # only allow this method for ar invoices (Debitorenbuchung)
289
  die "not an ar invoice" if $self->invoice and not $self->customer_id;
290

  
291
  die "add_ar_amount_row needs a chart object as chart param" unless $params{chart} && $params{chart}->isa('SL::DB::Chart');
292
  die unless $params{chart}->link =~ /AR_amount/;
293

  
294
  my $acc_trans = [];
295

  
296
  my $roundplaces = 2;
297
  my ($netamount,$taxamount);
298

  
299
  $netamount = $params{amount} * 1;
300
  my $tax = SL::DB::Manager::Tax->find_by(id => $params{tax_id}) || die "Can't find tax with id " . $params{tax_id};
301

  
302
  if ( $tax and $tax->rate != 0 ) {
303
    ($netamount, $taxamount) = Form->calculate_tax($params{amount}, $tax->rate, $self->taxincluded, $roundplaces);
304
  };
305
  next unless $netamount; # netamount mustn't be zero
306

  
307
  my $sign = $self->customer_id ? 1 : -1;
308
  my $acc = SL::DB::AccTransaction->new(
309
    amount     => $netamount * $sign,
310
    chart_id   => $params{chart}->id,
311
    chart_link => $params{chart}->link,
312
    transdate  => $self->transdate,
313
    taxkey     => $tax->taxkey,
314
    tax_id     => $tax->id,
315
    project_id => $params{project_id},
316
  );
317

  
318
  $self->add_transactions( $acc );
319
  push( @$acc_trans, $acc );
320

  
321
  if ( $taxamount ) {
322
     my $acc = SL::DB::AccTransaction->new(
323
       amount     => $taxamount * $sign,
324
       chart_id   => $tax->chart_id,
325
       chart_link => $tax->chart->link,
326
       transdate  => $self->transdate,
327
       taxkey     => $tax->taxkey,
328
       tax_id     => $tax->id,
329
     );
330
     $self->add_transactions( $acc );
331
     push( @$acc_trans, $acc );
332
  };
333
  return $acc_trans;
334
};
335

  
336
sub create_ar_row {
337
  my ($self, %params) = @_;
338
  # to be called after adding all AR_amount rows, adds an AR row
339

  
340
  # only allow this method for ar invoices (Debitorenbuchung)
341
  die if $self->invoice and not $self->customer_id;
342
  die "create_ar_row needs a chart object as a parameter" unless $params{chart} and ref($params{chart}) eq 'SL::DB::Chart';
343

  
344
  my @transactions = @{$self->transactions};
345
  # die "invoice has no acc_transactions" unless scalar @transactions > 0;
346
  return 0 unless scalar @transactions > 0;
347

  
348
  my $chart = $params{chart} || SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ar_chart_id);
349
  die "illegal chart in create_ar_row" unless $chart;
350

  
351
  die "receivables chart must have link 'AR'" unless $chart->link eq 'AR';
352

  
353
  my $acc_trans = [];
354

  
355
  # hardcoded entry for no tax: tax_id and taxkey should be 0
356
  my $tax = SL::DB::Manager::Tax->find_by(id => 0, taxkey => 0) || die "Can't find tax with id 0 and taxkey 0";
357

  
358
  my $sign = $self->customer_id ? -1 : 1;
359
  my $acc = SL::DB::AccTransaction->new(
360
    amount     => $self->amount * $sign,
361
    chart_id   => $params{chart}->id,
362
    chart_link => $params{chart}->link,
363
    transdate  => $self->transdate,
364
    taxkey     => $tax->taxkey,
365
    tax_id     => $tax->id,
366
  );
367
  $self->add_transactions( $acc );
368
  push( @$acc_trans, $acc );
369
  return $acc_trans;
370
};
371

  
372
sub validate_acc_trans {
373
  my ($self, %params) = @_;
374
  # should be able to check unsaved invoice objects with several acc_trans lines
375

  
376
  die "validate_acc_trans can't check invoice object with empty transactions" unless $self->transactions;
377

  
378
  my @transactions = @{$self->transactions};
379
  # die "invoice has no acc_transactions" unless scalar @transactions > 0;
380
  return 0 unless scalar @transactions > 0;
381
  return 0 unless $self->has_loaded_related('transactions');
382
  if ( $params{debug} ) {
383
    printf("starting validatation of invoice %s with trans_id %s and taxincluded %s\n", $self->invnumber, $self->id, $self->taxincluded);
384
    foreach my $acc ( @transactions ) {
385
      printf("chart: %s  amount: %s   tax_id: %s  link: %s\n", $acc->chart->accno, $acc->amount, $acc->tax_id, $acc->chart->link);
386
    };
387
  };
388

  
389
  my $acc_trans_sum = sum map { $_->amount } @transactions;
390

  
391
  unless ( $::form->round_amount($acc_trans_sum, 10) == 0 ) {
392
    my $string = "sum of acc_transactions isn't 0: $acc_trans_sum\n";
393

  
394
    if ( $params{debug} ) {
395
      foreach my $trans ( @transactions ) {
396
          $string .= sprintf("  %s %s %s\n", $trans->chart->accno, $trans->taxkey, $trans->amount);
397
      };
398
    };
399
    return 0;
400
  };
401

  
402
  # only use the first AR entry, so it also works for paid invoices
403
  my @ar_transactions = map { $_->amount } grep { $_->chart_link eq 'AR' } @transactions;
404
  my $ar_sum = $ar_transactions[0];
405
  # my $ar_sum = sum map { $_->amount } grep { $_->chart_link eq 'AR' } @transactions;
406

  
407
  unless ( $::form->round_amount($ar_sum * -1,2) == $::form->round_amount($self->amount,2) ) {
408
    if ( $params{debug} ) {
409
      printf("debug: (ar_sum) %s = %s (amount)\n",  $::form->round_amount($ar_sum * -1,2) , $::form->round_amount($self->amount, 2) );
410
      foreach my $trans ( @transactions ) {
411
        printf("  %s %s %s %s\n", $trans->chart->accno, $trans->taxkey, $trans->amount, $trans->chart->link);
412
      };
413
    };
414
    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));
415
  };
416

  
417
  return 1;
418
};
419

  
420
sub recalculate_amounts {
421
  my ($self, %params) = @_;
422
  # calculate and set amount and netamount from acc_trans objects
423

  
424
  croak ("Can only recalculate amounts for ar transactions") if $self->invoice;
425

  
426
  return undef unless $self->has_loaded_related('transactions');
427

  
428
  my ($netamount, $taxamount);
429

  
430
  my @transactions = @{$self->transactions};
431

  
432
  foreach my $acc ( @transactions ) {
433
    $netamount += $acc->amount if $acc->chart->link =~ /AR_amount/;
434
    $taxamount += $acc->amount if $acc->chart->link =~ /AR_tax/;
435
  };
436

  
437
  $self->amount($netamount+$taxamount);
438
  $self->netamount($netamount);
439
};
440

  
441

  
285 442
sub _post_create_assemblyitem_entries {
286 443
  my ($self, $assembly_entries) = @_;
287 444

  
......
502 659

  
503 660
See L<SL::DB::Object::basic_info>.
504 661

  
662
=item C<recalculate_amounts %params>
663

  
664
Calculate and set amount and netamount from acc_trans objects by summing up the
665
values of acc_trans objects with AR_amount and AR_tax link charts.
666
amount and netamount are set to the calculated values.
667

  
668
=item C<validate_acc_trans>
669

  
670
Checks if the sum of all associated acc_trans objects is 0 and checks whether
671
the amount of the AR acc_transaction matches the AR amount. Only the first AR
672
line is checked, because the sum of all AR lines is 0 for paid invoices.
673

  
674
Returns 0 or 1.
675

  
676
Can be called with a debug parameter which writes debug info to STDOUT, which is
677
useful in console mode or while writing tests.
678

  
679
 my $ar = SL::DB::Manager::Invoice->get_first();
680
 $ar->validate_acc_trans(debug => 1);
681

  
682
=item C<create_ar_row %params>
683

  
684
Creates a new acc_trans entry for the receivable (AR) entry of an existing AR
685
invoice object, which already has some income and tax acc_trans entries.
686

  
687
The acc_trans entry is also returned inside an array ref.
688

  
689
Mandatory params are
690

  
691
=over 2
692

  
693
=item * chart as an RDBO object, e.g. for bank. Must be a 'paid' chart.
694

  
695
=back
696

  
697
Currently the amount of the invoice object is used for the acc_trans amount.
698
Use C<recalculate_amounts> before calling this mehtod if amount it isn't known
699
yet or you didn't set it manually.
700

  
701
=item C<add_ar_amount_row %params>
702

  
703
Add a new entry for an existing AR invoice object. Creates an acc_trans entry,
704
and also adds an acc_trans tax entry, if the tax has an associated tax chart.
705
Also all acc_trans entries that were created are returned inside an array ref.
706

  
707
Mandatory params are
708

  
709
=over 2
710

  
711
=item * chart as an RDBO object, should be an income chart (link = AR_amount)
712

  
713
=item * tax_id
714

  
715
=item * amount
716

  
717
=back
718

  
505 719
=back
506 720

  
507 721
=head1 TODO
t/ar/ar.t
1
use strict;
2
use Test::More;
3

  
4
use lib 't';
5
use Support::TestSetup;
6
use Carp;
7
use Test::Exception;
8
use SL::DB::TaxZone;
9
use SL::DB::Buchungsgruppe;
10
use SL::DB::Currency;
11
use SL::DB::Customer;
12
use SL::DB::Employee;
13
use SL::DB::Invoice;
14
use SL::DATEV qw(:CONSTANTS);
15
use Data::Dumper;
16

  
17

  
18
my ($i, $customer, $vendor, $currency_id, @parts, $buchungsgruppe, $buchungsgruppe7, $unit, $employee, $ar_tax_19, $ar_tax_7,$ar_tax_0, $taxzone);
19
my ($ar_chart,$bank,$ar_amount_chart);
20
my $config = {};
21
$config->{numberformat} = '1.000,00';
22

  
23
sub reset_state {
24
  my %params = @_;
25

  
26
  $params{$_} ||= {} for qw(buchungsgruppe vendor customer ar_tax_19 ar_tax_7 ar_tax_0 );
27

  
28
  clear_up();
29

  
30
  $employee        = SL::DB::Manager::Employee->current                                                || croak "No employee";
31
  $taxzone         = SL::DB::Manager::TaxZone->find_by( description => 'Inland')                       || croak "No taxzone"; # only needed for setting customer/vendor
32
  $ar_tax_19       = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19, %{ $params{ar_tax_19} }) || croak "No 19% tax";
33
  $ar_tax_7        = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07, %{ $params{ar_tax_7} })  || croak "No 7% tax";
34
  $ar_tax_0        = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00, %{ $params{ar_tax_0} })  || croak "No 0% tax";
35
  $currency_id     = $::instance_conf->get_currency_id;
36

  
37
  $customer   = SL::DB::Customer->new(
38
    name        => 'Test Customer foo',
39
    currency_id => $currency_id,
40
    taxzone_id  => $taxzone->id,
41
  )->save;
42

  
43
  $ar_chart        = SL::DB::Manager::Chart->find_by( accno => '1400' ); # Forderungen
44
  $bank            = SL::DB::Manager::Chart->find_by( accno => '1200' ); # Bank
45
  $ar_amount_chart = SL::DB::Manager::Chart->find_by( accno => '8590' ); # verrechn., eigentlich Anzahlungen
46

  
47
};
48

  
49
sub ar {
50
  reset_state;
51
  my %params = @_;
52

  
53
  my $amount = $params{amount};
54
  my $customer = $params{customer};
55
  my $date = $params{date} || DateTime->today;
56
  my $with_payment = $params{with_payment} || 0;
57

  
58
  # SL::DB::Invoice has a _before_save_set_invnumber hook, so we don't need to pass invnumber
59
  my $invoice = SL::DB::Invoice->new(
60
      invoice          => 0,
61
      amount           => $amount,
62
      netamount        => $amount,
63
      transdate        => $date,
64
      taxincluded      => 'f',
65
      customer_id      => $customer->id,
66
      taxzone_id       => $customer->taxzone_id,
67
      currency_id      => $customer->currency_id,
68
      globalproject_id => $params{project},
69
      notes            => $params{notes},
70
      transactions     => [],
71
  );
72

  
73
  my $db = $invoice->db;
74

  
75
  $db->do_transaction( sub {
76

  
77
  my $tax = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0);
78

  
79
  $invoice->add_ar_amount_row(
80
    amount     => $amount / 2,
81
    chart      => $ar_amount_chart,
82
    tax_id     => $tax->id,
83
  );
84
  $invoice->add_ar_amount_row(
85
    amount     => $amount / 2,
86
    chart      => $ar_amount_chart,
87
    tax_id     => $tax->id,
88
  );
89

  
90
  $invoice->create_ar_row( chart => $ar_chart );
91

  
92
  _save_and_pay_and_check(invoice => $invoice, bank => $bank, pay => 1, check => 1);
93

  
94
  }) || die "something went wrong: " . $db->error;
95
  return $invoice->invnumber;
96
};
97

  
98
sub ar_with_tax {
99
  my %params = @_;
100

  
101
  my $amount       = $params{amount};
102
  my $customer     = $params{customer};
103
  my $date         = $params{date} || DateTime->today;
104
  my $with_payment = $params{with_payment} || 0;
105

  
106
  my $invoice = SL::DB::Invoice->new(
107
      invoice          => 0,
108
      amount           => $amount,
109
      netamount        => $amount,
110
      transdate        => $date,
111
      taxincluded      => 'f',
112
      customer_id      => $customer->id,
113
      taxzone_id       => $customer->taxzone_id,
114
      currency_id      => $customer->currency_id,
115
      globalproject_id => $params{project},
116
      notes            => $params{notes},
117
      transactions     => [],
118
  );
119

  
120
  my $db = $invoice->db;
121

  
122
  $db->do_transaction( sub {
123

  
124
  # TODO: check for currency and exchange rate
125

  
126
  my $tax = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19 );
127
  my $tax_id = $tax->id or die "can't find tax";
128

  
129
  $invoice->add_ar_amount_row(
130
    amount     => $amount / 2,
131
    chart      => $ar_amount_chart,
132
    tax_id     => $tax_id,
133
  );
134
  $invoice->add_ar_amount_row(
135
    amount     => $amount / 2,
136
    chart      => $ar_amount_chart,
137
    tax_id     => $tax_id,
138
  );
139

  
140
  $invoice->create_ar_row( chart => $ar_chart );
141
  _save_and_pay_and_check(invoice => $invoice, bank => $bank, pay => 1, check => 1);
142

  
143
  }) || die "something went wrong: " . $db->error;
144
  return $invoice->invnumber;
145
};
146

  
147
Support::TestSetup::login();
148

  
149
reset_state();
150

  
151
# check ar without tax
152
my $invnumber  = ar(customer => $customer, amount => 100, with_payment => 0 , notes => 'ar without tax');
153
my $inv = SL::DB::Manager::Invoice->find_by(invnumber => $invnumber);
154
my $number_of_acc_trans = scalar @{ $inv->transactions };
155
is($::form->round_amount($inv->amount), 100,  "invoice_amount = 100");
156
is($number_of_acc_trans, 5,  "number of transactions");
157
is($inv->datepaid->to_kivitendo, DateTime->today->to_kivitendo,  "datepaid");
158
is($inv->amount - $inv->paid, 0 ,  "paid = amount ");
159

  
160
# check ar with tax
161
my $invnumber2 = ar_with_tax(customer => $customer, amount => 200, with_payment => 0, notes => 'ar with taxincluded');
162
my $inv_with_tax = SL::DB::Manager::Invoice->find_by(invnumber => $invnumber2);
163
die unless $inv_with_tax;
164
is(scalar @{ $inv_with_tax->transactions } , 7,  "number of transactions for inv_with_tax");
165

  
166
# general checks
167
is(SL::DB::Manager::Invoice->get_all_count(), 2,  "total number of invoices created is 2");
168
done_testing;
169

  
170
clear_up();
171

  
172
1;
173

  
174
sub clear_up {
175
  SL::DB::Manager::AccTransaction->delete_all(all => 1);
176
  SL::DB::Manager::Invoice->delete_all(       all => 1);
177
  SL::DB::Manager::Customer->delete_all(      all => 1);
178
};
179

  
180
sub _save_and_pay_and_check {
181
  my %params = @_;
182
  my $invoice = $params{invoice};
183
  my $datev_check = 1;
184

  
185
  my $return = $invoice->save;
186

  
187
  $invoice->pay_invoice(chart_id     => $params{bank}->id,
188
                        amount       => $invoice->amount,
189
                        transdate    => $invoice->transdate->to_kivitendo,
190
                        payment_type => 'without_skonto',  # default if not specified
191
                       ) if $params{pay};
192

  
193
  if ($datev_check) {
194
    my $datev = SL::DATEV->new(
195
      exporttype => DATEV_ET_BUCHUNGEN,
196
      format     => DATEV_FORMAT_KNE,
197
      dbh        => $invoice->db->dbh,
198
      trans_id   => $invoice->id,
199
    );
200

  
201
    $datev->export;
202
    if ($datev->errors) {
203
      $invoice->db->dbh->rollback;
204
      die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
205
    }
206
  };
207
};

Auch abrufbar als: Unified diff