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