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