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};
196 my $current_invoice_item =
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 active_discount_source active_price_source), @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 : '',
207 $current_invoice_item->{"converted_from_orderitems_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
208 $current_invoice_item->{"converted_from_delivery_order_items_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::DeliveryOrder';
209 $current_invoice_item;
212 @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
213 @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
215 $invoice->invoiceitems(\@items);
221 my ($self, %params) = @_;
223 require SL::DB::Chart;
224 if (!$params{ar_id}) {
225 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
228 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
229 $params{ar_id} = $chart->id;
233 my %data = $self->calculate_prices_and_taxes;
235 $self->_post_create_assemblyitem_entries($data{assembly_items});
238 $self->_post_add_acctrans($data{amounts_cogs});
239 $self->_post_add_acctrans($data{amounts});
240 $self->_post_add_acctrans($data{taxes});
242 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
244 $self->_post_update_allocated($data{allocated});
247 if ($self->db->in_transaction) {
249 } elsif (!$self->db->do_transaction($worker)) {
250 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
257 sub _post_add_acctrans {
258 my ($self, $entries) = @_;
260 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
263 require SL::DB::AccTransaction;
264 require SL::DB::Chart;
265 while (my ($chart_id, $spec) = each %{ $entries }) {
266 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
267 $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
270 SL::DB::AccTransaction->new(trans_id => $self->id,
271 chart_id => $chart_id,
272 amount => $spec->{amount},
273 tax_id => $spec->{tax_id},
274 taxkey => $spec->{taxkey},
275 project_id => $self->globalproject_id,
276 transdate => $self->transdate,
277 chart_link => $chart_link)->save;
281 sub _post_create_assemblyitem_entries {
282 my ($self, $assembly_entries) = @_;
284 my $items = $self->invoiceitems;
288 foreach my $item (@{ $items }) {
289 next if $item->assemblyitem;
291 push @new_items, $item;
294 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
295 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
296 description => $assembly_item->{part}->description,
297 unit => $assembly_item->{part}->unit,
298 qty => $assembly_item->{qty},
299 allocated => $assembly_item->{allocated},
302 assemblyitem => 't');
306 $self->invoiceitems(\@new_items);
309 sub _post_update_allocated {
310 my ($self, $allocated) = @_;
312 while (my ($invoice_id, $diff) = each %{ $allocated }) {
313 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
314 where => [ id => $invoice_id ]);
321 return 'ar_transaction' if !$self->invoice;
322 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
323 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
324 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
328 sub displayable_state {
331 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
334 sub displayable_type {
337 return t8('AR Transaction') if $self->invoice_type eq 'ar_transaction';
338 return t8('Credit Note') if $self->invoice_type eq 'credit_note';
339 return t8('Invoice') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'invoice_storno';
340 return t8('Credit Note') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'credit_note_storno';
341 return t8('Invoice');
347 return t8('AR Transaction (abbreviation)') if $self->invoice_type eq 'ar_transaction';
348 return t8('Credit note (one letter abbreviation)') if $self->invoice_type eq 'credit_note';
349 return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'invoice_storno';
350 return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'credit_note_storno';
351 return t8('Invoice (one letter abbreviation)');
374 SL::DB::Invoice: Rose model for invoices (table "ar")
380 =item C<new_from $source, %params>
382 Creates a new C<SL::DB::Invoice> instance and copies as much
383 information from C<$source> as possible. At the moment only sales
384 orders and sales quotations are supported as sources.
386 The conversion copies order items into invoice items. Dates are copied
387 as appropriate, e.g. the C<transdate> field from an order will be
388 copied into the invoice's C<orddate> field.
390 C<%params> can include the following options:
396 An optional array reference of RDBO instances for the items to use. If
397 missing then the method C<items_sorted> will be called on
398 C<$source>. This option can be used to override the sorting, to
399 exclude certain positions or to add additional ones.
401 =item C<skip_items_negative_qty>
403 If trueish then items with a negative quantity are skipped. Items with
404 a quantity of 0 are not affected by this option.
406 =item C<skip_items_zero_qty>
408 If trueish then items with a quantity of 0 are skipped.
412 An optional hash reference. If it exists then it is passed to C<new>
413 allowing the caller to set certain attributes for the new delivery
418 Amounts, prices and taxes are not
419 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
420 can be used for this.
422 The object returned is not saved.
424 =item C<post %params>
426 Posts the invoice. Required parameters are:
432 The ID of the accounds receivable chart the invoices amounts are
433 posted to. If it is not set then the first chart configured for
434 accounts receivables is used.
438 This function implements several steps:
442 =item 1. It calculates all prices, amounts and taxes by calling
443 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
445 =item 2. A new and unique invoice number is created.
447 =item 3. All amounts for costs of goods sold are recorded in
450 =item 4. All amounts for parts, services and assemblies are recorded
451 in C<acc_trans> with their respective charts. This is determined by
452 the part's buchungsgruppen.
454 =item 5. The total amount is posted to the accounts receivable chart
455 and recorded in C<acc_trans>.
457 =item 6. Items in C<invoice> are updated according to their allocation
458 status (regarding for costs of goold sold). Will only be done if
459 kivitendo is not configured to use Einnahmenüberschussrechnungen.
461 =item 7. The invoice and its items are saved.
465 Returns C<$self> on success and C<undef> on failure. The whole process
466 is run inside a transaction. If it fails then nothing is saved to or
467 changed in the database. A new transaction is only started if none is
470 =item C<basic_info $field>
472 See L<SL::DB::Object::basic_info>.
478 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>