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