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