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