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