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