516352e8aea9f9c397b979e9aa6beebda705c909
[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_term->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   while (my ($chart_id, $spec) = each %{ $entries }) {
173     $spec = { taxkey => 0, amount => $spec } unless ref $spec;
174     SL::DB::AccTransaction->new(trans_id   => $self->id,
175                                 chart_id   => $chart_id,
176                                 amount     => $spec->{amount},
177                                 taxkey     => $spec->{taxkey},
178                                 project_id => $self->globalproject_id,
179                                 transdate  => $self->transdate)->save;
180   }
181 }
182
183 sub _post_create_assemblyitem_entries {
184   my ($self, $assembly_entries) = @_;
185
186   my $items = $self->invoiceitems;
187   my @new_items;
188
189   my $item_idx = 0;
190   foreach my $item (@{ $items }) {
191     next if $item->assemblyitem;
192
193     push @new_items, $item;
194     $item_idx++;
195
196     foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
197       push @new_items, SL::DB::InvoiceItem->new(parts_id     => $assembly_item->{part},
198                                                 description  => $assembly_item->{part}->description,
199                                                 unit         => $assembly_item->{part}->unit,
200                                                 qty          => $assembly_item->{qty},
201                                                 allocated    => $assembly_item->{allocated},
202                                                 sellprice    => 0,
203                                                 fxsellprice  => 0,
204                                                 assemblyitem => 't');
205     }
206   }
207
208   $self->invoiceitems(\@new_items);
209 }
210
211 sub _post_update_allocated {
212   my ($self, $allocated) = @_;
213
214   while (my ($invoice_id, $diff) = each %{ $allocated }) {
215     SL::DB::Manager::InvoiceItem->update_all(set   => { allocated => { sql => "allocated + $diff" } },
216                                              where => [ id        => $invoice_id ]);
217   }
218 }
219
220 sub invoice_type {
221   my ($self) = @_;
222
223   return 'ar_transaction'     if !$self->invoice;
224   return 'credit_note'        if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
225   return 'invoice_storno'     if $self->type ne 'credit_note' && $self->amount < 0 &&  $self->storno;
226   return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 &&  $self->storno;
227   return 'invoice';
228 }
229
230 sub displayable_state {
231   my $self = shift;
232
233   return $self->closed ? $::locale->text('closed') : $::locale->text('open');
234 }
235
236 1;
237
238 __END__
239
240 =pod
241
242 =head1 NAME
243
244 SL::DB::Invoice: Rose model for invoices (table "ar")
245
246 =head1 FUNCTIONS
247
248 =over 4
249
250 =item C<new_from $source>
251
252 Creates a new C<SL::DB::Invoice> instance and copies as much
253 information from C<$source> as possible. At the moment only sales
254 orders and sales quotations are supported as sources.
255
256 The conversion copies order items into invoice items. Dates are copied
257 as appropriate, e.g. the C<transdate> field from an order will be
258 copied into the invoice's C<orddate> field.
259
260 Amounts, prices and taxes are not
261 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
262 can be used for this.
263
264 The object returned is not saved.
265
266 =item C<post %params>
267
268 Posts the invoice. Required parameters are:
269
270 =over 2
271
272 =item * C<ar_id>
273
274 The ID of the accounds receivable chart the invoices amounts are
275 posted to. If it is not set then the first chart configured for
276 accounts receivables is used.
277
278 =back
279
280 This function implements several steps:
281
282 =over 2
283
284 =item 1. It calculates all prices, amounts and taxes by calling
285 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
286
287 =item 2. A new and unique invoice number is created.
288
289 =item 3. All amounts for costs of goods sold are recorded in
290 C<acc_trans>.
291
292 =item 4. All amounts for parts, services and assemblies are recorded
293 in C<acc_trans> with their respective charts. This is determined by
294 the part's buchungsgruppen.
295
296 =item 5. The total amount is posted to the accounts receivable chart
297 and recorded in C<acc_trans>.
298
299 =item 6. Items in C<invoice> are updated according to their allocation
300 status (regarding for costs of goold sold). Will only be done if
301 kivitendo is not configured to use Einnahmenüberschussrechnungen.
302
303 =item 7. The invoice and its items are saved.
304
305 =back
306
307 Returns C<$self> on success and C<undef> on failure. The whole process
308 is run inside a transaction. If it fails then nothing is saved to or
309 changed in the database. A new transaction is only started if none is
310 active.
311
312 =item C<basic_info $field>
313
314 See L<SL::DB::Object::basic_info>.
315
316 =back
317
318 =head1 AUTHOR
319
320 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
321
322 =cut