neue Methode items_sorted für Order, DeliveryOrder, Invoice, PurchaseInvoice
[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 SL::DB::MetaSetup::Invoice;
12 use SL::DB::Manager::Invoice;
13 use SL::DB::Helper::FlattenToForm;
14 use SL::DB::Helper::LinkedRecords;
15 use SL::DB::Helper::PriceTaxCalculator;
16 use SL::DB::Helper::PriceUpdater;
17 use SL::DB::Helper::TransNumberGenerator;
18 use SL::DB::AccTransaction;
19 use SL::DB::Chart;
20 use SL::DB::Employee;
21
22 __PACKAGE__->meta->add_relationship(
23   invoiceitems => {
24     type         => 'one to many',
25     class        => 'SL::DB::InvoiceItem',
26     column_map   => { id => 'trans_id' },
27     manager_args => {
28       with_objects => [ 'part' ]
29     }
30   },
31   storno_invoices => {
32     type          => 'one to many',
33     class         => 'SL::DB::Invoice',
34     column_map    => { id => 'storno_id' },
35   },
36   sepa_export_items => {
37     type            => 'one to many',
38     class           => 'SL::DB::SepaExportItem',
39     column_map      => { id => 'ar_id' },
40     manager_args    => { with_objects => [ 'sepa_export' ] }
41   },
42 );
43
44 __PACKAGE__->meta->initialize;
45
46 # methods
47
48 sub items { goto &invoiceitems; }
49
50 sub items_sorted {
51   my ($self) = @_;
52
53   my @sorted =  sort {$a->id <=> $b->id } @{ $self->items };
54   return wantarray ? @sorted : \@sorted;
55 }
56
57 sub is_sales {
58   # For compatibility with Order, DeliveryOrder
59   croak 'not an accessor' if @_ > 1;
60   return 1;
61 }
62
63 # it is assumed, that ordnumbers are unique here.
64 sub first_order_by_ordnumber {
65   my $self = shift;
66
67   my $orders = SL::DB::Manager::Order->get_all(
68     query => [
69       ordnumber => $self->ordnumber,
70
71     ],
72   );
73
74   return first { $_->is_type('sales_order') } @{ $orders };
75 }
76
77 sub abschlag_percentage {
78   my $self         = shift;
79   my $order        = $self->first_order_by_ordnumber or return;
80   my $order_amount = $order->netamount               or return;
81   return $self->abschlag
82     ? $self->netamount / $order_amount
83     : undef;
84 }
85
86 sub taxamount {
87   my $self = shift;
88   die 'not a setter method' if @_;
89
90   return ($self->amount || 0) - ($self->netamount || 0);
91 }
92
93 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
94
95 sub closed {
96   my ($self) = @_;
97   return $self->paid >= $self->amount;
98 }
99
100 sub new_from {
101   my ($class, $source, %params) = @_;
102
103   croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
104   croak("Cannot create invoices for purchase records")           unless $source->customer_id;
105
106   my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms->terms_netto : 0;
107
108   my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber quonumber
109                                                 department_id cp_id language_id payment_id delivery_customer_id delivery_vendor_id taxzone_id shipto_id
110                                                 globalproject_id transaction_description currency_id)),
111                transdate   => DateTime->today_local,
112                gldate      => DateTime->today_local,
113                duedate     => DateTime->today_local->add(days => $terms * 1),
114                invoice     => 1,
115                type        => 'invoice',
116                storno      => 0,
117                paid        => 0,
118                employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
119             );
120
121   if ($source->type =~ /_order$/) {
122     $args{deliverydate} = $source->reqdate;
123     $args{orddate}      = $source->transdate;
124   } else {
125     $args{quodate}      = $source->transdate;
126   }
127
128   my $invoice = $class->new(%args, %params);
129
130   my @items = map {
131     my $source_item = $_;
132     SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
133                                  qw(parts_id description qty sellprice discount project_id
134                                     serialnumber pricegroup_id ordnumber transdate cusordnumber unit
135                                     base_qty subtotal longdescription lastcost price_factor_id)),
136                             deliverydate => $source_item->reqdate,
137                             fxsellprice  => $source_item->sellprice,);
138   } @{ $source->items };
139
140   $invoice->invoiceitems(\@items);
141
142   return $invoice;
143 }
144
145 sub post {
146   my ($self, %params) = @_;
147
148   if (!$params{ar_id}) {
149     my $chart = SL::DB::Manager::Chart->get_all(query   => [ SL::DB::Manager::Chart->link_filter('AR') ],
150                                                 sort_by => 'id ASC',
151                                                 limit   => 1)->[0];
152     croak("No AR chart found and no parameter `ar_id' given") unless $chart;
153     $params{ar_id} = $chart->id;
154   }
155
156   my $worker = sub {
157     my %data = $self->calculate_prices_and_taxes;
158
159     $self->_post_create_assemblyitem_entries($data{assembly_items});
160     $self->create_trans_number;
161     $self->save;
162
163     $self->_post_add_acctrans($data{amounts_cogs});
164     $self->_post_add_acctrans($data{amounts});
165     $self->_post_add_acctrans($data{taxes});
166
167     $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
168
169     $self->_post_update_allocated($data{allocated});
170   };
171
172   if ($self->db->in_transaction) {
173     $worker->();
174   } elsif (!$self->db->do_transaction($worker)) {
175     $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
176     return undef;
177   }
178
179   return $self;
180 }
181
182 sub _post_add_acctrans {
183   my ($self, $entries) = @_;
184
185   my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
186   my $chart_link;
187
188   while (my ($chart_id, $spec) = each %{ $entries }) {
189     $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
190     $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
191     $chart_link ||= '';
192
193     SL::DB::AccTransaction->new(trans_id   => $self->id,
194                                 chart_id   => $chart_id,
195                                 amount     => $spec->{amount},
196                                 tax_id     => $spec->{tax_id},
197                                 taxkey     => $spec->{taxkey},
198                                 project_id => $self->globalproject_id,
199                                 transdate  => $self->transdate,
200                                 chart_link => $chart_link)->save;
201   }
202 }
203
204 sub _post_create_assemblyitem_entries {
205   my ($self, $assembly_entries) = @_;
206
207   my $items = $self->invoiceitems;
208   my @new_items;
209
210   my $item_idx = 0;
211   foreach my $item (@{ $items }) {
212     next if $item->assemblyitem;
213
214     push @new_items, $item;
215     $item_idx++;
216
217     foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
218       push @new_items, SL::DB::InvoiceItem->new(parts_id     => $assembly_item->{part},
219                                                 description  => $assembly_item->{part}->description,
220                                                 unit         => $assembly_item->{part}->unit,
221                                                 qty          => $assembly_item->{qty},
222                                                 allocated    => $assembly_item->{allocated},
223                                                 sellprice    => 0,
224                                                 fxsellprice  => 0,
225                                                 assemblyitem => 't');
226     }
227   }
228
229   $self->invoiceitems(\@new_items);
230 }
231
232 sub _post_update_allocated {
233   my ($self, $allocated) = @_;
234
235   while (my ($invoice_id, $diff) = each %{ $allocated }) {
236     SL::DB::Manager::InvoiceItem->update_all(set   => { allocated => { sql => "allocated + $diff" } },
237                                              where => [ id        => $invoice_id ]);
238   }
239 }
240
241 sub invoice_type {
242   my ($self) = @_;
243
244   return 'ar_transaction'     if !$self->invoice;
245   return 'credit_note'        if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
246   return 'invoice_storno'     if $self->type ne 'credit_note' && $self->amount < 0 &&  $self->storno;
247   return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 &&  $self->storno;
248   return 'invoice';
249 }
250
251 sub displayable_state {
252   my $self = shift;
253
254   return $self->closed ? $::locale->text('closed') : $::locale->text('open');
255 }
256
257 sub date {
258   goto &transdate;
259 }
260
261 1;
262
263 __END__
264
265 =pod
266
267 =head1 NAME
268
269 SL::DB::Invoice: Rose model for invoices (table "ar")
270
271 =head1 FUNCTIONS
272
273 =over 4
274
275 =item C<new_from $source>
276
277 Creates a new C<SL::DB::Invoice> instance and copies as much
278 information from C<$source> as possible. At the moment only sales
279 orders and sales quotations are supported as sources.
280
281 The conversion copies order items into invoice items. Dates are copied
282 as appropriate, e.g. the C<transdate> field from an order will be
283 copied into the invoice's C<orddate> field.
284
285 Amounts, prices and taxes are not
286 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
287 can be used for this.
288
289 The object returned is not saved.
290
291 =item C<post %params>
292
293 Posts the invoice. Required parameters are:
294
295 =over 2
296
297 =item * C<ar_id>
298
299 The ID of the accounds receivable chart the invoices amounts are
300 posted to. If it is not set then the first chart configured for
301 accounts receivables is used.
302
303 =back
304
305 This function implements several steps:
306
307 =over 2
308
309 =item 1. It calculates all prices, amounts and taxes by calling
310 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
311
312 =item 2. A new and unique invoice number is created.
313
314 =item 3. All amounts for costs of goods sold are recorded in
315 C<acc_trans>.
316
317 =item 4. All amounts for parts, services and assemblies are recorded
318 in C<acc_trans> with their respective charts. This is determined by
319 the part's buchungsgruppen.
320
321 =item 5. The total amount is posted to the accounts receivable chart
322 and recorded in C<acc_trans>.
323
324 =item 6. Items in C<invoice> are updated according to their allocation
325 status (regarding for costs of goold sold). Will only be done if
326 kivitendo is not configured to use Einnahmenüberschussrechnungen.
327
328 =item 7. The invoice and its items are saved.
329
330 =back
331
332 Returns C<$self> on success and C<undef> on failure. The whole process
333 is run inside a transaction. If it fails then nothing is saved to or
334 changed in the database. A new transaction is only started if none is
335 active.
336
337 =item C<basic_info $field>
338
339 See L<SL::DB::Object::basic_info>.
340
341 =back
342
343 =head1 AUTHOR
344
345 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
346
347 =cut