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