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