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