SL::DB::{Invoice,DeliveryOrder}->new_from: manuelles Filtern von Items via coderef
[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 { $params{item_filter}->($_) } @items if $params{item_filter};
207   @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
208   @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
209
210   $invoice->invoiceitems(\@items);
211
212   return $invoice;
213 }
214
215 sub post {
216   my ($self, %params) = @_;
217
218   require SL::DB::Chart;
219   if (!$params{ar_id}) {
220     my $chart = SL::DB::Manager::Chart->get_all(query   => [ SL::DB::Manager::Chart->link_filter('AR') ],
221                                                 sort_by => 'id ASC',
222                                                 limit   => 1)->[0];
223     croak("No AR chart found and no parameter `ar_id' given") unless $chart;
224     $params{ar_id} = $chart->id;
225   }
226
227   my $worker = sub {
228     my %data = $self->calculate_prices_and_taxes;
229
230     $self->_post_create_assemblyitem_entries($data{assembly_items});
231     $self->save;
232
233     $self->_post_add_acctrans($data{amounts_cogs});
234     $self->_post_add_acctrans($data{amounts});
235     $self->_post_add_acctrans($data{taxes});
236
237     $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
238
239     $self->_post_update_allocated($data{allocated});
240   };
241
242   if ($self->db->in_transaction) {
243     $worker->();
244   } elsif (!$self->db->do_transaction($worker)) {
245     $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
246     return undef;
247   }
248
249   return $self;
250 }
251
252 sub _post_add_acctrans {
253   my ($self, $entries) = @_;
254
255   my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
256   my $chart_link;
257
258   require SL::DB::AccTransaction;
259   require SL::DB::Chart;
260   while (my ($chart_id, $spec) = each %{ $entries }) {
261     $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
262     $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
263     $chart_link ||= '';
264
265     SL::DB::AccTransaction->new(trans_id   => $self->id,
266                                 chart_id   => $chart_id,
267                                 amount     => $spec->{amount},
268                                 tax_id     => $spec->{tax_id},
269                                 taxkey     => $spec->{taxkey},
270                                 project_id => $self->globalproject_id,
271                                 transdate  => $self->transdate,
272                                 chart_link => $chart_link)->save;
273   }
274 }
275
276 sub _post_create_assemblyitem_entries {
277   my ($self, $assembly_entries) = @_;
278
279   my $items = $self->invoiceitems;
280   my @new_items;
281
282   my $item_idx = 0;
283   foreach my $item (@{ $items }) {
284     next if $item->assemblyitem;
285
286     push @new_items, $item;
287     $item_idx++;
288
289     foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
290       push @new_items, SL::DB::InvoiceItem->new(parts_id     => $assembly_item->{part},
291                                                 description  => $assembly_item->{part}->description,
292                                                 unit         => $assembly_item->{part}->unit,
293                                                 qty          => $assembly_item->{qty},
294                                                 allocated    => $assembly_item->{allocated},
295                                                 sellprice    => 0,
296                                                 fxsellprice  => 0,
297                                                 assemblyitem => 't');
298     }
299   }
300
301   $self->invoiceitems(\@new_items);
302 }
303
304 sub _post_update_allocated {
305   my ($self, $allocated) = @_;
306
307   while (my ($invoice_id, $diff) = each %{ $allocated }) {
308     SL::DB::Manager::InvoiceItem->update_all(set   => { allocated => { sql => "allocated + $diff" } },
309                                              where => [ id        => $invoice_id ]);
310   }
311 }
312
313 sub invoice_type {
314   my ($self) = @_;
315
316   return 'ar_transaction'     if !$self->invoice;
317   return 'credit_note'        if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
318   return 'invoice_storno'     if $self->type ne 'credit_note' && $self->amount < 0 &&  $self->storno;
319   return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 &&  $self->storno;
320   return 'invoice';
321 }
322
323 sub displayable_state {
324   my $self = shift;
325
326   return $self->closed ? $::locale->text('closed') : $::locale->text('open');
327 }
328
329 sub displayable_type {
330   my ($self) = @_;
331
332   return t8('AR Transaction')                         if $self->invoice_type eq 'ar_transaction';
333   return t8('Credit Note')                            if $self->invoice_type eq 'credit_note';
334   return t8('Invoice') . "(" . t8('Storno') . ")"     if $self->invoice_type eq 'invoice_storno';
335   return t8('Credit Note') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'credit_note_storno';
336   return t8('Invoice');
337 }
338
339 sub abbreviation {
340   my ($self) = @_;
341
342   return t8('AR Transaction (abbreviation)')         if $self->invoice_type eq 'ar_transaction';
343   return t8('Credit note (one letter abbreviation)') if $self->invoice_type eq 'credit_note';
344   return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'invoice_storno';
345   return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")"  if $self->invoice_type eq 'credit_note_storno';
346   return t8('Invoice (one letter abbreviation)');
347 }
348
349 sub date {
350   goto &transdate;
351 }
352
353 sub reqdate {
354   goto &duedate;
355 }
356
357 sub customervendor {
358   goto &customer;
359 }
360
361 sub link {
362   my ($self) = @_;
363
364   my $html;
365   $html   = SL::Presenter->get->sales_invoice($self, display => 'inline') if $self->invoice;
366   $html   = SL::Presenter->get->ar_transaction($self, display => 'inline') if !$self->invoice;
367
368   return $html;
369 }
370
371 1;
372
373 __END__
374
375 =pod
376
377 =head1 NAME
378
379 SL::DB::Invoice: Rose model for invoices (table "ar")
380
381 =head1 FUNCTIONS
382
383 =over 4
384
385 =item C<new_from $source, %params>
386
387 Creates a new C<SL::DB::Invoice> instance and copies as much
388 information from C<$source> as possible. At the moment only sales
389 orders and sales quotations are supported as sources.
390
391 The conversion copies order items into invoice items. Dates are copied
392 as appropriate, e.g. the C<transdate> field from an order will be
393 copied into the invoice's C<orddate> field.
394
395 C<%params> can include the following options:
396
397 =over 2
398
399 =item C<items>
400
401 An optional array reference of RDBO instances for the items to use. If
402 missing then the method C<items_sorted> will be called on
403 C<$source>. This option can be used to override the sorting, to
404 exclude certain positions or to add additional ones.
405
406 =item C<skip_items_negative_qty>
407
408 If trueish then items with a negative quantity are skipped. Items with
409 a quantity of 0 are not affected by this option.
410
411 =item C<skip_items_zero_qty>
412
413 If trueish then items with a quantity of 0 are skipped.
414
415 =item C<item_filter>
416
417 An optional code reference that is called for each item with the item
418 as its sole parameter. Items for which the code reference returns a
419 falsish value will be skipped.
420
421 =item C<attributes>
422
423 An optional hash reference. If it exists then it is passed to C<new>
424 allowing the caller to set certain attributes for the new delivery
425 order.
426
427 =back
428
429 Amounts, prices and taxes are not
430 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
431 can be used for this.
432
433 The object returned is not saved.
434
435 =item C<post %params>
436
437 Posts the invoice. Required parameters are:
438
439 =over 2
440
441 =item * C<ar_id>
442
443 The ID of the accounds receivable chart the invoices amounts are
444 posted to. If it is not set then the first chart configured for
445 accounts receivables is used.
446
447 =back
448
449 This function implements several steps:
450
451 =over 2
452
453 =item 1. It calculates all prices, amounts and taxes by calling
454 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
455
456 =item 2. A new and unique invoice number is created.
457
458 =item 3. All amounts for costs of goods sold are recorded in
459 C<acc_trans>.
460
461 =item 4. All amounts for parts, services and assemblies are recorded
462 in C<acc_trans> with their respective charts. This is determined by
463 the part's buchungsgruppen.
464
465 =item 5. The total amount is posted to the accounts receivable chart
466 and recorded in C<acc_trans>.
467
468 =item 6. Items in C<invoice> are updated according to their allocation
469 status (regarding for costs of goold sold). Will only be done if
470 kivitendo is not configured to use Einnahmenüberschussrechnungen.
471
472 =item 7. The invoice and its items are saved.
473
474 =back
475
476 Returns C<$self> on success and C<undef> on failure. The whole process
477 is run inside a transaction. If it fails then nothing is saved to or
478 changed in the database. A new transaction is only started if none is
479 active.
480
481 =item C<basic_info $field>
482
483 See L<SL::DB::Object::basic_info>.
484
485 =back
486
487 =head1 AUTHOR
488
489 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
490
491 =cut