1 package SL::DB::Invoice;
6 use List::Util qw(first);
8 use Rose::DB::Object::Helpers ();
10 use SL::DB::MetaSetup::Invoice;
11 use SL::DB::Manager::Invoice;
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);
21 __PACKAGE__->meta->add_relationship(
23 type => 'one to many',
24 class => 'SL::DB::InvoiceItem',
25 column_map => { id => 'trans_id' },
27 with_objects => [ 'part' ]
31 type => 'one to many',
32 class => 'SL::DB::Invoice',
33 column_map => { id => 'storno_id' },
35 sepa_export_items => {
36 type => 'one to many',
37 class => 'SL::DB::SepaExportItem',
38 column_map => { id => 'ar_id' },
39 manager_args => { with_objects => [ 'sepa_export' ] }
43 class => 'SL::DB::Shipto',
44 column_map => { id => 'trans_id' },
45 query_args => [ module => 'AR' ],
48 type => 'one to many',
49 class => 'SL::DB::AccTransaction',
50 column_map => { id => 'trans_id' },
52 with_objects => [ 'chart' ],
53 sort_by => 'acc_trans_id ASC',
58 __PACKAGE__->meta->initialize;
60 __PACKAGE__->attr_html('notes');
61 __PACKAGE__->attr_sorted('items');
63 __PACKAGE__->before_save('_before_save_set_invnumber');
67 sub _before_save_set_invnumber {
70 $self->create_trans_number if !$self->invnumber;
77 sub items { goto &invoiceitems; }
78 sub add_items { goto &add_invoiceitems; }
81 # For compatibility with Order, DeliveryOrder
82 croak 'not an accessor' if @_ > 1;
86 # it is assumed, that ordnumbers are unique here.
87 sub first_order_by_ordnumber {
90 my $orders = SL::DB::Manager::Order->get_all(
92 ordnumber => $self->ordnumber,
97 return first { $_->is_type('sales_order') } @{ $orders };
100 sub abschlag_percentage {
102 my $order = $self->first_order_by_ordnumber or return;
103 my $order_amount = $order->netamount or return;
104 return $self->abschlag
105 ? $self->netamount / $order_amount
111 die 'not a setter method' if @_;
113 return ($self->amount || 0) - ($self->netamount || 0);
116 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
120 return $self->paid >= $self->amount;
123 sub _clone_orderitem_delivery_order_item_cvar {
126 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
127 $cloned->sub_module('invoice');
133 my ($class, $source, %params) = @_;
135 croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
136 croak("Cannot create invoices for purchase records") unless $source->customer_id;
138 require SL::DB::Employee;
140 my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms
141 : $source->customer_id ? $source ->customer->payment_terms
144 my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
146 if (ref($source) eq 'SL::DB::Order') {
147 @columns = qw(quonumber payment_id delivery_customer_id delivery_vendor_id);
148 @item_columns = qw(subtotal);
150 $item_parent_id_column = 'trans_id';
151 $item_parent_column = 'order';
154 @columns = qw(donumber);
156 $item_parent_id_column = 'delivery_order_id';
157 $item_parent_column = 'delivery_order';
160 my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
161 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
162 transdate => DateTime->today_local,
163 gldate => DateTime->today_local,
164 duedate => DateTime->today_local->add(days => ($terms ? $terms->terms_netto * 1 : 1)),
165 payment_id => $terms ? $terms->id : undef,
170 employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
173 if ($source->type =~ /_order$/) {
174 $args{deliverydate} = $source->reqdate;
175 $args{orddate} = $source->transdate;
177 $args{quodate} = $source->transdate;
180 my $invoice = $class->new(%args);
181 $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
182 my $items = delete($params{items}) || $source->items_sorted;
186 my $source_item = $_;
187 my $source_item_id = $_->$item_parent_id_column;
188 my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
190 $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
191 my $item_parent = $item_parents{$source_item_id};
192 my $current_invoice_item =
193 SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
194 qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
195 base_qty longdescription lastcost price_factor_id active_discount_source active_price_source), @item_columns),
196 deliverydate => $source_item->reqdate,
197 fxsellprice => $source_item->sellprice,
198 custom_variables => \@custom_variables,
199 ordnumber => ref($item_parent) eq 'SL::DB::Order' ? $item_parent->ordnumber : $source_item->ordnumber,
200 donumber => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber : $source_item->can('donumber') ? $source_item->donumber : '',
203 $current_invoice_item->{"converted_from_orderitems_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
204 $current_invoice_item->{"converted_from_delivery_order_items_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::DeliveryOrder';
205 $current_invoice_item;
208 @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
209 @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
211 $invoice->invoiceitems(\@items);
217 my ($self, %params) = @_;
219 require SL::DB::Chart;
220 if (!$params{ar_id}) {
221 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
224 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
225 $params{ar_id} = $chart->id;
229 my %data = $self->calculate_prices_and_taxes;
231 $self->_post_create_assemblyitem_entries($data{assembly_items});
234 $self->_post_add_acctrans($data{amounts_cogs});
235 $self->_post_add_acctrans($data{amounts});
236 $self->_post_add_acctrans($data{taxes});
238 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
240 $self->_post_update_allocated($data{allocated});
243 if ($self->db->in_transaction) {
245 } elsif (!$self->db->do_transaction($worker)) {
246 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
253 sub _post_add_acctrans {
254 my ($self, $entries) = @_;
256 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
259 require SL::DB::AccTransaction;
260 require SL::DB::Chart;
261 while (my ($chart_id, $spec) = each %{ $entries }) {
262 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
263 $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
266 SL::DB::AccTransaction->new(trans_id => $self->id,
267 chart_id => $chart_id,
268 amount => $spec->{amount},
269 tax_id => $spec->{tax_id},
270 taxkey => $spec->{taxkey},
271 project_id => $self->globalproject_id,
272 transdate => $self->transdate,
273 chart_link => $chart_link)->save;
277 sub _post_create_assemblyitem_entries {
278 my ($self, $assembly_entries) = @_;
280 my $items = $self->invoiceitems;
284 foreach my $item (@{ $items }) {
285 next if $item->assemblyitem;
287 push @new_items, $item;
290 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
291 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
292 description => $assembly_item->{part}->description,
293 unit => $assembly_item->{part}->unit,
294 qty => $assembly_item->{qty},
295 allocated => $assembly_item->{allocated},
298 assemblyitem => 't');
302 $self->invoiceitems(\@new_items);
305 sub _post_update_allocated {
306 my ($self, $allocated) = @_;
308 while (my ($invoice_id, $diff) = each %{ $allocated }) {
309 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
310 where => [ id => $invoice_id ]);
317 return 'ar_transaction' if !$self->invoice;
318 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
319 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
320 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
324 sub displayable_state {
327 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
330 sub displayable_type {
333 return t8('AR Transaction') if $self->invoice_type eq 'ar_transaction';
334 return t8('Credit Note') if $self->invoice_type eq 'credit_note';
335 return t8('Invoice') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'invoice_storno';
336 return t8('Credit Note') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'credit_note_storno';
337 return t8('Invoice');
343 return t8('AR Transaction (abbreviation)') if $self->invoice_type eq 'ar_transaction';
344 return t8('Credit note (one letter abbreviation)') if $self->invoice_type eq 'credit_note';
345 return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'invoice_storno';
346 return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'credit_note_storno';
347 return t8('Invoice (one letter abbreviation)');
370 SL::DB::Invoice: Rose model for invoices (table "ar")
376 =item C<new_from $source, %params>
378 Creates a new C<SL::DB::Invoice> instance and copies as much
379 information from C<$source> as possible. At the moment only sales
380 orders and sales quotations are supported as sources.
382 The conversion copies order items into invoice items. Dates are copied
383 as appropriate, e.g. the C<transdate> field from an order will be
384 copied into the invoice's C<orddate> field.
386 C<%params> can include the following options:
392 An optional array reference of RDBO instances for the items to use. If
393 missing then the method C<items_sorted> will be called on
394 C<$source>. This option can be used to override the sorting, to
395 exclude certain positions or to add additional ones.
397 =item C<skip_items_negative_qty>
399 If trueish then items with a negative quantity are skipped. Items with
400 a quantity of 0 are not affected by this option.
402 =item C<skip_items_zero_qty>
404 If trueish then items with a quantity of 0 are skipped.
408 An optional hash reference. If it exists then it is passed to C<new>
409 allowing the caller to set certain attributes for the new delivery
414 Amounts, prices and taxes are not
415 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
416 can be used for this.
418 The object returned is not saved.
420 =item C<post %params>
422 Posts the invoice. Required parameters are:
428 The ID of the accounds receivable chart the invoices amounts are
429 posted to. If it is not set then the first chart configured for
430 accounts receivables is used.
434 This function implements several steps:
438 =item 1. It calculates all prices, amounts and taxes by calling
439 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
441 =item 2. A new and unique invoice number is created.
443 =item 3. All amounts for costs of goods sold are recorded in
446 =item 4. All amounts for parts, services and assemblies are recorded
447 in C<acc_trans> with their respective charts. This is determined by
448 the part's buchungsgruppen.
450 =item 5. The total amount is posted to the accounts receivable chart
451 and recorded in C<acc_trans>.
453 =item 6. Items in C<invoice> are updated according to their allocation
454 status (regarding for costs of goold sold). Will only be done if
455 kivitendo is not configured to use Einnahmenüberschussrechnungen.
457 =item 7. The invoice and its items are saved.
461 Returns C<$self> on success and C<undef> on failure. The whole process
462 is run inside a transaction. If it fails then nothing is saved to or
463 changed in the database. A new transaction is only started if none is
466 =item C<basic_info $field>
468 See L<SL::DB::Object::basic_info>.
474 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>