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' ]
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 __PACKAGE__->meta->initialize;
48 sub items { goto &invoiceitems; }
51 # For compatibility with Order, DeliveryOrder
52 croak 'not an accessor' if @_ > 1;
56 # it is assumed, that ordnumbers are unique here.
57 sub first_order_by_ordnumber {
60 my $orders = SL::DB::Manager::Order->get_all(
62 ordnumber => $self->ordnumber,
67 return first { $_->is_type('sales_order') } @{ $orders };
70 sub abschlag_percentage {
72 my $order = $self->first_order_by_ordnumber or return;
73 my $order_amount = $order->netamount or return;
74 return $self->abschlag
75 ? $self->netamount / $order_amount
81 die 'not a setter method' if @_;
83 return ($self->amount || 0) - ($self->netamount || 0);
86 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
90 return $self->paid >= $self->amount;
94 my ($class, $source, %params) = @_;
96 croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
97 croak("Cannot create invoices for purchase records") unless $source->customer_id;
99 my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms->terms_netto : 0;
101 my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes curr salesman_id cusordnumber ordnumber quonumber
102 department_id cp_id language_id payment_id delivery_customer_id delivery_vendor_id taxzone_id shipto_id
103 globalproject_id transaction_description)),
104 transdate => DateTime->today_local,
105 gldate => DateTime->today_local,
106 duedate => DateTime->today_local->add(days => $terms * 1),
111 employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
114 if ($source->type =~ /_order$/) {
115 $args{deliverydate} = $source->reqdate;
116 $args{orddate} = $source->transdate;
118 $args{quodate} = $source->transdate;
121 my $invoice = $class->new(%args, %params);
124 my $source_item = $_;
125 SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
126 qw(parts_id description qty sellprice discount project_id
127 serialnumber pricegroup_id ordnumber transdate cusordnumber unit
128 base_qty subtotal longdescription lastcost price_factor_id)),
129 deliverydate => $source_item->reqdate,
130 fxsellprice => $source_item->sellprice,);
131 } @{ $source->items };
133 $invoice->invoiceitems(\@items);
139 my ($self, %params) = @_;
141 if (!$params{ar_id}) {
142 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
145 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
146 $params{ar_id} = $chart->id;
150 my %data = $self->calculate_prices_and_taxes;
152 $self->_post_create_assemblyitem_entries($data{assembly_items});
153 $self->create_trans_number;
156 $self->_post_add_acctrans($data{amounts_cogs});
157 $self->_post_add_acctrans($data{amounts});
158 $self->_post_add_acctrans($data{taxes});
160 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
162 $self->_post_update_allocated($data{allocated});
165 if ($self->db->in_transaction) {
167 } elsif (!$self->db->do_transaction($worker)) {
168 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
175 sub _post_add_acctrans {
176 my ($self, $entries) = @_;
178 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
181 while (my ($chart_id, $spec) = each %{ $entries }) {
182 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
183 $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
186 SL::DB::AccTransaction->new(trans_id => $self->id,
187 chart_id => $chart_id,
188 amount => $spec->{amount},
189 tax_id => $spec->{tax_id},
190 taxkey => $spec->{taxkey},
191 project_id => $self->globalproject_id,
192 transdate => $self->transdate,
193 chart_link => $chart_link)->save;
197 sub _post_create_assemblyitem_entries {
198 my ($self, $assembly_entries) = @_;
200 my $items = $self->invoiceitems;
204 foreach my $item (@{ $items }) {
205 next if $item->assemblyitem;
207 push @new_items, $item;
210 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
211 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
212 description => $assembly_item->{part}->description,
213 unit => $assembly_item->{part}->unit,
214 qty => $assembly_item->{qty},
215 allocated => $assembly_item->{allocated},
218 assemblyitem => 't');
222 $self->invoiceitems(\@new_items);
225 sub _post_update_allocated {
226 my ($self, $allocated) = @_;
228 while (my ($invoice_id, $diff) = each %{ $allocated }) {
229 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
230 where => [ id => $invoice_id ]);
237 return 'ar_transaction' if !$self->invoice;
238 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
239 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
240 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
244 sub displayable_state {
247 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
262 SL::DB::Invoice: Rose model for invoices (table "ar")
268 =item C<new_from $source>
270 Creates a new C<SL::DB::Invoice> instance and copies as much
271 information from C<$source> as possible. At the moment only sales
272 orders and sales quotations are supported as sources.
274 The conversion copies order items into invoice items. Dates are copied
275 as appropriate, e.g. the C<transdate> field from an order will be
276 copied into the invoice's C<orddate> field.
278 Amounts, prices and taxes are not
279 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
280 can be used for this.
282 The object returned is not saved.
284 =item C<post %params>
286 Posts the invoice. Required parameters are:
292 The ID of the accounds receivable chart the invoices amounts are
293 posted to. If it is not set then the first chart configured for
294 accounts receivables is used.
298 This function implements several steps:
302 =item 1. It calculates all prices, amounts and taxes by calling
303 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
305 =item 2. A new and unique invoice number is created.
307 =item 3. All amounts for costs of goods sold are recorded in
310 =item 4. All amounts for parts, services and assemblies are recorded
311 in C<acc_trans> with their respective charts. This is determined by
312 the part's buchungsgruppen.
314 =item 5. The total amount is posted to the accounts receivable chart
315 and recorded in C<acc_trans>.
317 =item 6. Items in C<invoice> are updated according to their allocation
318 status (regarding for costs of goold sold). Will only be done if
319 kivitendo is not configured to use Einnahmenüberschussrechnungen.
321 =item 7. The invoice and its items are saved.
325 Returns C<$self> on success and C<undef> on failure. The whole process
326 is run inside a transaction. If it fails then nothing is saved to or
327 changed in the database. A new transaction is only started if none is
330 =item C<basic_info $field>
332 See L<SL::DB::Object::basic_info>.
338 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>