Merge branch 'bankerweiterung_und_skonto'
[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 $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms
142             : $source->customer_id                              ? $source ->customer->payment_terms
143             :                                                     undef;
144
145   my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
146
147   if (ref($source) eq 'SL::DB::Order') {
148     @columns      = qw(quonumber payment_id delivery_customer_id delivery_vendor_id);
149     @item_columns = qw(subtotal);
150
151     $item_parent_id_column = 'trans_id';
152     $item_parent_column    = 'order';
153
154   } else {
155     @columns      = qw(donumber);
156
157     $item_parent_id_column = 'delivery_order_id';
158     $item_parent_column    = 'delivery_order';
159   }
160
161   my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
162                                                 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
163                transdate   => DateTime->today_local,
164                gldate      => DateTime->today_local,
165                duedate     => DateTime->today_local->add(days => ($terms ? $terms->terms_netto * 1 : 1)),
166                payment_id  => $terms ? $terms->id : undef,
167                invoice     => 1,
168                type        => 'invoice',
169                storno      => 0,
170                paid        => 0,
171                employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
172             );
173
174   if ($source->type =~ /_order$/) {
175     $args{deliverydate} = $source->reqdate;
176     $args{orddate}      = $source->transdate;
177   } else {
178     $args{quodate}      = $source->transdate;
179   }
180
181   my $invoice = $class->new(%args);
182   $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
183   my $items   = delete($params{items}) || $source->items_sorted;
184   my %item_parents;
185
186   my @items = map {
187     my $source_item      = $_;
188     my $source_item_id   = $_->$item_parent_id_column;
189     my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
190
191     $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
192     my $item_parent                  = $item_parents{$source_item_id};
193     my $current_invoice_item =
194       SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
195                                    qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
196                                       base_qty longdescription lastcost price_factor_id active_discount_source active_price_source), @item_columns),
197                                deliverydate     => $source_item->reqdate,
198                                fxsellprice      => $source_item->sellprice,
199                                custom_variables => \@custom_variables,
200                                ordnumber        => ref($item_parent) eq 'SL::DB::Order'         ? $item_parent->ordnumber : $source_item->ordnumber,
201                                donumber         => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber  : $source_item->can('donumber') ? $source_item->donumber : '',
202                              );
203
204     $current_invoice_item->{"converted_from_orderitems_id"}           = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
205     $current_invoice_item->{"converted_from_delivery_order_items_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::DeliveryOrder';
206     $current_invoice_item;
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 sub link {
364   my ($self) = @_;
365
366   my $html;
367   $html   = SL::Presenter->get->sales_invoice($self, display => 'inline') if $self->invoice;
368   $html   = SL::Presenter->get->ar_transaction($self, display => 'inline') if !$self->invoice;
369
370   return $html;
371 }
372
373 1;
374
375 __END__
376
377 =pod
378
379 =head1 NAME
380
381 SL::DB::Invoice: Rose model for invoices (table "ar")
382
383 =head1 FUNCTIONS
384
385 =over 4
386
387 =item C<new_from $source, %params>
388
389 Creates a new C<SL::DB::Invoice> instance and copies as much
390 information from C<$source> as possible. At the moment only sales
391 orders and sales quotations are supported as sources.
392
393 The conversion copies order items into invoice items. Dates are copied
394 as appropriate, e.g. the C<transdate> field from an order will be
395 copied into the invoice's C<orddate> field.
396
397 C<%params> can include the following options:
398
399 =over 2
400
401 =item C<items>
402
403 An optional array reference of RDBO instances for the items to use. If
404 missing then the method C<items_sorted> will be called on
405 C<$source>. This option can be used to override the sorting, to
406 exclude certain positions or to add additional ones.
407
408 =item C<skip_items_negative_qty>
409
410 If trueish then items with a negative quantity are skipped. Items with
411 a quantity of 0 are not affected by this option.
412
413 =item C<skip_items_zero_qty>
414
415 If trueish then items with a quantity of 0 are skipped.
416
417 =item C<attributes>
418
419 An optional hash reference. If it exists then it is passed to C<new>
420 allowing the caller to set certain attributes for the new delivery
421 order.
422
423 =back
424
425 Amounts, prices and taxes are not
426 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
427 can be used for this.
428
429 The object returned is not saved.
430
431 =item C<post %params>
432
433 Posts the invoice. Required parameters are:
434
435 =over 2
436
437 =item * C<ar_id>
438
439 The ID of the accounds receivable chart the invoices amounts are
440 posted to. If it is not set then the first chart configured for
441 accounts receivables is used.
442
443 =back
444
445 This function implements several steps:
446
447 =over 2
448
449 =item 1. It calculates all prices, amounts and taxes by calling
450 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
451
452 =item 2. A new and unique invoice number is created.
453
454 =item 3. All amounts for costs of goods sold are recorded in
455 C<acc_trans>.
456
457 =item 4. All amounts for parts, services and assemblies are recorded
458 in C<acc_trans> with their respective charts. This is determined by
459 the part's buchungsgruppen.
460
461 =item 5. The total amount is posted to the accounts receivable chart
462 and recorded in C<acc_trans>.
463
464 =item 6. Items in C<invoice> are updated according to their allocation
465 status (regarding for costs of goold sold). Will only be done if
466 kivitendo is not configured to use Einnahmenüberschussrechnungen.
467
468 =item 7. The invoice and its items are saved.
469
470 =back
471
472 Returns C<$self> on success and C<undef> on failure. The whole process
473 is run inside a transaction. If it fails then nothing is saved to or
474 changed in the database. A new transaction is only started if none is
475 active.
476
477 =item C<basic_info $field>
478
479 See L<SL::DB::Object::basic_info>.
480
481 =back
482
483 =head1 AUTHOR
484
485 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
486
487 =cut