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