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