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