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