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