0e9616651530f3243daae5e105f3294aef0a0bb4
[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
21 __PACKAGE__->meta->add_relationship(
22   invoiceitems => {
23     type         => 'one to many',
24     class        => 'SL::DB::InvoiceItem',
25     column_map   => { id => 'trans_id' },
26     manager_args => {
27       with_objects => [ 'part' ]
28     }
29   },
30   storno_invoices => {
31     type          => 'one to many',
32     class         => 'SL::DB::Invoice',
33     column_map    => { id => 'storno_id' },
34   },
35   sepa_export_items => {
36     type            => 'one to many',
37     class           => 'SL::DB::SepaExportItem',
38     column_map      => { id => 'ar_id' },
39     manager_args    => { with_objects => [ 'sepa_export' ] }
40   },
41   custom_shipto     => {
42     type            => 'one to one',
43     class           => 'SL::DB::Shipto',
44     column_map      => { id => 'trans_id' },
45     query_args      => [ module => 'AR' ],
46   },
47 );
48
49 __PACKAGE__->meta->initialize;
50
51 __PACKAGE__->before_save('_before_save_set_invnumber');
52
53 # hooks
54
55 sub _before_save_set_invnumber {
56   my ($self) = @_;
57
58   $self->create_trans_number if !$self->invnumber;
59
60   return 1;
61 }
62
63 # methods
64
65 sub items { goto &invoiceitems; }
66
67 sub items_sorted {
68   my ($self) = @_;
69
70   return [ sort {$a->id <=> $b->id } @{ $self->items } ];
71 }
72
73 sub is_sales {
74   # For compatibility with Order, DeliveryOrder
75   croak 'not an accessor' if @_ > 1;
76   return 1;
77 }
78
79 # it is assumed, that ordnumbers are unique here.
80 sub first_order_by_ordnumber {
81   my $self = shift;
82
83   my $orders = SL::DB::Manager::Order->get_all(
84     query => [
85       ordnumber => $self->ordnumber,
86
87     ],
88   );
89
90   return first { $_->is_type('sales_order') } @{ $orders };
91 }
92
93 sub abschlag_percentage {
94   my $self         = shift;
95   my $order        = $self->first_order_by_ordnumber or return;
96   my $order_amount = $order->netamount               or return;
97   return $self->abschlag
98     ? $self->netamount / $order_amount
99     : undef;
100 }
101
102 sub taxamount {
103   my $self = shift;
104   die 'not a setter method' if @_;
105
106   return ($self->amount || 0) - ($self->netamount || 0);
107 }
108
109 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
110
111 sub closed {
112   my ($self) = @_;
113   return $self->paid >= $self->amount;
114 }
115
116 sub _clone_orderitem_delivery_order_item_cvar {
117   my ($cvar) = @_;
118
119   my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
120   $cloned->sub_module('invoice');
121
122   return $cloned;
123 }
124
125 sub new_from {
126   my ($class, $source, %params) = @_;
127
128   croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
129   croak("Cannot create invoices for purchase records")           unless $source->customer_id;
130
131   require SL::DB::Employee;
132
133   my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms
134             : $source->customer_id                              ? $source ->customer->payment_terms
135             :                                                     undef;
136
137   my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
138
139   if (ref($source) eq 'SL::DB::Order') {
140     @columns      = qw(quonumber payment_id delivery_customer_id delivery_vendor_id);
141     @item_columns = qw(subtotal);
142
143     $item_parent_id_column = 'trans_id';
144     $item_parent_column    = 'order';
145
146   } else {
147     @columns      = qw(donumber);
148
149     $item_parent_id_column = 'delivery_order_id';
150     $item_parent_column    = 'delivery_order';
151   }
152
153   my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
154                                                 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
155                transdate   => DateTime->today_local,
156                gldate      => DateTime->today_local,
157                duedate     => DateTime->today_local->add(days => ($terms ? $terms->terms_netto * 1 : 1)),
158                payment_id  => $terms ? $terms->id : undef,
159                invoice     => 1,
160                type        => 'invoice',
161                storno      => 0,
162                paid        => 0,
163                employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
164             );
165
166   if ($source->type =~ /_order$/) {
167     $args{deliverydate} = $source->reqdate;
168     $args{orddate}      = $source->transdate;
169   } else {
170     $args{quodate}      = $source->transdate;
171   }
172
173   my $invoice = $class->new(%args);
174   $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
175   my $items   = delete($params{items}) || $source->items_sorted;
176   my %item_parents;
177
178   my @items = map {
179     my $source_item      = $_;
180     my $source_item_id   = $_->$item_parent_id_column;
181     my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
182
183     $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
184     my $item_parent                  = $item_parents{$source_item_id};
185
186     SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
187                                  qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
188                                     base_qty longdescription lastcost price_factor_id), @item_columns),
189                              deliverydate     => $source_item->reqdate,
190                              fxsellprice      => $source_item->sellprice,
191                              custom_variables => \@custom_variables,
192                              ordnumber        => ref($item_parent) eq 'SL::DB::Order'         ? $item_parent->ordnumber : $source_item->ordnumber,
193                              donumber         => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber  : $source_item->can('donumber') ? $source_item->donumber : '',
194                            );
195
196   } @{ $items };
197
198   @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
199
200   $invoice->invoiceitems(\@items);
201
202   return $invoice;
203 }
204
205 sub post {
206   my ($self, %params) = @_;
207
208   require SL::DB::Chart;
209   if (!$params{ar_id}) {
210     my $chart = SL::DB::Manager::Chart->get_all(query   => [ SL::DB::Manager::Chart->link_filter('AR') ],
211                                                 sort_by => 'id ASC',
212                                                 limit   => 1)->[0];
213     croak("No AR chart found and no parameter `ar_id' given") unless $chart;
214     $params{ar_id} = $chart->id;
215   }
216
217   my $worker = sub {
218     my %data = $self->calculate_prices_and_taxes;
219
220     $self->_post_create_assemblyitem_entries($data{assembly_items});
221     $self->save;
222
223     $self->_post_add_acctrans($data{amounts_cogs});
224     $self->_post_add_acctrans($data{amounts});
225     $self->_post_add_acctrans($data{taxes});
226
227     $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
228
229     $self->_post_update_allocated($data{allocated});
230   };
231
232   if ($self->db->in_transaction) {
233     $worker->();
234   } elsif (!$self->db->do_transaction($worker)) {
235     $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
236     return undef;
237   }
238
239   return $self;
240 }
241
242 sub _post_add_acctrans {
243   my ($self, $entries) = @_;
244
245   my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
246   my $chart_link;
247
248   require SL::DB::AccTransaction;
249   require SL::DB::Chart;
250   while (my ($chart_id, $spec) = each %{ $entries }) {
251     $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
252     $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
253     $chart_link ||= '';
254
255     SL::DB::AccTransaction->new(trans_id   => $self->id,
256                                 chart_id   => $chart_id,
257                                 amount     => $spec->{amount},
258                                 tax_id     => $spec->{tax_id},
259                                 taxkey     => $spec->{taxkey},
260                                 project_id => $self->globalproject_id,
261                                 transdate  => $self->transdate,
262                                 chart_link => $chart_link)->save;
263   }
264 }
265
266 sub _post_create_assemblyitem_entries {
267   my ($self, $assembly_entries) = @_;
268
269   my $items = $self->invoiceitems;
270   my @new_items;
271
272   my $item_idx = 0;
273   foreach my $item (@{ $items }) {
274     next if $item->assemblyitem;
275
276     push @new_items, $item;
277     $item_idx++;
278
279     foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
280       push @new_items, SL::DB::InvoiceItem->new(parts_id     => $assembly_item->{part},
281                                                 description  => $assembly_item->{part}->description,
282                                                 unit         => $assembly_item->{part}->unit,
283                                                 qty          => $assembly_item->{qty},
284                                                 allocated    => $assembly_item->{allocated},
285                                                 sellprice    => 0,
286                                                 fxsellprice  => 0,
287                                                 assemblyitem => 't');
288     }
289   }
290
291   $self->invoiceitems(\@new_items);
292 }
293
294 sub _post_update_allocated {
295   my ($self, $allocated) = @_;
296
297   while (my ($invoice_id, $diff) = each %{ $allocated }) {
298     SL::DB::Manager::InvoiceItem->update_all(set   => { allocated => { sql => "allocated + $diff" } },
299                                              where => [ id        => $invoice_id ]);
300   }
301 }
302
303 sub invoice_type {
304   my ($self) = @_;
305
306   return 'ar_transaction'     if !$self->invoice;
307   return 'credit_note'        if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
308   return 'invoice_storno'     if $self->type ne 'credit_note' && $self->amount < 0 &&  $self->storno;
309   return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 &&  $self->storno;
310   return 'invoice';
311 }
312
313 sub displayable_state {
314   my $self = shift;
315
316   return $self->closed ? $::locale->text('closed') : $::locale->text('open');
317 }
318
319 sub date {
320   goto &transdate;
321 }
322
323 1;
324
325 __END__
326
327 =pod
328
329 =head1 NAME
330
331 SL::DB::Invoice: Rose model for invoices (table "ar")
332
333 =head1 FUNCTIONS
334
335 =over 4
336
337 =item C<new_from $source, %params>
338
339 Creates a new C<SL::DB::Invoice> instance and copies as much
340 information from C<$source> as possible. At the moment only sales
341 orders and sales quotations are supported as sources.
342
343 The conversion copies order items into invoice items. Dates are copied
344 as appropriate, e.g. the C<transdate> field from an order will be
345 copied into the invoice's C<orddate> field.
346
347 C<%params> can include the following options:
348
349 =over 2
350
351 =item C<items>
352
353 An optional array reference of RDBO instances for the items to use. If
354 missing then the method C<items_sorted> will be called on
355 C<$source>. This option can be used to override the sorting, to
356 exclude certain positions or to add additional ones.
357
358 =item C<skip_items_zero_qty>
359
360 If trueish then items with a quantity of 0 are skipped.
361
362 =item C<attributes>
363
364 An optional hash reference. If it exists then it is passed to C<new>
365 allowing the caller to set certain attributes for the new delivery
366 order.
367
368 =back
369
370 Amounts, prices and taxes are not
371 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
372 can be used for this.
373
374 The object returned is not saved.
375
376 =item C<post %params>
377
378 Posts the invoice. Required parameters are:
379
380 =over 2
381
382 =item * C<ar_id>
383
384 The ID of the accounds receivable chart the invoices amounts are
385 posted to. If it is not set then the first chart configured for
386 accounts receivables is used.
387
388 =back
389
390 This function implements several steps:
391
392 =over 2
393
394 =item 1. It calculates all prices, amounts and taxes by calling
395 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
396
397 =item 2. A new and unique invoice number is created.
398
399 =item 3. All amounts for costs of goods sold are recorded in
400 C<acc_trans>.
401
402 =item 4. All amounts for parts, services and assemblies are recorded
403 in C<acc_trans> with their respective charts. This is determined by
404 the part's buchungsgruppen.
405
406 =item 5. The total amount is posted to the accounts receivable chart
407 and recorded in C<acc_trans>.
408
409 =item 6. Items in C<invoice> are updated according to their allocation
410 status (regarding for costs of goold sold). Will only be done if
411 kivitendo is not configured to use Einnahmenüberschussrechnungen.
412
413 =item 7. The invoice and its items are saved.
414
415 =back
416
417 Returns C<$self> on success and C<undef> on failure. The whole process
418 is run inside a transaction. If it fails then nothing is saved to or
419 changed in the database. A new transaction is only started if none is
420 active.
421
422 =item C<basic_info $field>
423
424 See L<SL::DB::Object::basic_info>.
425
426 =back
427
428 =head1 AUTHOR
429
430 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
431
432 =cut