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