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 SL::DB::MetaSetup::Invoice;
12 use SL::DB::Manager::Invoice;
13 use SL::DB::Helper::FlattenToForm;
14 use SL::DB::Helper::LinkedRecords;
15 use SL::DB::Helper::PriceTaxCalculator;
16 use SL::DB::Helper::PriceUpdater;
17 use SL::DB::Helper::TransNumberGenerator;
18 use SL::DB::AccTransaction;
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' ]
33 class => 'SL::DB::PaymentTerm',
34 column_map => { payment_id => 'id' },
38 class => 'SL::DB::Contact',
39 column_map => { cp_id => 'cp_id' },
43 class => 'SL::DB::Shipto',
44 column_map => { shipto_id => 'shipto_id' },
48 class => 'SL::DB::Department',
49 column_map => { department_id => 'id' },
53 class => 'SL::DB::Language',
54 column_map => { language_id => 'id' },
58 class => 'SL::DB::Employee',
59 column_map => { employee_id => 'id' },
63 __PACKAGE__->meta->initialize;
67 sub items { goto &invoiceitems; }
69 # it is assumed, that ordnumbers are unique here.
70 sub first_order_by_ordnumber {
73 my $orders = SL::DB::Manager::Order->get_all(
75 ordnumber => $self->ordnumber,
80 return first { $_->is_type('sales_order') } @{ $orders };
83 sub abschlag_percentage {
85 my $order = $self->first_order_by_ordnumber or return;
86 my $order_amount = $order->netamount or return;
87 return $self->abschlag
88 ? $self->netamount / $order_amount
94 die 'not a setter method' if @_;
96 return ($self->amount || 0) - ($self->netamount || 0);
99 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
103 return $self->paid >= $self->amount;
107 my ($class, $source, %params) = @_;
109 croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
110 croak("Cannot create invoices for purchase records") unless $source->customer_id;
112 my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_term->terms_netto : 0;
114 my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes curr salesman_id cusordnumber ordnumber quonumber
115 department_id cp_id language_id payment_id delivery_customer_id delivery_vendor_id taxzone_id shipto_id
116 globalproject_id transaction_description)),
117 transdate => DateTime->today_local,
118 gldate => DateTime->today_local,
119 duedate => DateTime->today_local->add(days => $terms * 1),
124 employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
127 if ($source->type =~ /_order$/) {
128 $args{deliverydate} = $source->reqdate;
129 $args{orddate} = $source->transdate;
131 $args{quodate} = $source->transdate;
134 my $invoice = $class->new(%args, %params);
137 my $source_item = $_;
138 SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
139 qw(parts_id description qty sellprice discount project_id
140 serialnumber pricegroup_id ordnumber transdate cusordnumber unit
141 base_qty subtotal longdescription lastcost price_factor_id)),
142 deliverydate => $source_item->reqdate,
143 fxsellprice => $source_item->sellprice,);
144 } @{ $source->items };
146 $invoice->invoiceitems(\@items);
152 my ($self, %params) = @_;
154 if (!$params{ar_id}) {
155 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
158 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
159 $params{ar_id} = $chart->id;
163 my %data = $self->calculate_prices_and_taxes;
165 $self->_post_create_assemblyitem_entries($data{assembly_items});
166 $self->create_trans_number;
169 $self->_post_add_acctrans($data{amounts_cogs});
170 $self->_post_add_acctrans($data{amounts});
171 $self->_post_add_acctrans($data{taxes});
173 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
175 $self->_post_update_allocated($data{allocated});
178 if ($self->db->in_transaction) {
180 } elsif (!$self->db->do_transaction($worker)) {
181 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
188 sub _post_add_acctrans {
189 my ($self, $entries) = @_;
191 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
193 while (my ($chart_id, $spec) = each %{ $entries }) {
194 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
195 SL::DB::AccTransaction->new(trans_id => $self->id,
196 chart_id => $chart_id,
197 amount => $spec->{amount},
198 tax_id => $spec->{tax_id},
199 taxkey => $spec->{taxkey},
200 project_id => $self->globalproject_id,
201 transdate => $self->transdate)->save;
205 sub _post_create_assemblyitem_entries {
206 my ($self, $assembly_entries) = @_;
208 my $items = $self->invoiceitems;
212 foreach my $item (@{ $items }) {
213 next if $item->assemblyitem;
215 push @new_items, $item;
218 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
219 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
220 description => $assembly_item->{part}->description,
221 unit => $assembly_item->{part}->unit,
222 qty => $assembly_item->{qty},
223 allocated => $assembly_item->{allocated},
226 assemblyitem => 't');
230 $self->invoiceitems(\@new_items);
233 sub _post_update_allocated {
234 my ($self, $allocated) = @_;
236 while (my ($invoice_id, $diff) = each %{ $allocated }) {
237 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
238 where => [ id => $invoice_id ]);
245 return 'ar_transaction' if !$self->invoice;
246 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
247 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
248 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
252 sub displayable_state {
255 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
266 SL::DB::Invoice: Rose model for invoices (table "ar")
272 =item C<new_from $source>
274 Creates a new C<SL::DB::Invoice> instance and copies as much
275 information from C<$source> as possible. At the moment only sales
276 orders and sales quotations are supported as sources.
278 The conversion copies order items into invoice items. Dates are copied
279 as appropriate, e.g. the C<transdate> field from an order will be
280 copied into the invoice's C<orddate> field.
282 Amounts, prices and taxes are not
283 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
284 can be used for this.
286 The object returned is not saved.
288 =item C<post %params>
290 Posts the invoice. Required parameters are:
296 The ID of the accounds receivable chart the invoices amounts are
297 posted to. If it is not set then the first chart configured for
298 accounts receivables is used.
302 This function implements several steps:
306 =item 1. It calculates all prices, amounts and taxes by calling
307 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
309 =item 2. A new and unique invoice number is created.
311 =item 3. All amounts for costs of goods sold are recorded in
314 =item 4. All amounts for parts, services and assemblies are recorded
315 in C<acc_trans> with their respective charts. This is determined by
316 the part's buchungsgruppen.
318 =item 5. The total amount is posted to the accounts receivable chart
319 and recorded in C<acc_trans>.
321 =item 6. Items in C<invoice> are updated according to their allocation
322 status (regarding for costs of goold sold). Will only be done if
323 kivitendo is not configured to use Einnahmenüberschussrechnungen.
325 =item 7. The invoice and its items are saved.
329 Returns C<$self> on success and C<undef> on failure. The whole process
330 is run inside a transaction. If it fails then nothing is saved to or
331 changed in the database. A new transaction is only started if none is
334 =item C<basic_info $field>
336 See L<SL::DB::Object::basic_info>.
342 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>