add_{ap|ar}_amount_row berücksichtigen gldate
[kivitendo-erp.git] / SL / DB / Invoice.pm
1 package SL::DB::Invoice;
2
3 use strict;
4
5 use Carp;
6 use List::Util qw(first sum);
7
8 use Rose::DB::Object::Helpers qw(has_loaded_related forget_related);
9 use SL::DB::MetaSetup::Invoice;
10 use SL::DB::Manager::Invoice;
11 use SL::DB::Helper::Payment qw(:ALL);
12 use SL::DB::Helper::AttrHTML;
13 use SL::DB::Helper::AttrSorted;
14 use SL::DB::Helper::FlattenToForm;
15 use SL::DB::Helper::LinkedRecords;
16 use SL::DB::Helper::PriceTaxCalculator;
17 use SL::DB::Helper::PriceUpdater;
18 use SL::DB::Helper::TransNumberGenerator;
19 use SL::Locale::String qw(t8);
20 use SL::DB::CustomVariable;
21
22 __PACKAGE__->meta->add_relationship(
23   invoiceitems => {
24     type         => 'one to many',
25     class        => 'SL::DB::InvoiceItem',
26     column_map   => { id => 'trans_id' },
27     manager_args => {
28       with_objects => [ 'part' ]
29     }
30   },
31   storno_invoices => {
32     type          => 'one to many',
33     class         => 'SL::DB::Invoice',
34     column_map    => { id => 'storno_id' },
35   },
36   sepa_export_items => {
37     type            => 'one to many',
38     class           => 'SL::DB::SepaExportItem',
39     column_map      => { id => 'ar_id' },
40     manager_args    => { with_objects => [ 'sepa_export' ] }
41   },
42   sepa_exports      => {
43     type            => 'many to many',
44     map_class       => 'SL::DB::SepaExportItem',
45     map_from        => 'ar',
46     map_to          => 'sepa_export',
47   },
48   custom_shipto     => {
49     type            => 'one to one',
50     class           => 'SL::DB::Shipto',
51     column_map      => { id => 'trans_id' },
52     query_args      => [ module => 'AR' ],
53   },
54   transactions   => {
55     type         => 'one to many',
56     class        => 'SL::DB::AccTransaction',
57     column_map   => { id => 'trans_id' },
58     manager_args => {
59       with_objects => [ 'chart' ],
60       sort_by      => 'acc_trans_id ASC',
61     },
62   },
63   dunnings       => {
64     type         => 'one to many',
65     class        => 'SL::DB::Dunning',
66     column_map   => { id => 'trans_id' },
67     manager_args => { with_objects => [ 'dunnings' ] }
68   },
69 );
70
71 __PACKAGE__->meta->initialize;
72
73 __PACKAGE__->attr_html('notes');
74 __PACKAGE__->attr_sorted('items');
75
76 __PACKAGE__->before_save('_before_save_set_invnumber');
77
78 # hooks
79
80 sub _before_save_set_invnumber {
81   my ($self) = @_;
82
83   $self->create_trans_number if !$self->invnumber;
84
85   return 1;
86 }
87
88 # methods
89
90 sub items { goto &invoiceitems; }
91 sub add_items { goto &add_invoiceitems; }
92 sub record_number { goto &invnumber; };
93
94 sub is_sales {
95   # For compatibility with Order, DeliveryOrder
96   croak 'not an accessor' if @_ > 1;
97   return 1;
98 }
99
100 # it is assumed, that ordnumbers are unique here.
101 sub first_order_by_ordnumber {
102   my $self = shift;
103
104   my $orders = SL::DB::Manager::Order->get_all(
105     query => [
106       ordnumber => $self->ordnumber,
107
108     ],
109   );
110
111   return first { $_->is_type('sales_order') } @{ $orders };
112 }
113
114 sub abschlag_percentage {
115   my $self         = shift;
116   my $order        = $self->first_order_by_ordnumber or return;
117   my $order_amount = $order->netamount               or return;
118   return $self->abschlag
119     ? $self->netamount / $order_amount
120     : undef;
121 }
122
123 sub taxamount {
124   my $self = shift;
125   die 'not a setter method' if @_;
126
127   return ($self->amount || 0) - ($self->netamount || 0);
128 }
129
130 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
131
132 sub closed {
133   my ($self) = @_;
134   return $self->paid >= $self->amount;
135 }
136
137 sub _clone_orderitem_delivery_order_item_cvar {
138   my ($cvar) = @_;
139
140   my $cloned = $_->clone_and_reset;
141   $cloned->sub_module('invoice');
142
143   return $cloned;
144 }
145
146 sub new_from {
147   my ($class, $source, %params) = @_;
148
149   croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
150   croak("Cannot create invoices for purchase records")           unless $source->customer_id;
151
152   require SL::DB::Employee;
153
154   my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
155
156   if (ref($source) eq 'SL::DB::Order') {
157     @columns      = qw(quonumber delivery_customer_id delivery_vendor_id);
158     @item_columns = qw(subtotal);
159
160     $item_parent_id_column = 'trans_id';
161     $item_parent_column    = 'order';
162
163   } else {
164     @columns      = qw(donumber);
165
166     $item_parent_id_column = 'delivery_order_id';
167     $item_parent_column    = 'delivery_order';
168   }
169
170   my $terms = $source->can('payment_id') ? $source->payment_terms : undef;
171   $terms = $source->customer->payment_terms if !defined $terms && $source->customer;
172
173   my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
174                                                 cp_id language_id taxzone_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
175                transdate   => DateTime->today_local,
176                gldate      => DateTime->today_local,
177                duedate     => $terms ? $terms->calc_date(reference_date => DateTime->today_local) : DateTime->today_local,
178                invoice     => 1,
179                type        => 'invoice',
180                storno      => 0,
181                paid        => 0,
182                employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
183             );
184
185   $args{payment_id} = ( $terms ? $terms->id : $source->payment_id);
186
187   if ($source->type =~ /_delivery_order$/) {
188     $args{deliverydate} = $source->reqdate;
189     if (my $order = SL::DB::Manager::Order->find_by(ordnumber => $source->ordnumber)) {
190       $args{orddate}    = $order->transdate;
191     }
192
193   } elsif ($source->type =~ /_order$/) {
194     $args{deliverydate} = $source->reqdate;
195     $args{orddate}      = $source->transdate;
196
197   } else {
198     $args{quodate}      = $source->transdate;
199   }
200
201   # Custom shipto addresses (the ones specific to the sales/purchase
202   # record and not to the customer/vendor) are only linked from shipto
203   # → ar. Meaning ar.shipto_id will not be filled in that
204   # case.
205   if (!$source->shipto_id && $source->id) {
206     $args{custom_shipto} = $source->custom_shipto->clone($class) if $source->can('custom_shipto') && $source->custom_shipto;
207
208   } else {
209     $args{shipto_id} = $source->shipto_id;
210   }
211
212   my $invoice = $class->new(%args);
213   $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
214   my $items   = delete($params{items}) || $source->items_sorted;
215   my %item_parents;
216
217   my @items = map {
218     my $source_item      = $_;
219     my $source_item_id   = $_->$item_parent_id_column;
220     my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
221
222     $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
223     my $item_parent                  = $item_parents{$source_item_id};
224     my $current_invoice_item =
225       SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
226                                    qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
227                                       base_qty longdescription lastcost price_factor_id active_discount_source active_price_source), @item_columns),
228                                deliverydate     => $source_item->reqdate,
229                                fxsellprice      => $source_item->sellprice,
230                                custom_variables => \@custom_variables,
231                                ordnumber        => ref($item_parent) eq 'SL::DB::Order'         ? $item_parent->ordnumber : $source_item->ordnumber,
232                                donumber         => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber  : $source_item->can('donumber') ? $source_item->donumber : '',
233                              );
234
235     $current_invoice_item->{"converted_from_orderitems_id"}           = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
236     $current_invoice_item->{"converted_from_delivery_order_items_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::DeliveryOrder';
237     $current_invoice_item;
238   } @{ $items };
239
240   @items = grep { $params{item_filter}->($_) } @items if $params{item_filter};
241   @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
242   @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
243
244   $invoice->invoiceitems(\@items);
245
246   return $invoice;
247 }
248
249 sub post {
250   my ($self, %params) = @_;
251
252   die "not an invoice" unless $self->invoice;
253
254   require SL::DB::Chart;
255   if (!$params{ar_id}) {
256     my $chart;
257     if ($::instance_conf->get_ar_chart_id) {
258       $chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ar_chart_id);
259     } else {
260       $chart = SL::DB::Manager::Chart->get_all(query   => [ SL::DB::Manager::Chart->link_filter('AR') ],
261                                                sort_by => 'id ASC',
262                                                limit   => 1)->[0];
263     };
264     croak("No AR chart found and no parameter 'ar_id' given") unless $chart;
265     $params{ar_id} = $chart->id;
266   }
267
268   if (!$self->db->with_transaction(sub {
269     my %data = $self->calculate_prices_and_taxes;
270
271     $self->_post_create_assemblyitem_entries($data{assembly_items});
272     $self->save;
273
274     $self->_post_add_acctrans($data{amounts_cogs});
275     $self->_post_add_acctrans($data{amounts});
276     $self->_post_add_acctrans($data{taxes});
277
278     $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
279
280     $self->_post_update_allocated($data{allocated});
281
282     $self->_post_book_rounding($data{rounding});
283
284     1;
285   })) {
286     $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
287     return undef;
288   }
289
290   return $self;
291 }
292
293 sub _post_add_acctrans {
294   my ($self, $entries) = @_;
295
296   my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
297   my $chart_link;
298
299   require SL::DB::AccTransaction;
300   require SL::DB::Chart;
301   while (my ($chart_id, $spec) = each %{ $entries }) {
302     $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
303     $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
304     $chart_link ||= '';
305
306     SL::DB::AccTransaction->new(trans_id   => $self->id,
307                                 chart_id   => $chart_id,
308                                 amount     => $spec->{amount},
309                                 tax_id     => $spec->{tax_id},
310                                 taxkey     => $spec->{taxkey},
311                                 project_id => $self->globalproject_id,
312                                 transdate  => $self->transdate,
313                                 chart_link => $chart_link)->save;
314   }
315 }
316
317 sub _post_book_rounding {
318   my ($self, $rounding) = @_;
319
320   my $tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
321   my $rnd_accno = $rounding == 0 ? 0
322                 : $rounding > 0  ? SL::DB::Default->get->rndgain_accno_id
323                 :                  SL::DB::Default->get->rndloss_accno_id
324   ;
325   if ($rnd_accno != 0) {
326     SL::DB::AccTransaction->new(trans_id   => $self->id,
327                                 chart_id   => $rnd_accno,
328                                 amount     => $rounding,
329                                 tax_id     => $tax_id,
330                                 taxkey     => 0,
331                                 project_id => $self->globalproject_id,
332                                 transdate  => $self->transdate,
333                                 chart_link => $rnd_accno)->save;
334   }
335 }
336
337 sub add_ar_amount_row {
338   my ($self, %params ) = @_;
339
340   # only allow this method for ar invoices (Debitorenbuchung)
341   die "not an ar invoice" if $self->invoice and not $self->customer_id;
342
343   die "add_ar_amount_row needs a chart object as chart param" unless $params{chart} && $params{chart}->isa('SL::DB::Chart');
344   die "chart must be an AR_amount chart" unless $params{chart}->link =~ /AR_amount/;
345
346   my $acc_trans = [];
347
348   my $roundplaces = 2;
349   my ($netamount,$taxamount);
350
351   $netamount = $params{amount} * 1;
352   my $tax = SL::DB::Manager::Tax->find_by(id => $params{tax_id}) || die "Can't find tax with id " . $params{tax_id};
353
354   if ( $tax and $tax->rate != 0 ) {
355     ($netamount, $taxamount) = Form->calculate_tax($params{amount}, $tax->rate, $self->taxincluded, $roundplaces);
356   };
357   next unless $netamount; # netamount mustn't be zero
358
359   my $sign = $self->customer_id ? 1 : -1;
360   my $acc = SL::DB::AccTransaction->new(
361     amount     => $netamount * $sign,
362     chart_id   => $params{chart}->id,
363     chart_link => $params{chart}->link,
364     transdate  => $self->transdate,
365     gldate     => $self->gldate,
366     taxkey     => $tax->taxkey,
367     tax_id     => $tax->id,
368     project_id => $params{project_id},
369   );
370
371   $self->add_transactions( $acc );
372   push( @$acc_trans, $acc );
373
374   if ( $taxamount ) {
375      my $acc = SL::DB::AccTransaction->new(
376        amount     => $taxamount * $sign,
377        chart_id   => $tax->chart_id,
378        chart_link => $tax->chart->link,
379        transdate  => $self->transdate,
380        gldate     => $self->gldate,
381        taxkey     => $tax->taxkey,
382        tax_id     => $tax->id,
383      );
384      $self->add_transactions( $acc );
385      push( @$acc_trans, $acc );
386   };
387   return $acc_trans;
388 };
389
390 sub create_ar_row {
391   my ($self, %params) = @_;
392   # to be called after adding all AR_amount rows, adds an AR row
393
394   # only allow this method for ar invoices (Debitorenbuchung)
395   die if $self->invoice and not $self->customer_id;
396   die "create_ar_row needs a chart object as a parameter" unless $params{chart} and ref($params{chart}) eq 'SL::DB::Chart';
397
398   my @transactions = @{$self->transactions};
399   # die "invoice has no acc_transactions" unless scalar @transactions > 0;
400   return 0 unless scalar @transactions > 0;
401
402   my $chart = $params{chart} || SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ar_chart_id);
403   die "illegal chart in create_ar_row" unless $chart;
404
405   die "receivables chart must have link 'AR'" unless $chart->link eq 'AR';
406
407   my $acc_trans = [];
408
409   # hardcoded entry for no tax: tax_id and taxkey should be 0
410   my $tax = SL::DB::Manager::Tax->find_by(id => 0, taxkey => 0) || die "Can't find tax with id 0 and taxkey 0";
411
412   my $sign = $self->customer_id ? -1 : 1;
413   my $acc = SL::DB::AccTransaction->new(
414     amount     => $self->amount * $sign,
415     chart_id   => $params{chart}->id,
416     chart_link => $params{chart}->link,
417     transdate  => $self->transdate,
418     taxkey     => $tax->taxkey,
419     tax_id     => $tax->id,
420   );
421   $self->add_transactions( $acc );
422   push( @$acc_trans, $acc );
423   return $acc_trans;
424 };
425
426 sub validate_acc_trans {
427   my ($self, %params) = @_;
428   # should be able to check unsaved invoice objects with several acc_trans lines
429
430   die "validate_acc_trans can't check invoice object with empty transactions" unless $self->transactions;
431
432   my @transactions = @{$self->transactions};
433   # die "invoice has no acc_transactions" unless scalar @transactions > 0;
434   return 0 unless scalar @transactions > 0;
435   return 0 unless $self->has_loaded_related('transactions');
436   if ( $params{debug} ) {
437     printf("starting validatation of invoice %s with trans_id %s and taxincluded %s\n", $self->invnumber, $self->id, $self->taxincluded);
438     foreach my $acc ( @transactions ) {
439       printf("chart: %s  amount: %s   tax_id: %s  link: %s\n", $acc->chart->accno, $acc->amount, $acc->tax_id, $acc->chart->link);
440     };
441   };
442
443   my $acc_trans_sum = sum map { $_->amount } @transactions;
444
445   unless ( $::form->round_amount($acc_trans_sum, 10) == 0 ) {
446     my $string = "sum of acc_transactions isn't 0: $acc_trans_sum\n";
447
448     if ( $params{debug} ) {
449       foreach my $trans ( @transactions ) {
450           $string .= sprintf("  %s %s %s\n", $trans->chart->accno, $trans->taxkey, $trans->amount);
451       };
452     };
453     return 0;
454   };
455
456   # only use the first AR entry, so it also works for paid invoices
457   my @ar_transactions = map { $_->amount } grep { $_->chart_link eq 'AR' } @transactions;
458   my $ar_sum = $ar_transactions[0];
459   # my $ar_sum = sum map { $_->amount } grep { $_->chart_link eq 'AR' } @transactions;
460
461   unless ( $::form->round_amount($ar_sum * -1,2) == $::form->round_amount($self->amount,2) ) {
462     if ( $params{debug} ) {
463       printf("debug: (ar_sum) %s = %s (amount)\n",  $::form->round_amount($ar_sum * -1,2) , $::form->round_amount($self->amount, 2) );
464       foreach my $trans ( @transactions ) {
465         printf("  %s %s %s %s\n", $trans->chart->accno, $trans->taxkey, $trans->amount, $trans->chart->link);
466       };
467     };
468     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));
469   };
470
471   return 1;
472 };
473
474 sub recalculate_amounts {
475   my ($self, %params) = @_;
476   # calculate and set amount and netamount from acc_trans objects
477
478   croak ("Can only recalculate amounts for ar transactions") if $self->invoice;
479
480   return undef unless $self->has_loaded_related('transactions');
481
482   my ($netamount, $taxamount);
483
484   my @transactions = @{$self->transactions};
485
486   foreach my $acc ( @transactions ) {
487     $netamount += $acc->amount if $acc->chart->link =~ /AR_amount/;
488     $taxamount += $acc->amount if $acc->chart->link =~ /AR_tax/;
489   };
490
491   $self->amount($netamount+$taxamount);
492   $self->netamount($netamount);
493 };
494
495
496 sub _post_create_assemblyitem_entries {
497   my ($self, $assembly_entries) = @_;
498
499   my $items = $self->invoiceitems;
500   my @new_items;
501
502   my $item_idx = 0;
503   foreach my $item (@{ $items }) {
504     next if $item->assemblyitem;
505
506     push @new_items, $item;
507     $item_idx++;
508
509     foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
510       push @new_items, SL::DB::InvoiceItem->new(parts_id     => $assembly_item->{part},
511                                                 description  => $assembly_item->{part}->description,
512                                                 unit         => $assembly_item->{part}->unit,
513                                                 qty          => $assembly_item->{qty},
514                                                 allocated    => $assembly_item->{allocated},
515                                                 sellprice    => 0,
516                                                 fxsellprice  => 0,
517                                                 assemblyitem => 't');
518     }
519   }
520
521   $self->invoiceitems(\@new_items);
522 }
523
524 sub _post_update_allocated {
525   my ($self, $allocated) = @_;
526
527   while (my ($invoice_id, $diff) = each %{ $allocated }) {
528     SL::DB::Manager::InvoiceItem->update_all(set   => { allocated => { sql => "allocated + $diff" } },
529                                              where => [ id        => $invoice_id ]);
530   }
531 }
532
533 sub invoice_type {
534   my ($self) = @_;
535
536   return 'ar_transaction'     if !$self->invoice;
537   return 'credit_note'        if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
538   return 'invoice_storno'     if $self->type ne 'credit_note' && $self->amount < 0 &&  $self->storno;
539   return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 &&  $self->storno;
540   return 'invoice';
541 }
542
543 sub displayable_state {
544   my $self = shift;
545
546   return $self->closed ? $::locale->text('closed') : $::locale->text('open');
547 }
548
549 sub displayable_type {
550   my ($self) = @_;
551
552   return t8('AR Transaction')                         if $self->invoice_type eq 'ar_transaction';
553   return t8('Credit Note')                            if $self->invoice_type eq 'credit_note';
554   return t8('Invoice') . "(" . t8('Storno') . ")"     if $self->invoice_type eq 'invoice_storno';
555   return t8('Credit Note') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'credit_note_storno';
556   return t8('Invoice');
557 }
558
559 sub displayable_name {
560   join ' ', grep $_, map $_[0]->$_, qw(displayable_type record_number);
561 };
562
563 sub abbreviation {
564   my ($self) = @_;
565
566   return t8('AR Transaction (abbreviation)')         if $self->invoice_type eq 'ar_transaction';
567   return t8('Credit note (one letter abbreviation)') if $self->invoice_type eq 'credit_note';
568   return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'invoice_storno';
569   return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")"  if $self->invoice_type eq 'credit_note_storno';
570   return t8('Invoice (one letter abbreviation)');
571 }
572
573 sub oneline_summary {
574   my $self = shift;
575
576   return sprintf("%s: %s %s %s (%s)", $self->abbreviation, $self->invnumber, $self->customer->name,
577                                       $::form->format_amount(\%::myconfig, $self->amount,2), $self->transdate->to_kivitendo);
578 }
579
580 sub date {
581   goto &transdate;
582 }
583
584 sub reqdate {
585   goto &duedate;
586 }
587
588 sub customervendor {
589   goto &customer;
590 }
591
592 sub link {
593   my ($self) = @_;
594
595   my $html;
596   $html   = $self->presenter->sales_invoice(display => 'inline') if $self->invoice;
597   $html   = $self->presenter->ar_transaction(display => 'inline') if !$self->invoice;
598
599   return $html;
600 }
601
602 sub mark_as_paid {
603   my ($self) = @_;
604
605   $self->update_attributes(paid => $self->amount);
606 }
607
608 1;
609
610 __END__
611
612 =pod
613
614 =encoding UTF-8
615
616 =head1 NAME
617
618 SL::DB::Invoice: Rose model for invoices (table "ar")
619
620 =head1 FUNCTIONS
621
622 =over 4
623
624 =item C<new_from $source, %params>
625
626 Creates a new C<SL::DB::Invoice> instance and copies as much
627 information from C<$source> as possible. At the moment only sales
628 orders and sales quotations are supported as sources.
629
630 The conversion copies order items into invoice items. Dates are copied
631 as appropriate, e.g. the C<transdate> field from an order will be
632 copied into the invoice's C<orddate> field.
633
634 C<%params> can include the following options:
635
636 =over 2
637
638 =item C<items>
639
640 An optional array reference of RDBO instances for the items to use. If
641 missing then the method C<items_sorted> will be called on
642 C<$source>. This option can be used to override the sorting, to
643 exclude certain positions or to add additional ones.
644
645 =item C<skip_items_negative_qty>
646
647 If trueish then items with a negative quantity are skipped. Items with
648 a quantity of 0 are not affected by this option.
649
650 =item C<skip_items_zero_qty>
651
652 If trueish then items with a quantity of 0 are skipped.
653
654 =item C<item_filter>
655
656 An optional code reference that is called for each item with the item
657 as its sole parameter. Items for which the code reference returns a
658 falsish value will be skipped.
659
660 =item C<attributes>
661
662 An optional hash reference. If it exists then it is passed to C<new>
663 allowing the caller to set certain attributes for the new invoice.
664 For example to set a different transdate (default is the current date),
665 call the method like this:
666
667    my %params;
668    $params{attributes}{transdate} = '28.08.2015';
669    $invoice = SL::DB::Invoice->new_from($self, %params)->post || die;
670
671 =back
672
673 Amounts, prices and taxes are not
674 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
675 can be used for this.
676
677 The object returned is not saved.
678
679 =item C<post %params>
680
681 Posts the invoice. Required parameters are:
682
683 =over 2
684
685 =item * C<ar_id>
686
687 The ID of the accounts receivable chart the invoice's amounts are
688 posted to. If it is not set then the first chart configured for
689 accounts receivables is used.
690
691 =back
692
693 This function implements several steps:
694
695 =over 2
696
697 =item 1. It calculates all prices, amounts and taxes by calling
698 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
699
700 =item 2. A new and unique invoice number is created.
701
702 =item 3. All amounts for costs of goods sold are recorded in
703 C<acc_trans>.
704
705 =item 4. All amounts for parts, services and assemblies are recorded
706 in C<acc_trans> with their respective charts. This is determined by
707 the part's buchungsgruppen.
708
709 =item 5. The total amount is posted to the accounts receivable chart
710 and recorded in C<acc_trans>.
711
712 =item 6. Items in C<invoice> are updated according to their allocation
713 status (regarding costs of goods sold). Will only be done if
714 kivitendo is not configured to use Einnahmenüberschussrechnungen.
715
716 =item 7. The invoice and its items are saved.
717
718 =back
719
720 Returns C<$self> on success and C<undef> on failure. The whole process
721 is run inside a transaction. If it fails then nothing is saved to or
722 changed in the database. A new transaction is only started if none are
723 active.
724
725 =item C<basic_info $field>
726
727 See L<SL::DB::Object::basic_info>.
728
729 =item C<closed>
730
731 Returns 1 or 0, depending on whether the invoice is closed or not. Currently
732 invoices that are overpaid also count as closed and credit notes in general.
733
734 =item C<recalculate_amounts %params>
735
736 Calculate and set amount and netamount from acc_trans objects by summing up the
737 values of acc_trans objects with AR_amount and AR_tax link charts.
738 amount and netamount are set to the calculated values.
739
740 =item C<validate_acc_trans>
741
742 Checks if the sum of all associated acc_trans objects is 0 and checks whether
743 the amount of the AR acc_transaction matches the AR amount. Only the first AR
744 line is checked, because the sum of all AR lines is 0 for paid invoices.
745
746 Returns 0 or 1.
747
748 Can be called with a debug parameter which writes debug info to STDOUT, which is
749 useful in console mode or while writing tests.
750
751  my $ar = SL::DB::Manager::Invoice->get_first();
752  $ar->validate_acc_trans(debug => 1);
753
754 =item C<create_ar_row %params>
755
756 Creates a new acc_trans entry for the receivable (AR) entry of an existing AR
757 invoice object, which already has some income and tax acc_trans entries.
758
759 The acc_trans entry is also returned inside an array ref.
760
761 Mandatory params are
762
763 =over 2
764
765 =item * chart as an RDBO object, e.g. for bank. Must be a 'paid' chart.
766
767 =back
768
769 Currently the amount of the invoice object is used for the acc_trans amount.
770 Use C<recalculate_amounts> before calling this method if amount isn't known
771 yet or you didn't set it manually.
772
773 =item C<add_ar_amount_row %params>
774
775 Add a new entry for an existing AR invoice object. Creates an acc_trans entry,
776 and also adds an acc_trans tax entry, if the tax has an associated tax chart.
777 Also all acc_trans entries that were created are returned inside an array ref.
778
779 Mandatory params are
780
781 =over 2
782
783 =item * chart as an RDBO object, should be an income chart (link = AR_amount)
784
785 =item * tax_id
786
787 =item * amount
788
789 =back
790
791 =item C<mark_as_paid>
792
793 Marks the invoice as paid by setting its C<paid> member to the value of C<amount>.
794
795 =back
796
797 =head1 TODO
798
799  As explained in the new_from example, it is possible to set transdate to a new value.
800  From a user / programm point of view transdate is more than holy and there should be
801  some validity checker available for controller code. At least the same logic like in
802  Form.pm from ar.pl should be available:
803   # see old stuff ar.pl post
804   #$form->error($locale->text('Cannot post transaction above the maximum future booking date!'))
805   #  if ($form->date_max_future($transdate, \%myconfig));
806   #$form->error($locale->text('Cannot post transaction for a closed period!')) if ($form->date_closed($form->{"transdate"}, \%myconfig));
807
808 =head1 AUTHOR
809
810 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
811
812 =cut