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