Refactoring
[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->terms_netto : 0;
134   my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
135
136   if (ref($source) eq 'SL::DB::Order') {
137     @columns      = qw(quonumber payment_id delivery_customer_id delivery_vendor_id);
138     @item_columns = qw(subtotal);
139
140     $item_parent_id_column = 'trans_id';
141     $item_parent_column    = 'order';
142
143   } else {
144     @columns      = qw(donumber);
145
146     $item_parent_id_column = 'delivery_order_id';
147     $item_parent_column    = 'delivery_order';
148   }
149
150   my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
151                                                 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
152                transdate   => DateTime->today_local,
153                gldate      => DateTime->today_local,
154                duedate     => DateTime->today_local->add(days => $terms * 1),
155                invoice     => 1,
156                type        => 'invoice',
157                storno      => 0,
158                paid        => 0,
159                employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
160             );
161
162   if ($source->type =~ /_order$/) {
163     $args{deliverydate} = $source->reqdate;
164     $args{orddate}      = $source->transdate;
165   } else {
166     $args{quodate}      = $source->transdate;
167   }
168
169   my $invoice = $class->new(%args, %{ $params{attributes} || {} });
170   my $items   = delete($params{items}) || $source->items_sorted;
171   my %item_parents;
172
173   my @items = map {
174     my $source_item      = $_;
175     my $source_item_id   = $_->$item_parent_id_column;
176     my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
177
178     $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
179     my $item_parent                  = $item_parents{$source_item_id};
180
181     SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
182                                  qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
183                                     base_qty longdescription lastcost price_factor_id), @item_columns),
184                              deliverydate     => $source_item->reqdate,
185                              fxsellprice      => $source_item->sellprice,
186                              custom_variables => \@custom_variables,
187                              ordnumber        => ref($item_parent) eq 'SL::DB::Order'         ? $item_parent->ordnumber : $source_item->ordnumber,
188                              donumber         => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber  : $source_item->can('donumber') ? $source_item->donumber : '',
189                            );
190
191   } @{ $items };
192
193   @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
194
195   $invoice->invoiceitems(\@items);
196
197   return $invoice;
198 }
199
200 sub post {
201   my ($self, %params) = @_;
202
203   require SL::DB::Chart;
204   if (!$params{ar_id}) {
205     my $chart = SL::DB::Manager::Chart->get_all(query   => [ SL::DB::Manager::Chart->link_filter('AR') ],
206                                                 sort_by => 'id ASC',
207                                                 limit   => 1)->[0];
208     croak("No AR chart found and no parameter `ar_id' given") unless $chart;
209     $params{ar_id} = $chart->id;
210   }
211
212   my $worker = sub {
213     my %data = $self->calculate_prices_and_taxes;
214
215     $self->_post_create_assemblyitem_entries($data{assembly_items});
216     $self->save;
217
218     $self->_post_add_acctrans($data{amounts_cogs});
219     $self->_post_add_acctrans($data{amounts});
220     $self->_post_add_acctrans($data{taxes});
221
222     $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
223
224     $self->_post_update_allocated($data{allocated});
225   };
226
227   if ($self->db->in_transaction) {
228     $worker->();
229   } elsif (!$self->db->do_transaction($worker)) {
230     $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
231     return undef;
232   }
233
234   return $self;
235 }
236
237 sub _post_add_acctrans {
238   my ($self, $entries) = @_;
239
240   my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
241   my $chart_link;
242
243   require SL::DB::AccTransaction;
244   require SL::DB::Chart;
245   while (my ($chart_id, $spec) = each %{ $entries }) {
246     $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
247     $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
248     $chart_link ||= '';
249
250     SL::DB::AccTransaction->new(trans_id   => $self->id,
251                                 chart_id   => $chart_id,
252                                 amount     => $spec->{amount},
253                                 tax_id     => $spec->{tax_id},
254                                 taxkey     => $spec->{taxkey},
255                                 project_id => $self->globalproject_id,
256                                 transdate  => $self->transdate,
257                                 chart_link => $chart_link)->save;
258   }
259 }
260
261 sub _post_create_assemblyitem_entries {
262   my ($self, $assembly_entries) = @_;
263
264   my $items = $self->invoiceitems;
265   my @new_items;
266
267   my $item_idx = 0;
268   foreach my $item (@{ $items }) {
269     next if $item->assemblyitem;
270
271     push @new_items, $item;
272     $item_idx++;
273
274     foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
275       push @new_items, SL::DB::InvoiceItem->new(parts_id     => $assembly_item->{part},
276                                                 description  => $assembly_item->{part}->description,
277                                                 unit         => $assembly_item->{part}->unit,
278                                                 qty          => $assembly_item->{qty},
279                                                 allocated    => $assembly_item->{allocated},
280                                                 sellprice    => 0,
281                                                 fxsellprice  => 0,
282                                                 assemblyitem => 't');
283     }
284   }
285
286   $self->invoiceitems(\@new_items);
287 }
288
289 sub _post_update_allocated {
290   my ($self, $allocated) = @_;
291
292   while (my ($invoice_id, $diff) = each %{ $allocated }) {
293     SL::DB::Manager::InvoiceItem->update_all(set   => { allocated => { sql => "allocated + $diff" } },
294                                              where => [ id        => $invoice_id ]);
295   }
296 }
297
298 sub invoice_type {
299   my ($self) = @_;
300
301   return 'ar_transaction'     if !$self->invoice;
302   return 'credit_note'        if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
303   return 'invoice_storno'     if $self->type ne 'credit_note' && $self->amount < 0 &&  $self->storno;
304   return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 &&  $self->storno;
305   return 'invoice';
306 }
307
308 sub displayable_state {
309   my $self = shift;
310
311   return $self->closed ? $::locale->text('closed') : $::locale->text('open');
312 }
313
314 sub date {
315   goto &transdate;
316 }
317
318 1;
319
320 __END__
321
322 =pod
323
324 =head1 NAME
325
326 SL::DB::Invoice: Rose model for invoices (table "ar")
327
328 =head1 FUNCTIONS
329
330 =over 4
331
332 =item C<new_from $source, %params>
333
334 Creates a new C<SL::DB::Invoice> instance and copies as much
335 information from C<$source> as possible. At the moment only sales
336 orders and sales quotations are supported as sources.
337
338 The conversion copies order items into invoice items. Dates are copied
339 as appropriate, e.g. the C<transdate> field from an order will be
340 copied into the invoice's C<orddate> field.
341
342 C<%params> can include the following options:
343
344 =over 2
345
346 =item C<items>
347
348 An optional array reference of RDBO instances for the items to use. If
349 missing then the method C<items_sorted> will be called on
350 C<$source>. This option can be used to override the sorting, to
351 exclude certain positions or to add additional ones.
352
353 =item C<skip_items_zero_qty>
354
355 If trueish then items with a quantity of 0 are skipped.
356
357 =item C<attributes>
358
359 An optional hash reference. If it exists then it is passed to C<new>
360 allowing the caller to set certain attributes for the new delivery
361 order.
362
363 =back
364
365 Amounts, prices and taxes are not
366 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
367 can be used for this.
368
369 The object returned is not saved.
370
371 =item C<post %params>
372
373 Posts the invoice. Required parameters are:
374
375 =over 2
376
377 =item * C<ar_id>
378
379 The ID of the accounds receivable chart the invoices amounts are
380 posted to. If it is not set then the first chart configured for
381 accounts receivables is used.
382
383 =back
384
385 This function implements several steps:
386
387 =over 2
388
389 =item 1. It calculates all prices, amounts and taxes by calling
390 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
391
392 =item 2. A new and unique invoice number is created.
393
394 =item 3. All amounts for costs of goods sold are recorded in
395 C<acc_trans>.
396
397 =item 4. All amounts for parts, services and assemblies are recorded
398 in C<acc_trans> with their respective charts. This is determined by
399 the part's buchungsgruppen.
400
401 =item 5. The total amount is posted to the accounts receivable chart
402 and recorded in C<acc_trans>.
403
404 =item 6. Items in C<invoice> are updated according to their allocation
405 status (regarding for costs of goold sold). Will only be done if
406 kivitendo is not configured to use Einnahmenüberschussrechnungen.
407
408 =item 7. The invoice and its items are saved.
409
410 =back
411
412 Returns C<$self> on success and C<undef> on failure. The whole process
413 is run inside a transaction. If it fails then nothing is saved to or
414 changed in the database. A new transaction is only started if none is
415 active.
416
417 =item C<basic_info $field>
418
419 See L<SL::DB::Object::basic_info>.
420
421 =back
422
423 =head1 AUTHOR
424
425 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
426
427 =cut