From: Moritz Bunkus Date: Tue, 24 Jun 2014 09:04:54 +0000 (+0200) Subject: Neuer Bericht »Liquiditätsvorschau« X-Git-Tag: release-3.2.0beta~411^2~18 X-Git-Url: http://wagnertech.de/git?a=commitdiff_plain;h=2ea1a379cc2e8992077a8a6f6e413952d1e32bfd;p=kivitendo-erp.git Neuer Bericht »Liquiditätsvorschau« --- diff --git a/SL/Controller/LiquidityProjection.pm b/SL/Controller/LiquidityProjection.pm new file mode 100644 index 000000000..24413d781 --- /dev/null +++ b/SL/Controller/LiquidityProjection.pm @@ -0,0 +1,81 @@ +package SL::Controller::LiquidityProjection; + +use strict; + +use parent qw(SL::Controller::Base); + +use SL::Locale::String; +use SL::LiquidityProjection; +use SL::Util qw(_hashify); + +__PACKAGE__->run_before('check_auth'); + +use Rose::Object::MakeMethods::Generic ( + scalar => [ qw(liquidity) ], + 'scalar --get_set_init' => [ qw(oe_report_columns_str) ], +); + + +# +# actions +# + +sub action_show { + my ($self) = @_; + + $self->liquidity(SL::LiquidityProjection->new(%{ $::form->{params} })->create) if $::form->{params}; + + $::form->{params} ||= { + months => 6, + type => 1, + salesman => 1, + buchungsgruppe => 1, + }; + + $self->render('liquidity_projection/show', title => t8('Liquidity projection')); +} + +# +# filters +# + +sub check_auth { $::auth->assert('report') } +sub init_oe_report_columns_str { join '&', map { "$_=Y" } qw(open delivered notdelivered l_ordnumber l_transdate l_reqdate l_name l_employee l_salesman l_netamount l_amount l_transaction_description) } + +# +# helpers +# + +sub link_to_old_orders { + my $self = shift; + my %params = _hashify(0, @_); + + my $reqdate = $params{reqdate}; + my $months = $params{months} * 1; + + my $fields = ''; + + if ($reqdate eq 'old') { + $fields .= '&reqdate_unset_or_old=Y'; + + } elsif ($reqdate eq 'future') { + my @now = localtime; + $fields .= '&reqdatefrom=' . $self->iso_to_display(SL::LiquidityProjection::_the_date($now[5] + 1900, $now[4] + 1 + $months) . '-01'); + + } else { + $reqdate =~ m/(\d+)-(\d+)/; + $fields .= '&reqdatefrom=' . $self->iso_to_display($reqdate . '-01'); + $fields .= '&reqdateto=' . $self->iso_to_display($reqdate . sprintf('-%02d', DateTime->last_day_of_month(year => $1, month => $2)->day)); + + } + + return "oe.pl?action=orders&type=sales_order&vc=customer&" . $self->oe_report_columns_str . $fields; +} + +sub iso_to_display { + my ($self, $date) = @_; + + $::locale->reformat_date({ dateformat => 'yyyy-mm-dd' }, $date, $::myconfig{dateformat}); +} + +1; diff --git a/SL/LiquidityProjection.pm b/SL/LiquidityProjection.pm new file mode 100644 index 000000000..03a1894c2 --- /dev/null +++ b/SL/LiquidityProjection.pm @@ -0,0 +1,296 @@ +package SL::LiquidityProjection; + +use strict; + +use List::MoreUtils qw(uniq); + +use SL::DBUtils; + +sub new { + my $package = shift; + my $self = bless {}, $package; + + my %params = @_; + + $self->{params} = \%params; + + my @now = localtime; + my $now_year = $now[5] + 1900; + my $now_month = $now[4] + 1; + + $self->{min_date} = _the_date($now_year, $now_month); + $self->{max_date} = _the_date($now_year, $now_month + $params{months} - 1); + + $self; +} + +# Algorithmus: +# +# Für den aktuellen Monat und alle x Folgemonate soll der geplante +# Liquiditätszufluss aufgeschlüsselt werden. Der Zufluss berechnet +# sich dabei aus: +# +# 1. Summe aller offenen Auträge +# +# 2. abzüglich aller zu diesen Aufträgen erstellten Rechnungen +# (Teillieferungen/Teilrechnungen) +# +# 3. zuzüglich alle aktiven Wartungsverträge, die in dem jeweiligen +# Monat ihre Saldierungsperiode haben, außer Wartungsverträgen, die +# für den jeweiligen Monat bereits abgerechnet wurden. +# +# Diese Werte sollen zusätzlich optional nach Verkäufer(in) und nach +# Buchungsgruppe aufgeschlüsselt werden. +# +# Diese Lösung geht deshalb immer über die Positionen der Belege +# (wegen der Buchungsgruppe) und berechnet die Summen daraus manuell. +# +# Alle Aufträge, deren Lieferdatum leer ist, oder deren Lieferdatum +# vor dem aktuellen Monat liegt, werden in einer Kategorie 'alt' +# zusammengefasst. +# +# Alle Aufträge, deren Lieferdatum nach dem zu betrachtenden Zeitraum +# (aktueller Monat + x Monate) liegen, werden in einer Kategorie +# 'Zukunft' zusammengefasst. +# +# Insgesamt läuft es wie folgt ab: +# +# 1. Es wird das Datum aller periodisch erzeugten Rechnungen innerhalb +# des Betrachtungszeitraumes herausgesucht. +# +# 2. Alle aktiven Wartungsvertragskonfigurationen werden +# ausgelesen. Die Saldierungsmonate werden solange aufaddiert, wie der +# dabei herauskommende Monat nicht nach dem zu betrachtenden Zeitraum +# liegt. +# +# 3. Für jedes Saldierungsintervall, das innerhalb des +# Betrachtungszeitraumes liegt, und für das es für den Monat noch +# keine Rechnung gibt (siehe 1.), wird diese Konfiguration für den +# Monat vorgemerkt. +# +# 4. Es werden für alle offenen Kundenaufträge die Positionen +# ausgelesen und mit Verkäufer(in), Buchungsgruppe verknüpft. Aus +# Menge, Einzelpreis und Zeilenrabatt wird die Zeilensumme berechnet. +# +# 5. Mit den Informationen aus 3. und 4. werden Datenstrukturen +# initialisiert, die für die Gesamtsummen, für alle Verkäufer(innen), +# für alle Buchungsgruppen, für alle Monate Werte enthalten. +# +# 6. Es wird über alle Einträge aus 4. iteriert. Die Zeilensummen +# werden in den entsprechenden Datenstrukturen aus 5. addiert. +# +# 7. Es wird über alle Einträge aus 3. iteriert. Die Zeilensummen +# werden in den entsprechenden Datenstrukturen aus 5. addiert. +# +# 8. Es werden alle Rechnungspositionen ausgelesen, bei denen die +# Auftragsnummer einer der aus 5. ermittelten Aufträge entspricht. +# +# 9. Es wird über alle Einträge aus 8. iteriert. Die Zeilensummen +# werden von den entsprechenden Datenstrukturen aus 5. abgezogen. Als +# Datum wird dabei das Datum des zu der Rechnung gehörenden Auftrages +# genommen. Als Buchungsgruppe wird die Buchungsgruppe der Zeile +# genommen. Falls es passieren sollte, dass diese Buchungsgruppe in +# den Aufträgen nie vorgekommen ist (sprich Rechnung enthält +# Positionen, die im Auftrag nicht enthalten sind, und die komplett +# andere Buchungsgruppen verwenden), so wird schlicht die allererste +# in 4. gefundene Buchungsgruppe damit belastet. + +sub create { + my ($self) = @_; + my %params = %{ $self->{params} }; + + my $dbh = $params{dbh} || $::form->get_standard_dbh; + my ($sth, $ref, $query); + + $params{months} ||= 6; + + # 1. Auslesen aller erzeugten periodischen Rechnungen im + # Betrachtungszeitraum + my $q_min_date = $dbh->quote($self->{min_date} . '-01'); + $query = <= to_date($q_min_date, 'YYYY-MM-DD')) +SQL + + my %periodic_invoices; + $sth = prepare_execute_query($::form, $dbh, $query); + while ($ref = $sth->fetchrow_hashref) { + $periodic_invoices{ $ref->{config_id} } ||= { }; + $periodic_invoices{ $ref->{config_id} }->{ $ref->{period_start_date} } = 1; + } + $sth->finish; + + # 2. Auslesen aktiver Wartungsvertragskonfigurationen + $query = < 1, 'q' => 3, 'y' => 12 ); + my @scentries; + $sth = prepare_execute_query($::form, $dbh, $query); + while ($ref = $sth->fetchrow_hashref) { + my ($year, $month) = ($ref->{start_year}, $ref->{start_month}); + my $date; + + while (($date = _the_date($year, $month)) le $self->{max_date}) { + if (($date ge $self->{min_date}) && (!$periodic_invoices{ $ref->{config_id} } || !$periodic_invoices{ $ref->{config_id} }->{$date})) { + push @scentries, { buchungsgruppe => $ref->{buchungsgruppe}, + salesman => $ref->{salesman}, + linetotal => $ref->{linetotal}, + date => $date, + }; + } + + ($year, $month) = _fix_date($year, $month + ($periodicities{ $ref->{periodicity} } || 1)); + } + } + $sth->finish; + + # 4. Auslesen offener Aufträge + $query = <{salesman} } (@entries, @scentries); + my @buchungsgruppen = uniq map { $_->{buchungsgruppe} } (@entries, @scentries); + my @now = localtime; + my @dates = map { $self->_date_for($now[5] + 1900, $now[4] + $_) } (0..$self->{params}->{months} + 1); + my %dates_by_ordnumber = map { $_->{ordnumber} => $self->_date_for($_) } @entries; + my %salesman_by_ordnumber = map { $_->{ordnumber} => $_->{salesman} } @entries; + my %date_sorter = ( old => '0000-00', future => '9999-99' ); + + my $projection = { total => { map { $_ => 0 } @dates }, + order => { map { $_ => 0 } @dates }, + partial => { map { $_ => 0 } @dates }, + support => { map { $_ => 0 } @dates }, + salesman => { map { $_ => { map { $_ => 0 } @dates } } @salesmen }, + buchungsgruppe => { map { $_ => { map { $_ => 0 } @dates } } @buchungsgruppen }, + sorted => { month => [ sort { ($date_sorter{$a} || $a) cmp ($date_sorter{$b} || $b) } @dates ], + salesman => [ sort { $a cmp $b } @salesmen ], + buchungsgruppe => [ sort { $a cmp $b } @buchungsgruppen ], + type => [ qw(order partial support) ], + }, + }; + + # 6. Aufsummieren der Auftragspositionen + foreach $ref (@entries) { + my $date = $self->_date_for($ref); + + $projection->{total}->{$date} += $ref->{linetotal}; + $projection->{order}->{$date} += $ref->{linetotal}; + $projection->{salesman}->{ $ref->{salesman} }->{$date} += $ref->{linetotal}; + $projection->{buchungsgruppe}->{ $ref->{buchungsgruppe} }->{$date} += $ref->{linetotal}; + } + + # 7. Aufsummieren der Wartungsvertragspositionen + foreach $ref (@scentries) { + my $date = $ref->{date}; + + $projection->{total}->{$date} += $ref->{linetotal}; + $projection->{support}->{$date} += $ref->{linetotal}; + $projection->{salesman}->{ $ref->{salesman} }->{$date} += $ref->{linetotal}; + $projection->{buchungsgruppe}->{ $ref->{buchungsgruppe} }->{$date} += $ref->{linetotal}; + } + + if (%dates_by_ordnumber) { + # 8. Auslesen von Positionen von Teilrechnungen zu Aufträgen + my $ordnumbers = join ', ', map { $dbh->quote($_) } keys %dates_by_ordnumber; + $query = <{ordnumber} } || die; + my $salesman = $salesman_by_ordnumber{ $ref->{ordnumber} } || die; + my $buchungsgruppe = $projection->{buchungsgruppe}->{ $ref->{buchungsgruppe} } ? $ref->{buchungsgruppe} : $buchungsgruppen[0]; + + $projection->{partial}->{$date} -= $ref->{linetotal}; + $projection->{total}->{$date} -= $ref->{linetotal}; + $projection->{salesman}->{$salesman}->{$date} -= $ref->{linetotal}; + $projection->{buchungsgruppe}->{$buchungsgruppe}->{$date} -= $ref->{linetotal}; + } + } + + return $projection; +} + +# Skaliert '$year' und '$month' so, dass 1 <= Monat <= 12 gilt. Zum +# Einfachen Addieren gedacht, z.B. +# +# my ($new_year, $new_month) = _fix_date($old_year, $old_month + 6); + +sub _fix_date { + my $year = shift; + my $month = shift; + + $year += int(($month - 1) / 12); + $month = (($month - 1) % 12 ) + 1; + + ($year, $month); +} + +# Formartiert Jahr & Monat wie benötigt. + +sub _the_date { + sprintf '%04d-%02d', _fix_date(@_); +} + +# Mappt Datum auf Kategorie. Ist das Datum leer, oder liegt es vor dem +# Betrachtungszeitraum, so ist die Kategorie 'old'. Liegt das Datum +# nach dem Betrachtungszeitraum, so ist die Kategorie +# 'future'. Andernfalls ist sie das formartierte Datum selber. + +sub _date_for { + my $self = shift; + my $ref = ref $_[0] eq 'HASH' ? shift : { year => $_[0], month => $_[1] }; + + return 'old' if !$ref->{year} || !$ref->{month}; + + my $date = _the_date($ref->{year}, $ref->{month}); + + $date lt $self->{min_date} ? 'old' + : $date gt $self->{max_date} ? 'future' + : $date; +} + +1; diff --git a/SL/OE.pm b/SL/OE.pm index 01e930cbf..b6f653aed 100644 --- a/SL/OE.pm +++ b/SL/OE.pm @@ -244,6 +244,10 @@ SQL $query .= qq| AND ${not} COALESCE(pcfg.active, 'f')|; } + if ($form->{reqdate_unset_or_old}) { + $query .= qq| AND ((o.reqdate IS NULL) OR (o.reqdate < date_trunc('month', current_date)))|; + } + if (($form->{order_probability_value} || '') ne '') { my $op = $form->{order_probability_value} eq 'le' ? '<=' : '>='; $query .= qq| AND (o.order_probability ${op} ?)|; diff --git a/bin/mozilla/oe.pl b/bin/mozilla/oe.pl index a7207ff81..6d3fb1939 100644 --- a/bin/mozilla/oe.pl +++ b/bin/mozilla/oe.pl @@ -883,7 +883,7 @@ sub orders { push @hidden_variables, "l_subtotal", $form->{vc}, qw(l_closed l_notdelivered open closed delivered notdelivered ordnumber quonumber cusordnumber transaction_description transdatefrom transdateto type vc employee_id salesman_id reqdatefrom reqdateto projectnumber project_id periodic_invoices_active periodic_invoices_inactive - business_id shippingpoint taxzone_id + business_id shippingpoint taxzone_id reqdate_unset_or_old order_probability_op order_probability_value expected_billing_date_from expected_billing_date_to); my @keys_for_url = grep { $form->{$_} } @hidden_variables; @@ -969,6 +969,7 @@ sub orders { push @options, $locale->text('Delivery Order created') if $form->{delivered}; push @options, $locale->text('Not delivered') if $form->{notdelivered}; push @options, $locale->text('Periodic invoices active') if $form->{periodic_invoices_active}; + push @options, $locale->text('Reqdate not set or before current month') if $form->{reqdate_unset_or_old}; if ($form->{business_id}) { my $vc_type_label = $form->{vc} eq 'customer' ? $locale->text('Customer type') : $locale->text('Vendor type'); diff --git a/locale/de/all b/locale/de/all index 3f9ec36a2..191f9f611 100755 --- a/locale/de/all +++ b/locale/de/all @@ -325,6 +325,7 @@ $self->{texts} = { 'Basic Settings for the Requirement Spec Template' => 'Grundeinstellungen der Pflichtenheftvorlage', 'Basic settings' => 'Grundeinstellungen', 'Basic settings actions' => 'Aktionen zu Grundeinstellungen', + 'Basis of calculation' => 'Berechnungsgrundlage', 'Batch Printing' => 'Druck', 'Bcc' => 'Bcc', 'Bcc E-mail' => 'BCC (E-Mail)', @@ -365,6 +366,7 @@ $self->{texts} = { 'Both' => 'Beide', 'Bottom' => 'Unten', 'Bought' => 'Gekauft', + 'Break down by' => 'Aufschlüsseln nach', 'Break up the update and contact a service provider.' => 'Diese Option bricht das Update ab. Bitte kontaktieren Sie Ihren Administrator oder beauftragen einen Dienstleister.', 'Buchungsdatum' => 'Buchungsdatum', 'Buchungsgruppe' => 'Buchungsgruppe', @@ -1378,6 +1380,7 @@ $self->{texts} = { 'Link to' => 'Verknüpfen mit', 'Link to the following project:' => 'Mit dem folgenden Projekt verknüpfen:', 'Linked Records' => 'Verknüpfte Belege', + 'Liquidity projection' => 'Liquiditätsübersicht', 'List Accounts' => 'Konten anzeigen', 'List Languages' => 'Sprachen anzeigen', 'List Price' => 'Listenpreis', @@ -1594,6 +1597,7 @@ $self->{texts} = { 'Number of bins' => 'Anzahl Lagerplätze', 'Number of copies' => 'Anzahl Kopien', 'Number of entries changed: #1' => 'Anzahl geänderter Einträge: #1', + 'Number of months' => 'Anzahl Monate', 'Number of new bins' => 'Anzahl neuer Lagerplätze', 'Number pages' => 'Seiten nummerieren', 'Number variables: \'PRECISION=n\' forces numbers to be shown with exactly n decimal places.' => 'Zahlenvariablen: Mit \'PRECISION=n\' erzwingt man, dass Zahlen mit n Nachkommastellen formatiert werden.', @@ -1680,6 +1684,7 @@ $self->{texts} = { 'Part Number' => 'Artikelnummer', 'Part Number missing!' => 'Artikelnummer fehlt!', 'Part picker' => 'Artikelauswahl', + 'Partial invoices' => 'Teilrechnungen', 'Partnumber' => 'Artikelnummer', 'Partnumber must not be set to empty!' => 'Die Artikelnummer darf nicht auf leer geändert werden.', 'Partnumber not unique!' => 'Artikelnummer bereits vorhanden!', @@ -1938,6 +1943,7 @@ $self->{texts} = { 'Representative' => 'Vertreter', 'Representative for Customer' => 'Vertreter für Kunden', 'Reqdate' => 'Liefertermin', + 'Reqdate not set or before current month' => 'Lieferdatum nicht gesetzt oder vor aktuellem Monat', 'Request Quotations' => 'Preisanfragen', 'Request for Quotation' => 'Anfrage', 'Request for Quotation Number' => 'Anfragenummer', @@ -3001,6 +3007,7 @@ $self->{texts} = { 'not yet executed' => 'Noch nicht ausgeführt', 'number' => 'Nummer', 'oe.pl::search called with unknown type' => 'oe.pl::search mit unbekanntem Typ aufgerufen', + 'old' => 'alt', 'on the same day' => 'am selben Tag', 'one-time execution' => 'einmalige Ausführung', 'only OB Transactions' => 'nur EB-Buchungen', @@ -3023,6 +3030,7 @@ $self->{texts} = { 'prev' => 'zurück', 'print' => 'drucken', 'proforma' => 'Proforma', + 'prospective' => 'zukünftig', 'purchase_delivery_order_list' => 'lieferscheinliste_einkauf', 'purchase_order' => 'Auftrag', 'purchase_order_list' => 'lieferantenauftragsliste', diff --git a/menus/erp.ini b/menus/erp.ini index fde2dd8c4..637ac6431 100644 --- a/menus/erp.ini +++ b/menus/erp.ini @@ -450,6 +450,10 @@ ACCESS=report module=controller.pl action=FinancialOverview/list +[Reports--Liquidity projection] +ACCESS=report +module=controller.pl +action=LiquidityProjection/show [Batch Printing] ACCESS=batch_printing diff --git a/templates/webpages/liquidity_projection/_filter.html b/templates/webpages/liquidity_projection/_filter.html new file mode 100644 index 000000000..34bf90a58 --- /dev/null +++ b/templates/webpages/liquidity_projection/_filter.html @@ -0,0 +1,27 @@ +[%- USE LxERP -%][%- USE L -%] + +
+ [% L.hidden_tag('action', 'LiquidityProjection/show') %] + + + + + + + + + + + +
[% LxERP.t8("Number of months") %][% L.input_tag("params.months", FORM.params.months, class="initial_focus") %]
[% LxERP.t8("Break down by") %] + [% L.checkbox_tag("params.type", value=1, checked=FORM.params.type, label=LxERP.t8("Basis of calculation")) %] +
+ [% L.checkbox_tag("params.salesman", value=1, checked=FORM.params.salesman, label=LxERP.t8("Salesman")) %] +
+ [% L.checkbox_tag("params.buchungsgruppe", value=1, checked=FORM.params.buchungsgruppe, label=LxERP.t8("Buchungsgruppe")) %] +
+ +

