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