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