+ [% L.submit_tag("dummy", LxERP.t8("Show")) %] +

+
diff --git a/templates/webpages/liquidity_projection/_result.html b/templates/webpages/liquidity_projection/_result.html new file mode 100644 index 000000000..bcc6e20b6 --- /dev/null +++ b/templates/webpages/liquidity_projection/_result.html @@ -0,0 +1,74 @@ +[%- USE HTML -%][%- USE LxERP -%] +[%- SET name_col = FORM.params.salesman || FORM.params.buchungsgruppe || FORM.params.type %] + + + + + [%- IF name_col %] + + [%- END %] + [%- FOREACH month = SELF.liquidity.sorted.month %] + + [%- END %] + + + [% IF FORM.params.type %] + [% FOREACH type = SELF.liquidity.sorted.type %] + + + + + [%- FOREACH month = SELF.liquidity.sorted.month %] + + [%- END %] + + [%- END %] + [%- END %] + + [%- IF FORM.params.salesman %] + [%- FOREACH salesman = SELF.liquidity.sorted.salesman %] + + + + + [%- FOREACH month = SELF.liquidity.sorted.month %] + + [%- END %] + + [%- END %] + [%- END %] + + [%- IF FORM.params.buchungsgruppe %] + [%- FOREACH buchungsgruppe = SELF.liquidity.sorted.buchungsgruppe %] + + + + + [%- FOREACH month = SELF.liquidity.sorted.month %] + + [%- END %] + + [%- END %] + [%- END %] + + + + [% IF name_col %][% END %] + [%- FOREACH month = SELF.liquidity.sorted.month %] + + [%- END %] + +
[% LxERP.t8("Type") %][% LxERP.t8("Name") %][%- IF month == 'old' %][% LxERP.t8("old") %][% ELSIF month == 'future' %][% LxERP.t8("prospective") %][% ELSE %][%- HTML.escape(month) %][% END %]
[% IF loop.first %][% LxERP.t8("Basis of calculation") %][% END %] + [% IF type == 'order' %][% LxERP.t8("Sales Orders") %] + [% ELSIF type == 'partial' %][% LxERP.t8("Partial invoices") %] + [% ELSE %][% LxERP.t8("Periodic Invoices") %] + [% END %] + [% LxERP.format_amount(SELF.liquidity.$type.$month, 2) %]
[% IF loop.first %][% LxERP.t8("Salesman") %][% END %][%- HTML.escape(salesman) %][% LxERP.format_amount(SELF.liquidity.salesman.$salesman.$month, 2) %]
[% IF loop.first %][% LxERP.t8("Buchungsgruppe") %][% END %][%- HTML.escape(buchungsgruppe) %][% LxERP.format_amount(SELF.liquidity.buchungsgruppe.$buchungsgruppe.$month, 2) %]
[% LxERP.t8("Total") %] + [% IF SELF.liquidity.total.$month > 0 %] + + [% END %] + [% LxERP.format_amount(SELF.liquidity.total.$month, 2) %] + [% IF SELF.liquidity.total.$month > 0 %] + + [% END %] +
diff --git a/templates/webpages/liquidity_projection/show.html b/templates/webpages/liquidity_projection/show.html new file mode 100644 index 000000000..ce6bf82cd --- /dev/null +++ b/templates/webpages/liquidity_projection/show.html @@ -0,0 +1,15 @@ +[% USE HTML %][% USE LxERP %][%- USE L -%] + + +

[% HTML.escape(title) %]

+ + [% PROCESS 'liquidity_projection/_filter.html' %] + + [%- IF SELF.liquidity %] +
+ + [% PROCESS 'liquidity_projection/_result.html' %] + [% END %] + + +