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