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