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