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::FlattenToForm;
16 use SL::DB::Helper::LinkedRecords;
17 use SL::DB::Helper::PriceTaxCalculator;
18 use SL::DB::Helper::PriceUpdater;
19 use SL::DB::Helper::TransNumberGenerator;
20 use SL::Locale::String qw(t8);
22 __PACKAGE__->meta->add_relationship(
24 type => 'one to many',
25 class => 'SL::DB::InvoiceItem',
26 column_map => { id => 'trans_id' },
28 with_objects => [ 'part' ]
32 type => 'one to many',
33 class => 'SL::DB::Invoice',
34 column_map => { id => 'storno_id' },
36 sepa_export_items => {
37 type => 'one to many',
38 class => 'SL::DB::SepaExportItem',
39 column_map => { id => 'ar_id' },
40 manager_args => { with_objects => [ 'sepa_export' ] }
44 class => 'SL::DB::Shipto',
45 column_map => { id => 'trans_id' },
46 query_args => [ module => 'AR' ],
49 type => 'one to many',
50 class => 'SL::DB::AccTransaction',
51 column_map => { id => 'trans_id' },
53 with_objects => [ 'chart' ],
54 sort_by => 'acc_trans_id ASC',
59 __PACKAGE__->meta->initialize;
61 __PACKAGE__->before_save('_before_save_set_invnumber');
65 sub _before_save_set_invnumber {
68 $self->create_trans_number if !$self->invnumber;
75 sub items { goto &invoiceitems; }
76 sub add_items { goto &add_invoiceitems; }
81 return [ sort {$a->position <=> $b->position } @{ $self->items } ];
85 # For compatibility with Order, DeliveryOrder
86 croak 'not an accessor' if @_ > 1;
90 # it is assumed, that ordnumbers are unique here.
91 sub first_order_by_ordnumber {
94 my $orders = SL::DB::Manager::Order->get_all(
96 ordnumber => $self->ordnumber,
101 return first { $_->is_type('sales_order') } @{ $orders };
104 sub abschlag_percentage {
106 my $order = $self->first_order_by_ordnumber or return;
107 my $order_amount = $order->netamount or return;
108 return $self->abschlag
109 ? $self->netamount / $order_amount
115 die 'not a setter method' if @_;
117 return ($self->amount || 0) - ($self->netamount || 0);
120 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
124 return $self->paid >= $self->amount;
127 sub _clone_orderitem_delivery_order_item_cvar {
130 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
131 $cloned->sub_module('invoice');
137 my ($class, $source, %params) = @_;
139 croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
140 croak("Cannot create invoices for purchase records") unless $source->customer_id;
142 require SL::DB::Employee;
144 my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms
145 : $source->customer_id ? $source ->customer->payment_terms
148 my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
150 if (ref($source) eq 'SL::DB::Order') {
151 @columns = qw(quonumber payment_id delivery_customer_id delivery_vendor_id);
152 @item_columns = qw(subtotal);
154 $item_parent_id_column = 'trans_id';
155 $item_parent_column = 'order';
158 @columns = qw(donumber);
160 $item_parent_id_column = 'delivery_order_id';
161 $item_parent_column = 'delivery_order';
164 my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
165 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
166 transdate => DateTime->today_local,
167 gldate => DateTime->today_local,
168 duedate => DateTime->today_local->add(days => ($terms ? $terms->terms_netto * 1 : 1)),
169 payment_id => $terms ? $terms->id : undef,
174 employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
177 if ($source->type =~ /_order$/) {
178 $args{deliverydate} = $source->reqdate;
179 $args{orddate} = $source->transdate;
181 $args{quodate} = $source->transdate;
184 my $invoice = $class->new(%args);
185 $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
186 my $items = delete($params{items}) || $source->items_sorted;
190 my $source_item = $_;
191 my $source_item_id = $_->$item_parent_id_column;
192 my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
194 $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
195 my $item_parent = $item_parents{$source_item_id};
197 SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
198 qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
199 base_qty longdescription lastcost price_factor_id), @item_columns),
200 deliverydate => $source_item->reqdate,
201 fxsellprice => $source_item->sellprice,
202 custom_variables => \@custom_variables,
203 ordnumber => ref($item_parent) eq 'SL::DB::Order' ? $item_parent->ordnumber : $source_item->ordnumber,
204 donumber => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber : $source_item->can('donumber') ? $source_item->donumber : '',
209 @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
210 @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
212 $invoice->invoiceitems(\@items);
218 my ($self, %params) = @_;
220 require SL::DB::Chart;
221 if (!$params{ar_id}) {
222 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
225 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
226 $params{ar_id} = $chart->id;
230 my %data = $self->calculate_prices_and_taxes;
232 $self->_post_create_assemblyitem_entries($data{assembly_items});
235 $self->_post_add_acctrans($data{amounts_cogs});
236 $self->_post_add_acctrans($data{amounts});
237 $self->_post_add_acctrans($data{taxes});
239 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
241 $self->_post_update_allocated($data{allocated});
244 if ($self->db->in_transaction) {
246 } elsif (!$self->db->do_transaction($worker)) {
247 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
254 sub _post_add_acctrans {
255 my ($self, $entries) = @_;
257 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
260 require SL::DB::AccTransaction;
261 require SL::DB::Chart;
262 while (my ($chart_id, $spec) = each %{ $entries }) {
263 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
264 $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
267 SL::DB::AccTransaction->new(trans_id => $self->id,
268 chart_id => $chart_id,
269 amount => $spec->{amount},
270 tax_id => $spec->{tax_id},
271 taxkey => $spec->{taxkey},
272 project_id => $self->globalproject_id,
273 transdate => $self->transdate,
274 chart_link => $chart_link)->save;
278 sub _post_create_assemblyitem_entries {
279 my ($self, $assembly_entries) = @_;
281 my $items = $self->invoiceitems;
285 foreach my $item (@{ $items }) {
286 next if $item->assemblyitem;
288 push @new_items, $item;
291 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
292 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
293 description => $assembly_item->{part}->description,
294 unit => $assembly_item->{part}->unit,
295 qty => $assembly_item->{qty},
296 allocated => $assembly_item->{allocated},
299 assemblyitem => 't');
303 $self->invoiceitems(\@new_items);
306 sub _post_update_allocated {
307 my ($self, $allocated) = @_;
309 while (my ($invoice_id, $diff) = each %{ $allocated }) {
310 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
311 where => [ id => $invoice_id ]);
318 return 'ar_transaction' if !$self->invoice;
319 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
320 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
321 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
325 sub displayable_state {
328 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
331 sub displayable_type {
334 return t8('AR Transaction') if $self->invoice_type eq 'ar_transaction';
335 return t8('Credit Note') if $self->invoice_type eq 'credit_note';
336 return t8('Invoice') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'invoice_storno';
337 return t8('Credit Note') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'credit_note_storno';
338 return t8('Invoice');
344 return t8('AR Transaction (abbreviation)') if $self->invoice_type eq 'ar_transaction';
345 return t8('Credit note (one letter abbreviation)') if $self->invoice_type eq 'credit_note';
346 return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'invoice_storno';
347 return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'credit_note_storno';
348 return t8('Invoice (one letter abbreviation)');
371 SL::DB::Invoice: Rose model for invoices (table "ar")
377 =item C<new_from $source, %params>
379 Creates a new C<SL::DB::Invoice> instance and copies as much
380 information from C<$source> as possible. At the moment only sales
381 orders and sales quotations are supported as sources.
383 The conversion copies order items into invoice items. Dates are copied
384 as appropriate, e.g. the C<transdate> field from an order will be
385 copied into the invoice's C<orddate> field.
387 C<%params> can include the following options:
393 An optional array reference of RDBO instances for the items to use. If
394 missing then the method C<items_sorted> will be called on
395 C<$source>. This option can be used to override the sorting, to
396 exclude certain positions or to add additional ones.
398 =item C<skip_items_negative_qty>
400 If trueish then items with a negative quantity are skipped. Items with
401 a quantity of 0 are not affected by this option.
403 =item C<skip_items_zero_qty>
405 If trueish then items with a quantity of 0 are skipped.
409 An optional hash reference. If it exists then it is passed to C<new>
410 allowing the caller to set certain attributes for the new delivery
415 Amounts, prices and taxes are not
416 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
417 can be used for this.
419 The object returned is not saved.
421 =item C<post %params>
423 Posts the invoice. Required parameters are:
429 The ID of the accounds receivable chart the invoices amounts are
430 posted to. If it is not set then the first chart configured for
431 accounts receivables is used.
435 This function implements several steps:
439 =item 1. It calculates all prices, amounts and taxes by calling
440 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
442 =item 2. A new and unique invoice number is created.
444 =item 3. All amounts for costs of goods sold are recorded in
447 =item 4. All amounts for parts, services and assemblies are recorded
448 in C<acc_trans> with their respective charts. This is determined by
449 the part's buchungsgruppen.
451 =item 5. The total amount is posted to the accounts receivable chart
452 and recorded in C<acc_trans>.
454 =item 6. Items in C<invoice> are updated according to their allocation
455 status (regarding for costs of goold sold). Will only be done if
456 kivitendo is not configured to use Einnahmenüberschussrechnungen.
458 =item 7. The invoice and its items are saved.
462 Returns C<$self> on success and C<undef> on failure. The whole process
463 is run inside a transaction. If it fails then nothing is saved to or
464 changed in the database. A new transaction is only started if none is
467 =item C<basic_info $field>
469 See L<SL::DB::Object::basic_info>.
475 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>