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