FiBu Schellsuche in Headerzeile
[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 abbreviation {
322   my $self = shift;
323
324   return t8('AR Transaction (abbreviation)') if !$self->invoice;
325   return t8('Credit note (one letter abbreviation)') if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
326   return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->type ne 'credit_note' && $self->amount < 0 &&  $self->storno;
327   return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->type eq 'credit_note' && $self->amount > 0 &&  $self->storno;
328   return t8('Invoice (one letter abbreviation)');
329
330 }
331
332 sub date {
333   goto &transdate;
334 }
335
336 sub transactions {
337   my ($self) = @_;
338
339   return unless $self->id;
340
341   require SL::DB::AccTransaction;
342   SL::DB::Manager::AccTransaction->get_all(query => [ trans_id => $self->id ]);
343 }
344
345 1;
346
347 __END__
348
349 =pod
350
351 =head1 NAME
352
353 SL::DB::Invoice: Rose model for invoices (table "ar")
354
355 =head1 FUNCTIONS
356
357 =over 4
358
359 =item C<new_from $source, %params>
360
361 Creates a new C<SL::DB::Invoice> instance and copies as much
362 information from C<$source> as possible. At the moment only sales
363 orders and sales quotations are supported as sources.
364
365 The conversion copies order items into invoice items. Dates are copied
366 as appropriate, e.g. the C<transdate> field from an order will be
367 copied into the invoice's C<orddate> field.
368
369 C<%params> can include the following options:
370
371 =over 2
372
373 =item C<items>
374
375 An optional array reference of RDBO instances for the items to use. If
376 missing then the method C<items_sorted> will be called on
377 C<$source>. This option can be used to override the sorting, to
378 exclude certain positions or to add additional ones.
379
380 =item C<skip_items_zero_qty>
381
382 If trueish then items with a quantity of 0 are skipped.
383
384 =item C<attributes>
385
386 An optional hash reference. If it exists then it is passed to C<new>
387 allowing the caller to set certain attributes for the new delivery
388 order.
389
390 =back
391
392 Amounts, prices and taxes are not
393 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
394 can be used for this.
395
396 The object returned is not saved.
397
398 =item C<post %params>
399
400 Posts the invoice. Required parameters are:
401
402 =over 2
403
404 =item * C<ar_id>
405
406 The ID of the accounds receivable chart the invoices amounts are
407 posted to. If it is not set then the first chart configured for
408 accounts receivables is used.
409
410 =back
411
412 This function implements several steps:
413
414 =over 2
415
416 =item 1. It calculates all prices, amounts and taxes by calling
417 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
418
419 =item 2. A new and unique invoice number is created.
420
421 =item 3. All amounts for costs of goods sold are recorded in
422 C<acc_trans>.
423
424 =item 4. All amounts for parts, services and assemblies are recorded
425 in C<acc_trans> with their respective charts. This is determined by
426 the part's buchungsgruppen.
427
428 =item 5. The total amount is posted to the accounts receivable chart
429 and recorded in C<acc_trans>.
430
431 =item 6. Items in C<invoice> are updated according to their allocation
432 status (regarding for costs of goold sold). Will only be done if
433 kivitendo is not configured to use Einnahmenüberschussrechnungen.
434
435 =item 7. The invoice and its items are saved.
436
437 =back
438
439 Returns C<$self> on success and C<undef> on failure. The whole process
440 is run inside a transaction. If it fails then nothing is saved to or
441 changed in the database. A new transaction is only started if none is
442 active.
443
444 =item C<basic_info $field>
445
446 See L<SL::DB::Object::basic_info>.
447
448 =back
449
450 =head1 AUTHOR
451
452 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
453
454 =cut