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