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.
4 package SL::DB::Invoice;
9 use List::Util qw(first);
11 use Rose::DB::Object::Helpers ();
13 use SL::DB::MetaSetup::Invoice;
14 use SL::DB::Manager::Invoice;
15 use SL::DB::Helper::AttrHTML;
16 use SL::DB::Helper::FlattenToForm;
17 use SL::DB::Helper::LinkedRecords;
18 use SL::DB::Helper::PriceTaxCalculator;
19 use SL::DB::Helper::PriceUpdater;
20 use SL::DB::Helper::TransNumberGenerator;
21 use SL::Locale::String qw(t8);
22 use SL::DB::CustomVariable;
23 use SL::DB::AccTransaction;
25 __PACKAGE__->meta->add_relationship(
27 type => 'one to many',
28 class => 'SL::DB::InvoiceItem',
29 column_map => { id => 'trans_id' },
31 with_objects => [ 'part' ]
35 type => 'one to many',
36 class => 'SL::DB::Invoice',
37 column_map => { id => 'storno_id' },
39 sepa_export_items => {
40 type => 'one to many',
41 class => 'SL::DB::SepaExportItem',
42 column_map => { id => 'ar_id' },
43 manager_args => { with_objects => [ 'sepa_export' ] }
47 class => 'SL::DB::Shipto',
48 column_map => { id => 'trans_id' },
49 query_args => [ module => 'AR' ],
52 type => 'one to many',
53 class => 'SL::DB::AccTransaction',
54 column_map => { id => 'trans_id' },
56 with_objects => [ 'chart' ],
57 sort_by => 'acc_trans_id ASC',
62 __PACKAGE__->meta->initialize;
64 __PACKAGE__->attr_html('notes');
66 __PACKAGE__->before_save('_before_save_set_invnumber');
70 sub _before_save_set_invnumber {
73 $self->create_trans_number if !$self->invnumber;
80 sub items { goto &invoiceitems; }
81 sub add_items { goto &add_invoiceitems; }
86 return [ sort {$a->position <=> $b->position } @{ $self->items } ];
90 # For compatibility with Order, DeliveryOrder
91 croak 'not an accessor' if @_ > 1;
95 # it is assumed, that ordnumbers are unique here.
96 sub first_order_by_ordnumber {
99 my $orders = SL::DB::Manager::Order->get_all(
101 ordnumber => $self->ordnumber,
106 return first { $_->is_type('sales_order') } @{ $orders };
109 sub abschlag_percentage {
111 my $order = $self->first_order_by_ordnumber or return;
112 my $order_amount = $order->netamount or return;
113 return $self->abschlag
114 ? $self->netamount / $order_amount
120 die 'not a setter method' if @_;
122 return ($self->amount || 0) - ($self->netamount || 0);
125 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
129 return $self->paid >= $self->amount;
132 sub _clone_orderitem_delivery_order_item_cvar {
135 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
136 $cloned->sub_module('invoice');
142 my ($class, $source, %params) = @_;
144 croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
145 croak("Cannot create invoices for purchase records") unless $source->customer_id;
147 require SL::DB::Employee;
149 my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms
150 : $source->customer_id ? $source ->customer->payment_terms
153 my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
155 if (ref($source) eq 'SL::DB::Order') {
156 @columns = qw(quonumber payment_id delivery_customer_id delivery_vendor_id);
157 @item_columns = qw(subtotal);
159 $item_parent_id_column = 'trans_id';
160 $item_parent_column = 'order';
163 @columns = qw(donumber);
165 $item_parent_id_column = 'delivery_order_id';
166 $item_parent_column = 'delivery_order';
169 my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
170 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
171 transdate => DateTime->today_local,
172 gldate => DateTime->today_local,
173 duedate => DateTime->today_local->add(days => ($terms ? $terms->terms_netto * 1 : 1)),
174 payment_id => $terms ? $terms->id : undef,
179 employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
182 if ($source->type =~ /_order$/) {
183 $args{deliverydate} = $source->reqdate;
184 $args{orddate} = $source->transdate;
186 $args{quodate} = $source->transdate;
189 my $invoice = $class->new(%args);
190 $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
191 my $items = delete($params{items}) || $source->items_sorted;
195 my $source_item = $_;
196 my $source_item_id = $_->$item_parent_id_column;
197 my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
199 $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
200 my $item_parent = $item_parents{$source_item_id};
201 my $current_invoice_item =
202 SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
203 qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
204 base_qty longdescription lastcost price_factor_id active_discount_source active_price_source), @item_columns),
205 deliverydate => $source_item->reqdate,
206 fxsellprice => $source_item->sellprice,
207 custom_variables => \@custom_variables,
208 ordnumber => ref($item_parent) eq 'SL::DB::Order' ? $item_parent->ordnumber : $source_item->ordnumber,
209 donumber => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber : $source_item->can('donumber') ? $source_item->donumber : '',
212 $current_invoice_item->{"converted_from_orderitems_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
213 $current_invoice_item->{"converted_from_delivery_order_items_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::DeliveryOrder';
214 $current_invoice_item;
217 @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
218 @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
220 $invoice->invoiceitems(\@items);
226 my ($self, %params) = @_;
228 require SL::DB::Chart;
229 if (!$params{ar_id}) {
230 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
233 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
234 $params{ar_id} = $chart->id;
238 my %data = $self->calculate_prices_and_taxes;
240 $self->_post_create_assemblyitem_entries($data{assembly_items});
243 $self->_post_add_acctrans($data{amounts_cogs});
244 $self->_post_add_acctrans($data{amounts});
245 $self->_post_add_acctrans($data{taxes});
247 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
249 $self->_post_update_allocated($data{allocated});
252 if ($self->db->in_transaction) {
254 } elsif (!$self->db->do_transaction($worker)) {
255 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
262 sub _post_add_acctrans {
263 my ($self, $entries) = @_;
265 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
268 require SL::DB::AccTransaction;
269 require SL::DB::Chart;
270 while (my ($chart_id, $spec) = each %{ $entries }) {
271 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
272 $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
275 SL::DB::AccTransaction->new(trans_id => $self->id,
276 chart_id => $chart_id,
277 amount => $spec->{amount},
278 tax_id => $spec->{tax_id},
279 taxkey => $spec->{taxkey},
280 project_id => $self->globalproject_id,
281 transdate => $self->transdate,
282 chart_link => $chart_link)->save;
286 sub _post_create_assemblyitem_entries {
287 my ($self, $assembly_entries) = @_;
289 my $items = $self->invoiceitems;
293 foreach my $item (@{ $items }) {
294 next if $item->assemblyitem;
296 push @new_items, $item;
299 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
300 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
301 description => $assembly_item->{part}->description,
302 unit => $assembly_item->{part}->unit,
303 qty => $assembly_item->{qty},
304 allocated => $assembly_item->{allocated},
307 assemblyitem => 't');
311 $self->invoiceitems(\@new_items);
314 sub _post_update_allocated {
315 my ($self, $allocated) = @_;
317 while (my ($invoice_id, $diff) = each %{ $allocated }) {
318 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
319 where => [ id => $invoice_id ]);
326 return 'ar_transaction' if !$self->invoice;
327 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
328 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
329 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
333 sub displayable_state {
336 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
339 sub displayable_type {
342 return t8('AR Transaction') if $self->invoice_type eq 'ar_transaction';
343 return t8('Credit Note') if $self->invoice_type eq 'credit_note';
344 return t8('Invoice') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'invoice_storno';
345 return t8('Credit Note') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'credit_note_storno';
346 return t8('Invoice');
352 return t8('AR Transaction (abbreviation)') if $self->invoice_type eq 'ar_transaction';
353 return t8('Credit note (one letter abbreviation)') if $self->invoice_type eq 'credit_note';
354 return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'invoice_storno';
355 return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'credit_note_storno';
356 return t8('Invoice (one letter abbreviation)');
372 my ($self, %params) = @_;
374 #Mark invoice as paid
375 $self->paid($self->paid+$params{amount});
378 Common::check_params(\%params, qw(chart_id trans_id amount transdate));
380 #account of bank account or cash
381 my $account_bank = SL::DB::Manager::Chart->find_by(id => $params{chart_id});
383 #Search the contra account
384 my $acc_trans = SL::DB::Manager::AccTransaction->find_by(trans_id => $params{trans_id},
385 or => [ chart_link => { like => "%:AR" },
386 chart_link => { like => "AR:%" },
387 chart_link => "AR" ]);
388 my $contra_account = SL::DB::Manager::Chart->find_by(id => $acc_trans->chart_id);
390 #Two new transfers in acc_trans (for bank account and for contra account)
391 my $new_acc_trans = SL::DB::AccTransaction->new(trans_id => $params{trans_id},
392 chart_id => $account_bank->id,
393 chart_link => $account_bank->link,
394 amount => (-1 * $params{amount}),
395 transdate => $params{transdate},
396 source => $params{source},
399 tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
400 $new_acc_trans->save;
401 $new_acc_trans = SL::DB::AccTransaction->new(trans_id => $params{trans_id},
402 chart_id => $contra_account->id,
403 chart_link => $contra_account->link,
404 amount => $params{amount},
405 transdate => $params{transdate},
406 source => $params{source},
409 tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
410 $new_acc_trans->save;
417 $html = SL::Presenter->get->sales_invoice($self, display => 'inline') if $self->invoice;
418 $html = SL::Presenter->get->ar_transaction($self, display => 'inline') if !$self->invoice;
423 >>>>>>> Test: Bank-Commit zusammengefasst
432 SL::DB::Invoice: Rose model for invoices (table "ar")
438 =item C<new_from $source, %params>
440 Creates a new C<SL::DB::Invoice> instance and copies as much
441 information from C<$source> as possible. At the moment only sales
442 orders and sales quotations are supported as sources.
444 The conversion copies order items into invoice items. Dates are copied
445 as appropriate, e.g. the C<transdate> field from an order will be
446 copied into the invoice's C<orddate> field.
448 C<%params> can include the following options:
454 An optional array reference of RDBO instances for the items to use. If
455 missing then the method C<items_sorted> will be called on
456 C<$source>. This option can be used to override the sorting, to
457 exclude certain positions or to add additional ones.
459 =item C<skip_items_negative_qty>
461 If trueish then items with a negative quantity are skipped. Items with
462 a quantity of 0 are not affected by this option.
464 =item C<skip_items_zero_qty>
466 If trueish then items with a quantity of 0 are skipped.
470 An optional hash reference. If it exists then it is passed to C<new>
471 allowing the caller to set certain attributes for the new delivery
476 Amounts, prices and taxes are not
477 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
478 can be used for this.
480 The object returned is not saved.
482 =item C<post %params>
484 Posts the invoice. Required parameters are:
490 The ID of the accounds receivable chart the invoices amounts are
491 posted to. If it is not set then the first chart configured for
492 accounts receivables is used.
496 This function implements several steps:
500 =item 1. It calculates all prices, amounts and taxes by calling
501 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
503 =item 2. A new and unique invoice number is created.
505 =item 3. All amounts for costs of goods sold are recorded in
508 =item 4. All amounts for parts, services and assemblies are recorded
509 in C<acc_trans> with their respective charts. This is determined by
510 the part's buchungsgruppen.
512 =item 5. The total amount is posted to the accounts receivable chart
513 and recorded in C<acc_trans>.
515 =item 6. Items in C<invoice> are updated according to their allocation
516 status (regarding for costs of goold sold). Will only be done if
517 kivitendo is not configured to use Einnahmenüberschussrechnungen.
519 =item 7. The invoice and its items are saved.
523 Returns C<$self> on success and C<undef> on failure. The whole process
524 is run inside a transaction. If it fails then nothing is saved to or
525 changed in the database. A new transaction is only started if none is
528 =item C<basic_info $field>
530 See L<SL::DB::Object::basic_info>.
536 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>