3ecc9575e704500cadb5d2ab13b286331d6f8a37
[kivitendo-erp.git] / SL / DB / Invoice.pm
1 # This file has been auto-generated only because it didn't exist.
2 # Feel free to modify it at will; it will not be overwritten automatically.
3
4 package SL::DB::Invoice;
5
6 use strict;
7
8 use Carp;
9 use List::Util qw(first);
10
11 use Rose::DB::Object::Helpers ();
12
13 use SL::DB::MetaSetup::Invoice;
14 use SL::DB::Manager::Invoice;
15 use SL::DB::Helper::AttrHTML;
16 use SL::DB::Helper::FlattenToForm;
17 use SL::DB::Helper::LinkedRecords;
18 use SL::DB::Helper::PriceTaxCalculator;
19 use SL::DB::Helper::PriceUpdater;
20 use SL::DB::Helper::TransNumberGenerator;
21 use SL::Locale::String qw(t8);
22 use SL::DB::CustomVariable;
23 use SL::DB::AccTransaction;
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   custom_shipto     => {
46     type            => 'one to one',
47     class           => 'SL::DB::Shipto',
48     column_map      => { id => 'trans_id' },
49     query_args      => [ module => 'AR' ],
50   },
51   transactions   => {
52     type         => 'one to many',
53     class        => 'SL::DB::AccTransaction',
54     column_map   => { id => 'trans_id' },
55     manager_args => {
56       with_objects => [ 'chart' ],
57       sort_by      => 'acc_trans_id ASC',
58     },
59   },
60 );
61
62 __PACKAGE__->meta->initialize;
63
64 __PACKAGE__->attr_html('notes');
65
66 __PACKAGE__->before_save('_before_save_set_invnumber');
67
68 # hooks
69
70 sub _before_save_set_invnumber {
71   my ($self) = @_;
72
73   $self->create_trans_number if !$self->invnumber;
74
75   return 1;
76 }
77
78 # methods
79
80 sub items { goto &invoiceitems; }
81 sub add_items { goto &add_invoiceitems; }
82
83 sub items_sorted {
84   my ($self) = @_;
85
86   return [ sort {$a->position <=> $b->position } @{ $self->items } ];
87 }
88
89 sub is_sales {
90   # For compatibility with Order, DeliveryOrder
91   croak 'not an accessor' if @_ > 1;
92   return 1;
93 }
94
95 # it is assumed, that ordnumbers are unique here.
96 sub first_order_by_ordnumber {
97   my $self = shift;
98
99   my $orders = SL::DB::Manager::Order->get_all(
100     query => [
101       ordnumber => $self->ordnumber,
102
103     ],
104   );
105
106   return first { $_->is_type('sales_order') } @{ $orders };
107 }
108
109 sub abschlag_percentage {
110   my $self         = shift;
111   my $order        = $self->first_order_by_ordnumber or return;
112   my $order_amount = $order->netamount               or return;
113   return $self->abschlag
114     ? $self->netamount / $order_amount
115     : undef;
116 }
117
118 sub taxamount {
119   my $self = shift;
120   die 'not a setter method' if @_;
121
122   return ($self->amount || 0) - ($self->netamount || 0);
123 }
124
125 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
126
127 sub closed {
128   my ($self) = @_;
129   return $self->paid >= $self->amount;
130 }
131
132 sub _clone_orderitem_delivery_order_item_cvar {
133   my ($cvar) = @_;
134
135   my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
136   $cloned->sub_module('invoice');
137
138   return $cloned;
139 }
140
141 sub new_from {
142   my ($class, $source, %params) = @_;
143
144   croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
145   croak("Cannot create invoices for purchase records")           unless $source->customer_id;
146
147   require SL::DB::Employee;
148
149   my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms
150             : $source->customer_id                              ? $source ->customer->payment_terms
151             :                                                     undef;
152
153   my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
154
155   if (ref($source) eq 'SL::DB::Order') {
156     @columns      = qw(quonumber payment_id delivery_customer_id delivery_vendor_id);
157     @item_columns = qw(subtotal);
158
159     $item_parent_id_column = 'trans_id';
160     $item_parent_column    = 'order';
161
162   } else {
163     @columns      = qw(donumber);
164
165     $item_parent_id_column = 'delivery_order_id';
166     $item_parent_column    = 'delivery_order';
167   }
168
169   my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
170                                                 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
171                transdate   => DateTime->today_local,
172                gldate      => DateTime->today_local,
173                duedate     => DateTime->today_local->add(days => ($terms ? $terms->terms_netto * 1 : 1)),
174                payment_id  => $terms ? $terms->id : undef,
175                invoice     => 1,
176                type        => 'invoice',
177                storno      => 0,
178                paid        => 0,
179                employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
180             );
181
182   if ($source->type =~ /_order$/) {
183     $args{deliverydate} = $source->reqdate;
184     $args{orddate}      = $source->transdate;
185   } else {
186     $args{quodate}      = $source->transdate;
187   }
188
189   my $invoice = $class->new(%args);
190   $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
191   my $items   = delete($params{items}) || $source->items_sorted;
192   my %item_parents;
193
194   my @items = map {
195     my $source_item      = $_;
196     my $source_item_id   = $_->$item_parent_id_column;
197     my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
198
199     $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
200     my $item_parent                  = $item_parents{$source_item_id};
201     my $current_invoice_item =
202       SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
203                                    qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
204                                       base_qty longdescription lastcost price_factor_id active_discount_source active_price_source), @item_columns),
205                                deliverydate     => $source_item->reqdate,
206                                fxsellprice      => $source_item->sellprice,
207                                custom_variables => \@custom_variables,
208                                ordnumber        => ref($item_parent) eq 'SL::DB::Order'         ? $item_parent->ordnumber : $source_item->ordnumber,
209                                donumber         => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber  : $source_item->can('donumber') ? $source_item->donumber : '',
210                              );
211
212     $current_invoice_item->{"converted_from_orderitems_id"}           = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
213     $current_invoice_item->{"converted_from_delivery_order_items_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::DeliveryOrder';
214     $current_invoice_item;
215   } @{ $items };
216
217   @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
218   @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
219
220   $invoice->invoiceitems(\@items);
221
222   return $invoice;
223 }
224
225 sub post {
226   my ($self, %params) = @_;
227
228   require SL::DB::Chart;
229   if (!$params{ar_id}) {
230     my $chart = SL::DB::Manager::Chart->get_all(query   => [ SL::DB::Manager::Chart->link_filter('AR') ],
231                                                 sort_by => 'id ASC',
232                                                 limit   => 1)->[0];
233     croak("No AR chart found and no parameter `ar_id' given") unless $chart;
234     $params{ar_id} = $chart->id;
235   }
236
237   my $worker = sub {
238     my %data = $self->calculate_prices_and_taxes;
239
240     $self->_post_create_assemblyitem_entries($data{assembly_items});
241     $self->save;
242
243     $self->_post_add_acctrans($data{amounts_cogs});
244     $self->_post_add_acctrans($data{amounts});
245     $self->_post_add_acctrans($data{taxes});
246
247     $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
248
249     $self->_post_update_allocated($data{allocated});
250   };
251
252   if ($self->db->in_transaction) {
253     $worker->();
254   } elsif (!$self->db->do_transaction($worker)) {
255     $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
256     return undef;
257   }
258
259   return $self;
260 }
261
262 sub _post_add_acctrans {
263   my ($self, $entries) = @_;
264
265   my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
266   my $chart_link;
267
268   require SL::DB::AccTransaction;
269   require SL::DB::Chart;
270   while (my ($chart_id, $spec) = each %{ $entries }) {
271     $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
272     $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
273     $chart_link ||= '';
274
275     SL::DB::AccTransaction->new(trans_id   => $self->id,
276                                 chart_id   => $chart_id,
277                                 amount     => $spec->{amount},
278                                 tax_id     => $spec->{tax_id},
279                                 taxkey     => $spec->{taxkey},
280                                 project_id => $self->globalproject_id,
281                                 transdate  => $self->transdate,
282                                 chart_link => $chart_link)->save;
283   }
284 }
285
286 sub _post_create_assemblyitem_entries {
287   my ($self, $assembly_entries) = @_;
288
289   my $items = $self->invoiceitems;
290   my @new_items;
291
292   my $item_idx = 0;
293   foreach my $item (@{ $items }) {
294     next if $item->assemblyitem;
295
296     push @new_items, $item;
297     $item_idx++;
298
299     foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
300       push @new_items, SL::DB::InvoiceItem->new(parts_id     => $assembly_item->{part},
301                                                 description  => $assembly_item->{part}->description,
302                                                 unit         => $assembly_item->{part}->unit,
303                                                 qty          => $assembly_item->{qty},
304                                                 allocated    => $assembly_item->{allocated},
305                                                 sellprice    => 0,
306                                                 fxsellprice  => 0,
307                                                 assemblyitem => 't');
308     }
309   }
310
311   $self->invoiceitems(\@new_items);
312 }
313
314 sub _post_update_allocated {
315   my ($self, $allocated) = @_;
316
317   while (my ($invoice_id, $diff) = each %{ $allocated }) {
318     SL::DB::Manager::InvoiceItem->update_all(set   => { allocated => { sql => "allocated + $diff" } },
319                                              where => [ id        => $invoice_id ]);
320   }
321 }
322
323 sub invoice_type {
324   my ($self) = @_;
325
326   return 'ar_transaction'     if !$self->invoice;
327   return 'credit_note'        if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
328   return 'invoice_storno'     if $self->type ne 'credit_note' && $self->amount < 0 &&  $self->storno;
329   return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 &&  $self->storno;
330   return 'invoice';
331 }
332
333 sub displayable_state {
334   my $self = shift;
335
336   return $self->closed ? $::locale->text('closed') : $::locale->text('open');
337 }
338
339 sub displayable_type {
340   my ($self) = @_;
341
342   return t8('AR Transaction')                         if $self->invoice_type eq 'ar_transaction';
343   return t8('Credit Note')                            if $self->invoice_type eq 'credit_note';
344   return t8('Invoice') . "(" . t8('Storno') . ")"     if $self->invoice_type eq 'invoice_storno';
345   return t8('Credit Note') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'credit_note_storno';
346   return t8('Invoice');
347 }
348
349 sub abbreviation {
350   my ($self) = @_;
351
352   return t8('AR Transaction (abbreviation)')         if $self->invoice_type eq 'ar_transaction';
353   return t8('Credit note (one letter abbreviation)') if $self->invoice_type eq 'credit_note';
354   return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'invoice_storno';
355   return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")"  if $self->invoice_type eq 'credit_note_storno';
356   return t8('Invoice (one letter abbreviation)');
357 }
358
359 sub date {
360   goto &transdate;
361 }
362
363 sub reqdate {
364   goto &duedate;
365 }
366
367 sub customervendor {
368   goto &customer;
369 }
370
371 sub pay_invoice {
372   my ($self, %params) = @_;
373
374   #Mark invoice as paid
375   $self->paid($self->paid+$params{amount});
376   $self->save;
377
378   Common::check_params(\%params, qw(chart_id trans_id amount transdate));
379
380   #account of bank account or cash
381   my $account_bank = SL::DB::Manager::Chart->find_by(id => $params{chart_id});
382
383   #Search the contra account
384   my $acc_trans = SL::DB::Manager::AccTransaction->find_by(trans_id   => $params{trans_id},
385                                                            or => [ chart_link => { like => "%:AR" },
386                                                                    chart_link => { like => "AR:%" },
387                                                                    chart_link => "AR" ]);
388   my $contra_account = SL::DB::Manager::Chart->find_by(id => $acc_trans->chart_id);
389
390   #Two new transfers in acc_trans (for bank account and for contra account)
391   my $new_acc_trans = SL::DB::AccTransaction->new(trans_id   => $params{trans_id},
392                                                   chart_id   => $account_bank->id,
393                                                   chart_link => $account_bank->link,
394                                                   amount     => (-1 * $params{amount}),
395                                                   transdate  => $params{transdate},
396                                                   source     => $params{source},
397                                                   memo       => '',
398                                                   taxkey     => 0,
399                                                   tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
400   $new_acc_trans->save;
401   $new_acc_trans = SL::DB::AccTransaction->new(trans_id   => $params{trans_id},
402                                                chart_id   => $contra_account->id,
403                                                chart_link => $contra_account->link,
404                                                amount     => $params{amount},
405                                                transdate  => $params{transdate},
406                                                source     => $params{source},
407                                                memo       => '',
408                                                taxkey     => 0,
409                                                tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
410   $new_acc_trans->save;
411 }
412
413 sub link {
414   my ($self) = @_;
415
416   my $html;
417   $html   = SL::Presenter->get->sales_invoice($self, display => 'inline') if $self->invoice;
418   $html   = SL::Presenter->get->ar_transaction($self, display => 'inline') if !$self->invoice;
419
420   return $html;
421 }
422
423 >>>>>>> Test: Bank-Commit zusammengefasst
424 1;
425
426 __END__
427
428 =pod
429
430 =head1 NAME
431
432 SL::DB::Invoice: Rose model for invoices (table "ar")
433
434 =head1 FUNCTIONS
435
436 =over 4
437
438 =item C<new_from $source, %params>
439
440 Creates a new C<SL::DB::Invoice> instance and copies as much
441 information from C<$source> as possible. At the moment only sales
442 orders and sales quotations are supported as sources.
443
444 The conversion copies order items into invoice items. Dates are copied
445 as appropriate, e.g. the C<transdate> field from an order will be
446 copied into the invoice's C<orddate> field.
447
448 C<%params> can include the following options:
449
450 =over 2
451
452 =item C<items>
453
454 An optional array reference of RDBO instances for the items to use. If
455 missing then the method C<items_sorted> will be called on
456 C<$source>. This option can be used to override the sorting, to
457 exclude certain positions or to add additional ones.
458
459 =item C<skip_items_negative_qty>
460
461 If trueish then items with a negative quantity are skipped. Items with
462 a quantity of 0 are not affected by this option.
463
464 =item C<skip_items_zero_qty>
465
466 If trueish then items with a quantity of 0 are skipped.
467
468 =item C<attributes>
469
470 An optional hash reference. If it exists then it is passed to C<new>
471 allowing the caller to set certain attributes for the new delivery
472 order.
473
474 =back
475
476 Amounts, prices and taxes are not
477 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
478 can be used for this.
479
480 The object returned is not saved.
481
482 =item C<post %params>
483
484 Posts the invoice. Required parameters are:
485
486 =over 2
487
488 =item * C<ar_id>
489
490 The ID of the accounds receivable chart the invoices amounts are
491 posted to. If it is not set then the first chart configured for
492 accounts receivables is used.
493
494 =back
495
496 This function implements several steps:
497
498 =over 2
499
500 =item 1. It calculates all prices, amounts and taxes by calling
501 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
502
503 =item 2. A new and unique invoice number is created.
504
505 =item 3. All amounts for costs of goods sold are recorded in
506 C<acc_trans>.
507
508 =item 4. All amounts for parts, services and assemblies are recorded
509 in C<acc_trans> with their respective charts. This is determined by
510 the part's buchungsgruppen.
511
512 =item 5. The total amount is posted to the accounts receivable chart
513 and recorded in C<acc_trans>.
514
515 =item 6. Items in C<invoice> are updated according to their allocation
516 status (regarding for costs of goold sold). Will only be done if
517 kivitendo is not configured to use Einnahmenüberschussrechnungen.
518
519 =item 7. The invoice and its items are saved.
520
521 =back
522
523 Returns C<$self> on success and C<undef> on failure. The whole process
524 is run inside a transaction. If it fails then nothing is saved to or
525 changed in the database. A new transaction is only started if none is
526 active.
527
528 =item C<basic_info $field>
529
530 See L<SL::DB::Object::basic_info>.
531
532 =back
533
534 =head1 AUTHOR
535
536 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
537
538 =cut