1 package SL::DB::Invoice;
6 use List::Util qw(first);
8 use Rose::DB::Object::Helpers ();
9 use SL::DB::MetaSetup::Invoice;
10 use SL::DB::Manager::Invoice;
11 use SL::DB::Helper::Payment qw(:ALL);
12 use SL::DB::Helper::AttrHTML;
13 use SL::DB::Helper::AttrSorted;
14 use SL::DB::Helper::FlattenToForm;
15 use SL::DB::Helper::LinkedRecords;
16 use SL::DB::Helper::PriceTaxCalculator;
17 use SL::DB::Helper::PriceUpdater;
18 use SL::DB::Helper::TransNumberGenerator;
19 use SL::Locale::String qw(t8);
20 use SL::DB::CustomVariable;
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__->attr_html('notes');
62 __PACKAGE__->attr_sorted('items');
64 __PACKAGE__->before_save('_before_save_set_invnumber');
68 sub _before_save_set_invnumber {
71 $self->create_trans_number if !$self->invnumber;
78 sub items { goto &invoiceitems; }
79 sub add_items { goto &add_invoiceitems; }
82 # For compatibility with Order, DeliveryOrder
83 croak 'not an accessor' if @_ > 1;
87 # it is assumed, that ordnumbers are unique here.
88 sub first_order_by_ordnumber {
91 my $orders = SL::DB::Manager::Order->get_all(
93 ordnumber => $self->ordnumber,
98 return first { $_->is_type('sales_order') } @{ $orders };
101 sub abschlag_percentage {
103 my $order = $self->first_order_by_ordnumber or return;
104 my $order_amount = $order->netamount or return;
105 return $self->abschlag
106 ? $self->netamount / $order_amount
112 die 'not a setter method' if @_;
114 return ($self->amount || 0) - ($self->netamount || 0);
117 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
121 return $self->paid >= $self->amount;
124 sub _clone_orderitem_delivery_order_item_cvar {
127 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
128 $cloned->sub_module('invoice');
134 my ($class, $source, %params) = @_;
136 croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
137 croak("Cannot create invoices for purchase records") unless $source->customer_id;
139 require SL::DB::Employee;
141 my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
143 if (ref($source) eq 'SL::DB::Order') {
144 @columns = qw(quonumber delivery_customer_id delivery_vendor_id);
145 @item_columns = qw(subtotal);
147 $item_parent_id_column = 'trans_id';
148 $item_parent_column = 'order';
151 @columns = qw(donumber);
153 $item_parent_id_column = 'delivery_order_id';
154 $item_parent_column = 'delivery_order';
157 my $terms = $source->can('payment_id') ? $source->payment_terms : undef;
159 my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
160 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id payment_id), @columns),
161 transdate => DateTime->today_local,
162 gldate => DateTime->today_local,
163 duedate => $terms ? $terms->calc_date(reference_date => DateTime->today_local) : DateTime->today_local,
168 employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
171 if ($source->type =~ /_order$/) {
172 $args{deliverydate} = $source->reqdate;
173 $args{orddate} = $source->transdate;
175 $args{quodate} = $source->transdate;
178 my $invoice = $class->new(%args);
179 $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
180 my $items = delete($params{items}) || $source->items_sorted;
184 my $source_item = $_;
185 my $source_item_id = $_->$item_parent_id_column;
186 my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
188 $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
189 my $item_parent = $item_parents{$source_item_id};
190 my $current_invoice_item =
191 SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
192 qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
193 base_qty longdescription lastcost price_factor_id active_discount_source active_price_source), @item_columns),
194 deliverydate => $source_item->reqdate,
195 fxsellprice => $source_item->sellprice,
196 custom_variables => \@custom_variables,
197 ordnumber => ref($item_parent) eq 'SL::DB::Order' ? $item_parent->ordnumber : $source_item->ordnumber,
198 donumber => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber : $source_item->can('donumber') ? $source_item->donumber : '',
201 $current_invoice_item->{"converted_from_orderitems_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
202 $current_invoice_item->{"converted_from_delivery_order_items_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::DeliveryOrder';
203 $current_invoice_item;
206 @items = grep { $params{item_filter}->($_) } @items if $params{item_filter};
207 @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
208 @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
210 $invoice->invoiceitems(\@items);
216 my ($self, %params) = @_;
218 require SL::DB::Chart;
219 if (!$params{ar_id}) {
220 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
223 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
224 $params{ar_id} = $chart->id;
228 my %data = $self->calculate_prices_and_taxes;
230 $self->_post_create_assemblyitem_entries($data{assembly_items});
233 $self->_post_add_acctrans($data{amounts_cogs});
234 $self->_post_add_acctrans($data{amounts});
235 $self->_post_add_acctrans($data{taxes});
237 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
239 $self->_post_update_allocated($data{allocated});
242 if ($self->db->in_transaction) {
244 } elsif (!$self->db->do_transaction($worker)) {
245 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
252 sub _post_add_acctrans {
253 my ($self, $entries) = @_;
255 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
258 require SL::DB::AccTransaction;
259 require SL::DB::Chart;
260 while (my ($chart_id, $spec) = each %{ $entries }) {
261 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
262 $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
265 SL::DB::AccTransaction->new(trans_id => $self->id,
266 chart_id => $chart_id,
267 amount => $spec->{amount},
268 tax_id => $spec->{tax_id},
269 taxkey => $spec->{taxkey},
270 project_id => $self->globalproject_id,
271 transdate => $self->transdate,
272 chart_link => $chart_link)->save;
276 sub _post_create_assemblyitem_entries {
277 my ($self, $assembly_entries) = @_;
279 my $items = $self->invoiceitems;
283 foreach my $item (@{ $items }) {
284 next if $item->assemblyitem;
286 push @new_items, $item;
289 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
290 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
291 description => $assembly_item->{part}->description,
292 unit => $assembly_item->{part}->unit,
293 qty => $assembly_item->{qty},
294 allocated => $assembly_item->{allocated},
297 assemblyitem => 't');
301 $self->invoiceitems(\@new_items);
304 sub _post_update_allocated {
305 my ($self, $allocated) = @_;
307 while (my ($invoice_id, $diff) = each %{ $allocated }) {
308 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
309 where => [ id => $invoice_id ]);
316 return 'ar_transaction' if !$self->invoice;
317 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
318 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
319 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
323 sub displayable_state {
326 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
329 sub displayable_type {
332 return t8('AR Transaction') if $self->invoice_type eq 'ar_transaction';
333 return t8('Credit Note') if $self->invoice_type eq 'credit_note';
334 return t8('Invoice') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'invoice_storno';
335 return t8('Credit Note') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'credit_note_storno';
336 return t8('Invoice');
342 return t8('AR Transaction (abbreviation)') if $self->invoice_type eq 'ar_transaction';
343 return t8('Credit note (one letter abbreviation)') if $self->invoice_type eq 'credit_note';
344 return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'invoice_storno';
345 return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'credit_note_storno';
346 return t8('Invoice (one letter abbreviation)');
365 $html = SL::Presenter->get->sales_invoice($self, display => 'inline') if $self->invoice;
366 $html = SL::Presenter->get->ar_transaction($self, display => 'inline') if !$self->invoice;
379 SL::DB::Invoice: Rose model for invoices (table "ar")
385 =item C<new_from $source, %params>
387 Creates a new C<SL::DB::Invoice> instance and copies as much
388 information from C<$source> as possible. At the moment only sales
389 orders and sales quotations are supported as sources.
391 The conversion copies order items into invoice items. Dates are copied
392 as appropriate, e.g. the C<transdate> field from an order will be
393 copied into the invoice's C<orddate> field.
395 C<%params> can include the following options:
401 An optional array reference of RDBO instances for the items to use. If
402 missing then the method C<items_sorted> will be called on
403 C<$source>. This option can be used to override the sorting, to
404 exclude certain positions or to add additional ones.
406 =item C<skip_items_negative_qty>
408 If trueish then items with a negative quantity are skipped. Items with
409 a quantity of 0 are not affected by this option.
411 =item C<skip_items_zero_qty>
413 If trueish then items with a quantity of 0 are skipped.
417 An optional code reference that is called for each item with the item
418 as its sole parameter. Items for which the code reference returns a
419 falsish value will be skipped.
423 An optional hash reference. If it exists then it is passed to C<new>
424 allowing the caller to set certain attributes for the new delivery
429 Amounts, prices and taxes are not
430 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
431 can be used for this.
433 The object returned is not saved.
435 =item C<post %params>
437 Posts the invoice. Required parameters are:
443 The ID of the accounds receivable chart the invoices amounts are
444 posted to. If it is not set then the first chart configured for
445 accounts receivables is used.
449 This function implements several steps:
453 =item 1. It calculates all prices, amounts and taxes by calling
454 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
456 =item 2. A new and unique invoice number is created.
458 =item 3. All amounts for costs of goods sold are recorded in
461 =item 4. All amounts for parts, services and assemblies are recorded
462 in C<acc_trans> with their respective charts. This is determined by
463 the part's buchungsgruppen.
465 =item 5. The total amount is posted to the accounts receivable chart
466 and recorded in C<acc_trans>.
468 =item 6. Items in C<invoice> are updated according to their allocation
469 status (regarding for costs of goold sold). Will only be done if
470 kivitendo is not configured to use Einnahmenüberschussrechnungen.
472 =item 7. The invoice and its items are saved.
476 Returns C<$self> on success and C<undef> on failure. The whole process
477 is run inside a transaction. If it fails then nothing is saved to or
478 changed in the database. A new transaction is only started if none is
481 =item C<basic_info $field>
483 See L<SL::DB::Object::basic_info>.
489 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>