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