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