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