Revision 4f43ec85
Von Kivitendo Admin vor fast 9 Jahren hinzugefügt
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
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