chart.link in der acc_trans
[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   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 1;
245
246 __END__
247
248 =pod
249
250 =head1 NAME
251
252 SL::DB::Invoice: Rose model for invoices (table "ar")
253
254 =head1 FUNCTIONS
255
256 =over 4
257
258 =item C<new_from $source>
259
260 Creates a new C<SL::DB::Invoice> instance and copies as much
261 information from C<$source> as possible. At the moment only sales
262 orders and sales quotations are supported as sources.
263
264 The conversion copies order items into invoice items. Dates are copied
265 as appropriate, e.g. the C<transdate> field from an order will be
266 copied into the invoice's C<orddate> field.
267
268 Amounts, prices and taxes are not
269 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
270 can be used for this.
271
272 The object returned is not saved.
273
274 =item C<post %params>
275
276 Posts the invoice. Required parameters are:
277
278 =over 2
279
280 =item * C<ar_id>
281
282 The ID of the accounds receivable chart the invoices amounts are
283 posted to. If it is not set then the first chart configured for
284 accounts receivables is used.
285
286 =back
287
288 This function implements several steps:
289
290 =over 2
291
292 =item 1. It calculates all prices, amounts and taxes by calling
293 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
294
295 =item 2. A new and unique invoice number is created.
296
297 =item 3. All amounts for costs of goods sold are recorded in
298 C<acc_trans>.
299
300 =item 4. All amounts for parts, services and assemblies are recorded
301 in C<acc_trans> with their respective charts. This is determined by
302 the part's buchungsgruppen.
303
304 =item 5. The total amount is posted to the accounts receivable chart
305 and recorded in C<acc_trans>.
306
307 =item 6. Items in C<invoice> are updated according to their allocation
308 status (regarding for costs of goold sold). Will only be done if
309 kivitendo is not configured to use Einnahmenüberschussrechnungen.
310
311 =item 7. The invoice and its items are saved.
312
313 =back
314
315 Returns C<$self> on success and C<undef> on failure. The whole process
316 is run inside a transaction. If it fails then nothing is saved to or
317 changed in the database. A new transaction is only started if none is
318 active.
319
320 =item C<basic_info $field>
321
322 See L<SL::DB::Object::basic_info>.
323
324 =back
325
326 =head1 AUTHOR
327
328 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
329
330 =cut