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