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