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