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