Pflichtenhefte: Zeit- und Kostenschätzungsmaske
authorMoritz Bunkus <m.bunkus@linet-services.de>
Tue, 16 Apr 2013 13:49:59 +0000 (15:49 +0200)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Tue, 1 Apr 2014 11:02:29 +0000 (13:02 +0200)
14 files changed:
SL/Controller/Helper/RequirementSpec.pm [new file with mode: 0644]
SL/Controller/RequirementSpec.pm
SL/Controller/RequirementSpecItem.pm
SL/Controller/Test.pm [new file with mode: 0644]
css/requirement_spec.css
js/locale/de.js
js/requirement_spec.js
locale/de/all
templates/webpages/requirement_spec/_edit_time_and_cost_estimate.html [new file with mode: 0644]
templates/webpages/requirement_spec/_edit_time_and_cost_estimate_item.html [new file with mode: 0644]
templates/webpages/requirement_spec/_form.html
templates/webpages/requirement_spec/_show_time_and_cost_estimate.html [new file with mode: 0644]
templates/webpages/requirement_spec/_show_time_and_cost_estimate_item.html [new file with mode: 0644]
templates/webpages/requirement_spec/show.html

diff --git a/SL/Controller/Helper/RequirementSpec.pm b/SL/Controller/Helper/RequirementSpec.pm
new file mode 100644 (file)
index 0000000..b24c4aa
--- /dev/null
@@ -0,0 +1,25 @@
+package SL::Controller::Helper::RequirementSpec;
+
+use strict;
+
+use Exporter qw(import);
+our @EXPORT = qw(init_visible_section);
+
+use SL::DB::Manager::RequirementSpecItem;
+
+sub init_visible_section {
+  my ($self)       = @_;
+
+  my $content_id   = $::form->{current_content_id};
+  my $content_type = $::form->{current_content_type};
+
+  return undef unless $content_id;
+  return undef unless $content_type =~ m/section|function-block/;
+
+  $self->visible_item(SL::DB::Manager::RequirementSpecItem->find_by(id => $content_id));
+  return undef unless $self->visible_item;
+
+  return $self->visible_section($self->visible_item->section);
+}
+
+1;
index 5d581f4..48af731 100644 (file)
@@ -10,8 +10,11 @@ use SL::Controller::Helper::Paginated;
 use SL::Controller::Helper::Sorted;
 use SL::Controller::Helper::ParseFilter;
 use SL::Controller::Helper::ReportGenerator;
+use SL::Controller::Helper::RequirementSpec;
 use SL::DB::Customer;
 use SL::DB::Project;
+use SL::DB::RequirementSpecComplexity;
+use SL::DB::RequirementSpecRisk;
 use SL::DB::RequirementSpecStatus;
 use SL::DB::RequirementSpecType;
 use SL::DB::RequirementSpec;
@@ -20,13 +23,12 @@ use SL::Locale::String;
 
 use Rose::Object::MakeMethods::Generic
 (
- scalar => [ qw(requirement_spec requirement_spec_item customers projects types statuses db_args flat_filter is_template) ],
+  scalar                  => [ qw(requirement_spec_item customers types statuses db_args flat_filter is_template visible_item visible_section) ],
+  'scalar --get_set_init' => [ qw(requirement_spec complexities risks projects) ],
 );
 
 __PACKAGE__->run_before('setup');
-__PACKAGE__->run_before('load_requirement_spec',      only => [ qw(    ajax_edit        update show destroy) ]);
-__PACKAGE__->run_before('load_select_options',        only => [ qw(new ajax_edit create update list) ]);
-__PACKAGE__->run_before('load_search_select_options', only => [ qw(                            list) ]);
+__PACKAGE__->run_before('load_select_options',  only => [ qw(new ajax_edit create update list) ]);
 
 __PACKAGE__->get_models_url_params('flat_filter');
 __PACKAGE__->make_paginated(
@@ -53,6 +55,7 @@ __PACKAGE__->make_sorted(
 # actions
 #
 
+
 sub action_list {
   my ($self) = @_;
 
@@ -79,6 +82,59 @@ sub action_ajax_edit {
   $self->render('requirement_spec/_form', { layout => 0 }, submit_as => 'ajax');
 }
 
+sub action_ajax_show_time_and_cost_estimate {
+  my ($self) = @_;
+
+  $self->render('requirement_spec/_show_time_and_cost_estimate', { layout => 0 });
+}
+
+sub action_ajax_cancel_time_and_cost_estimate {
+  my ($self) = @_;
+
+  my $html   = $self->render('requirement_spec/_show_time_and_cost_estimate', { output => 0 });
+
+  SL::ClientJS->new
+    ->replaceWith('#time_cost_estimate', $html)
+    ->render($self);
+}
+
+sub action_ajax_edit_time_and_cost_estimate {
+  my ($self) = @_;
+
+  my $html   = $self->render('requirement_spec/_edit_time_and_cost_estimate', { output => 0 });
+
+  SL::ClientJS->new
+    ->replaceWith('#time_cost_estimate', $html)
+    ->render($self);
+}
+
+sub action_ajax_save_time_and_cost_estimate {
+  my ($self) = @_;
+
+  $self->requirement_spec->db->do_transaction(sub {
+    # Make Emacs happy
+    1;
+    foreach my $attributes (@{ $::form->{requirement_spec_items} || [] }) {
+      SL::DB::RequirementSpecItem
+        ->new(id => delete $attributes->{id})
+        ->load
+        ->update_attributes(%{ $attributes });
+    }
+
+    1;
+  });
+
+  my $html = $self->render('requirement_spec/_show_time_and_cost_estimate', { output => 0 });
+  my $js   = SL::ClientJS->new->replaceWith('#time_cost_estimate', $html);
+
+  if ($self->visible_section) {
+    $html = $self->render('requirement_spec_item/_section', { output => 0 }, requirement_spec_item => $self->visible_section);
+    $js->html('#column-content', $html);
+  }
+
+  $js->render($self);
+}
+
 sub action_show {
   my ($self) = @_;
 
@@ -131,34 +187,42 @@ sub setup {
   $::request->{layout}->use_stylesheet("${_}.css") for qw(jquery.contextMenu requirement_spec);
   $::request->{layout}->use_javascript("${_}.js") for qw(jquery.jstree jquery/jquery.contextMenu client_js requirement_spec);
   $self->is_template($::form->{is_template} ? 1 : 0);
+  $self->init_visible_section;
 
   return 1;
 }
 
-sub load_requirement_spec {
+sub init_complexities {
+  my ($self) = @_;
+  return SL::DB::Manager::RequirementSpecComplexity->get_all_sorted;
+}
+
+sub init_risks {
+  my ($self) = @_;
+  return SL::DB::Manager::RequirementSpecRisk->get_all_sorted;
+}
+
+sub init_projects {
+  my ($self) = @_;
+  $self->projects(SL::DB::Manager::Project->get_all_sorted);
+}
+
+sub init_requirement_spec {
   my ($self) = @_;
-  $self->requirement_spec(SL::DB::RequirementSpec->new(id => $::form->{id})->load || die "No such requirement spec");
+  $self->requirement_spec(SL::DB::RequirementSpec->new(id => $::form->{id})->load || die "No such requirement spec") if $::form->{id};
 }
 
 sub load_select_options {
   my ($self) = @_;
 
   my @filter = ('!obsolete' => 1);
-  if ($self->requirement_spec && $self->requirement_spec->customer_id) {
-    @filter = ( or => [ @filter, id => $self->requirement_spec->customer_id ] );
-  }
+  @filter    = ( or => [ @filter, id => $self->requirement_spec->customer_id ] ) if $self->requirement_spec && $self->requirement_spec->customer_id;
 
   $self->customers(SL::DB::Manager::Customer->get_all_sorted(where => \@filter));
   $self->statuses( SL::DB::Manager::RequirementSpecStatus->get_all_sorted);
   $self->types(    SL::DB::Manager::RequirementSpecType->get_all_sorted);
 }
 
-sub load_search_select_options {
-  my ($self) = @_;
-
-  $self->projects(SL::DB::Manager::Project->get_all_sorted);
-}
-
 #
 # helpers
 #
index d4f3b8f..7cad811 100644 (file)
@@ -10,6 +10,7 @@ use List::Util qw(first);
 use Time::HiRes ();
 
 use SL::Clipboard;
+use SL::Controller::Helper::RequirementSpec;
 use SL::DB::RequirementSpec;
 use SL::DB::RequirementSpecComplexity;
 use SL::DB::RequirementSpecItem;
@@ -490,21 +491,6 @@ sub format_exception {
   return join "\n", (split m/\n/, $@)[0..4];
 }
 
-sub init_visible_section {
-  my ($self)       = @_;
-
-  my $content_id   = $::form->{current_content_id};
-  my $content_type = $::form->{current_content_type};
-
-  return undef unless $content_id;
-  return undef unless $content_type =~ m/section|function-block/;
-
-  $self->visible_item(SL::DB::Manager::RequirementSpecItem->find_by(id => $content_id));
-  return undef unless $self->visible_item;
-
-  return $self->visible_section($self->visible_item->section);
-}
-
 sub init_complexities {
   my ($self) = @_;
 
diff --git a/SL/Controller/Test.pm b/SL/Controller/Test.pm
new file mode 100644 (file)
index 0000000..30638a1
--- /dev/null
@@ -0,0 +1,17 @@
+package SL::Controller::Test;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use Data::Dumper;
+use SL::ClientJS;
+
+sub action_dump_form {
+  my ($self) = @_;
+
+  my $output = Dumper($::form);
+  $self->render(\$output, { type => 'text' });
+}
+
+1;
index d5edd4b..4562817 100644 (file)
@@ -43,8 +43,9 @@ table.rs_input_field input, table.rs_input_field select {
 /* Special things that apply to the context menu */
 /* ------------------------------------------------------------ */
 
-.context-menu-item.icon-flag { background-image: url("../image/flag-red.png"); }
-
+.context-menu-item.icon-flag  { background-image: url("../image/flag-red.png"); }
+.context-menu-item.icon-close { background-image: url("../image/document-close.png"); }
+.context-menu-item.icon-save  { background-image: url("../image/document-save.png"); }
 
 /* ------------------------------------------------------------ */
 /* Sections & function blocks */
index d1f1ef8..5da04ec 100644 (file)
@@ -5,21 +5,23 @@ namespace("kivi").setupLocale({
 "Add sub function block":"Unterfunktionsblock hinzufügen",
 "Add text block":"Textblock erfassen",
 "Are you sure?":"Sind Sie sicher?",
+"Copy":"Kopieren",
 "Database Connection Test":"Test der Datenbankverbindung",
+"Delete text block":"Textblock löschen",
+"Delete":"Löschen",
+"Do you really want to cancel?":"Wollen Sie wirklich abbrechen?",
 "Do you want to set the account number \"#1\" to \"#2\" and the name \"#3\" to \"#4\"?":"Soll die Kontonummer \"#1\" zu \"#2\" und den Name \"#3\" zu \"#4\" geändert werden?",
+"Edit text block":"Textblock bearbeiten",
+"Edit":"Bearbeiten",
 "Enter longdescription":"Langtext eingeben",
 "Map":"Karte",
 "Part picker":"Artikelauswahl",
+"Paste":"Einfügen",
+"Save":"Speichern",
 "The description is missing.":"Die Beschreibung fehlt.",
 "The name is missing.":"Der Name fehlt.",
 "The name must only consist of letters, numbers and underscores and start with a letter.":"Der Name darf nur aus Buchstaben (keine Umlaute), Ziffern und Unterstrichen bestehen und muss mit einem Buchstaben beginnen.",
 "The option field is empty.":"Das Optionsfeld ist leer.",
 "The selected database is still configured for client \"#1\". If you delete the database that client will stop working until you re-configure it. Do you still want to delete the database?":"Die auswählte Datenbank ist noch für Mandant \"#1\" konfiguriert. Wenn Sie die Datenbank löschen, wird der Mandanten nicht mehr funktionieren, bis er anders konfiguriert wurde. Wollen Sie die Datenbank trotzdem löschen?"
-"Copy":"Kopieren",
-"Delete":"Löschen",
-"Delete text block":"Textblock löschen",
-"Edit":"Bearbeiten",
-"Edit text block":"Textblock bearbeiten",
-"Paste":"Einfügen",
 "Toggle marker":"Markierung umschalten"
 });
index 618ffb1..a259682 100644 (file)
@@ -268,6 +268,32 @@ function requirement_spec_item_popup_menu_hidden(opt) {
   return handle_item_popup_menu_markings(opt, false);
 }
 
+// -------------------------------------------------------------------------
+// -------------------------- time/cost estimate ---------------------------
+// -------------------------------------------------------------------------
+
+function standard_time_cost_estimate_ajax_call(key, opt) {
+  if ((key == 'cancel') && !confirm(kivi.t8('Do you really want to cancel?')))
+    return true;
+
+  var data = "action=RequirementSpec/ajax_" + key + "_time_and_cost_estimate&";
+
+  if (key == 'save')
+    data += $('#edit_time_cost_estimate_form').serialize()
+         +  '&' + $('#current_content_type').serialize()
+         +  '&' + $('#current_content_id').serialize();
+  else
+    data += 'id=' + encodeURIComponent($('#requirement_spec_id').val());
+
+  $.post("controller.pl", data, eval_json_result);
+
+  return true;
+}
+
+// -------------------------------------------------------------------------
+// ----------------------------- context menus -----------------------------
+// -------------------------------------------------------------------------
+
 function create_requirement_spec_context_menus() {
   var events = {
     show: requirement_spec_text_block_popup_menu_shown,
@@ -330,4 +356,19 @@ function create_requirement_spec_context_menus() {
       , paste:                  { name: kivi.t8('Paste'),                  icon: "paste",  callback: standard_item_ajax_call }
     }
   });
+
+  $.contextMenu({
+    selector: '.time-cost-estimate-context-menu',
+    events:   events,
+    items:    { edit: { name: kivi.t8('Edit'), icon: "edit", callback: standard_time_cost_estimate_ajax_call } }
+  });
+
+  $.contextMenu({
+    selector: '.edit-time-cost-estimate-context-menu',
+    events:   events,
+    items:    {
+        save:   { name: kivi.t8('Save'),   icon: "save",  callback: standard_time_cost_estimate_ajax_call }
+      , cancel: { name: kivi.t8('Cancel'), icon: "close", callback: standard_time_cost_estimate_ajax_call }
+    }
+  });
 }
index 0f7f918..da52e72 100755 (executable)
@@ -514,6 +514,7 @@ $self->{texts} = {
   'Copy file from #1 to #2 failed: #3' => 'Kopieren der Datei von #1 nach #2 schlug fehl: #3',
   'Copy'                        => 'Kopieren',
   'Correct taxkey'              => 'Richtiger Steuerschlüssel',
+  'Cost'                        => 'Kosten',
   'Costs'                       => 'Kosten',
   'Could not load class #1 (#2): "#3"' => 'Konnte Klasse #1 (#2) nicht laden: "#3"',
   'Could not load class #1, #2' => 'Konnte Klasse #1 nicht laden: "#2"',
@@ -765,6 +766,7 @@ $self->{texts} = {
   'Do not change the tax rate of taxkey 0.' => 'Ändern Sie nicht den Steuersatz vom Steuerschlüssel 0.',
   'Do not check for duplicates' => 'Nicht nach Dubletten suchen',
   'Do not set default buchungsgruppe' => 'Nie Standardbuchungsgruppe setzen',
+  'Do you really want to cancel?' => 'Wollen Sie wirklich abbrechen?',
   'Do you really want to close the following SEPA exports? No payment will be recorded for bank collections that haven\'t been marked as executed yet.' => 'Wollen Sie wirklich die folgenden SEPA-Exporte abschließen? Für Überweisungen, die noch nicht gebucht wurden, werden dann keine Zahlungen verbucht.',
   'Do you really want to close the following SEPA exports? No payment will be recorded for bank transfers that haven\'t been marked as executed yet.' => 'Wollen Sie wirklich die folgenden SEPA-Exporte abschließen? Für Überweisungen, die noch nicht gebucht wurden, werden dann keine Zahlungen verbucht.',
   'Do you really want to delete AP transaction #1?' => 'Wollen Sie wirklich die Kreditorenbuchung #1 löschen?',
@@ -1064,6 +1066,7 @@ $self->{texts} = {
   'Full Access'                 => 'Vollzugriff',
   'Full Preview'                => 'Alles',
   'Full access to all functions' => 'Vollzugriff auf alle Funktionen',
+  'Function block'              => 'Funktionsblock',
   'Function/position'           => 'Funktion/Position',
   'Fwd'                         => 'Vorw&auml;rts',
   'GL Transaction'              => 'Dialogbuchung',
@@ -1395,6 +1398,7 @@ $self->{texts} = {
   'Name and Street'             => 'Name und Straße',
   'National Expenses'           => 'Aufwand Inland',
   'National Revenues'           => 'Erl&ouml;se Inland',
+  'Neither sections nor function blocks have been created yet.' => 'Es wurden bisher weder Abschnitte noch Funktionsblöcke angelegt.',
   'Net Income Statement'        => 'Einnahmenüberschußrechnung',
   'Net amount'                  => 'Nettobetrag',
   'Net amount (for verification)' => 'Nettobetrag (zur Überprüfung)',
@@ -2091,6 +2095,8 @@ $self->{texts} = {
   'Such entries cannot be exported into the DATEV format and have to be fixed as well.' => 'Solche Einträge sind aber nicht DATEV-exportiertbar und müssen ebenfalls korrigiert werden.',
   'Sum Credit'                  => 'Summe Haben',
   'Sum Debit'                   => 'Summe Soll',
+  'Sum for #1'                  => 'Summe für #1',
+  'Sum for section'             => 'Summe für Abschnitt',
   'Sum for'                     => 'Summe für',
   'Sum open amount'             => 'Summierter offener Betrag',
   'Sum per'                     => 'Summe per',
@@ -2459,6 +2465,7 @@ $self->{texts} = {
   'Three Options:'              => 'Drei Optionen:',
   'Time Format'                 => 'Uhrzeitformat',
   'Time Tracking'               => 'Zeiterfassung',
+  'Time and cost estimate'      => 'Zeit- und Kostenschätzung',
   'Time estimate'               => 'Zeitschätzung',
   'Time period for the analysis:' => 'Analysezeitraum:',
   'Timestamp'                   => 'Uhrzeit',
diff --git a/templates/webpages/requirement_spec/_edit_time_and_cost_estimate.html b/templates/webpages/requirement_spec/_edit_time_and_cost_estimate.html
new file mode 100644 (file)
index 0000000..f0e6831
--- /dev/null
@@ -0,0 +1,44 @@
+[%- USE LxERP -%][%- USE L -%][%- USE HTML -%][%- USE P -%]
+[%- DEFAULT id_prefix = 'time_and_cost_estimate_form' %]
+
+<div id="time_cost_estimate" class="edit-time-cost-estimate-context-menu">
+ [%- IF !SELF.requirement_spec.sections.size %]
+  <p>[%- LxERP.t8("Neither sections nor function blocks have been created yet.") %]</p>
+
+ [%- ELSE %]
+
+  [%- SET at_least_one_function_block = 0 %]
+
+  <form method="post" id="edit_time_cost_estimate_form">
+   [%- L.hidden_tag('id', SELF.requirement_spec.id, id=id_prefix _ '_id') -%]
+
+   [%# time-cost-estimate-context-menu %]
+   <table class="time-cost-estimate">
+    <tbody>
+     [%- FOREACH section = SELF.requirement_spec.sections %]
+      <tr class="listheading">
+       <th>[%- LxERP.t8("Function block") %]</th>
+       <th>[%- LxERP.t8("Complexity") %]</th>
+       <th>[%- LxERP.t8("Risk") %]</th>
+       <th align="right">[%- LxERP.t8("Time estimate") %]</th>
+      </tr>
+
+      <tr class="listrow section">
+       <td colspan="5">[%- HTML.escape(section.fb_number) %]: [%- HTML.escape(section.title) %]</td>
+      </tr>
+
+      [%- IF section.children.size %]
+       [%- SET at_least_one_function_block = 1 %]
+       [%- FOREACH child = section.children %]
+        [%- INCLUDE 'requirement_spec/_edit_time_and_cost_estimate_item.html'
+                    id_prefix = id_prefix
+                    item      = child
+                    level     = 1 %]
+       [%- END %]
+      [%- END -%]
+     [%- END -%]
+    </tbody>
+   </table>
+  </form>
+ [%- END %]
+</div>
diff --git a/templates/webpages/requirement_spec/_edit_time_and_cost_estimate_item.html b/templates/webpages/requirement_spec/_edit_time_and_cost_estimate_item.html
new file mode 100644 (file)
index 0000000..a9f6fe5
--- /dev/null
@@ -0,0 +1,28 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE P -%][%- USE L -%]
+<tr class="listrow">
+ [% L.hidden_tag("requirement_spec_items[+].id", item.id, id = id_prefix _ '_item_id') %]
+
+ <td style="padding-left: [%- level * 50 -%]px">
+  [%- P.simple_format(item.fb_number _ ": " _ item.description) -%]
+ </td>
+ <td>
+  [%- L.select_tag('requirement_spec_items[].complexity_id', SELF.complexities, id=id_prefix _ '_complexity_id', title_key='description', default=item.complexity_id, style="width: 100%") %]<br>
+ </td>
+ <td>
+  [%- L.select_tag('requirement_spec_items[].risk_id', SELF.risks, id=id_prefix _ '_risk_id', title_key='description', default=item.risk_id, style="width: 100%") %]<br>
+ </td>
+ [%- IF !item.children.size -%]
+  <td align="right" nowrap>[%- P.man_days_tag('requirement_spec_items[].time_estimation', item, id=id_prefix _ '_time_estimation') %]</td>
+ [%- ELSE -%]
+  <td>&nbsp;</td>
+ [%- END -%]
+</tr>
+
+[%- IF item.children.size -%]
+ [%- FOREACH child = item.children -%]
+  [%- INCLUDE 'requirement_spec/_edit_time_and_cost_estimate_item.html'
+              id_prefix = id_prefix
+              item      = child
+              level     = level + 1 -%]
+ [%- END -%]
+[%- END -%]
index 69a111f..fe3ca31 100644 (file)
@@ -29,7 +29,7 @@
 
   <tr>
    <td>[% LxERP.t8("Hourly Rate") %]</td>
-   <td>[% L.input_tag(form_prefix _ "hourly_rate_as_number", SELF.requirement_spec.hourly_rate_as_number, id=id_prefix _ '_hourly_rate_as_number') %]</td>
+   <td>[% L.input_tag("requirement_spec.hourly_rate_as_number", SELF.requirement_spec.hourly_rate_as_number, id=id_prefix _ '_hourly_rate_as_number') %]</td>
   </tr>
 
  </table>
diff --git a/templates/webpages/requirement_spec/_show_time_and_cost_estimate.html b/templates/webpages/requirement_spec/_show_time_and_cost_estimate.html
new file mode 100644 (file)
index 0000000..1c51e05
--- /dev/null
@@ -0,0 +1,53 @@
+[%- USE LxERP -%][%- USE L -%][%- USE HTML -%][%- USE P -%]
+[%- DEFAULT id_prefix = 'time_and_cost_estimate_form' %]
+
+<div id="time_cost_estimate">
+ [%- IF !SELF.requirement_spec.sections.size %]
+  <p>[%- LxERP.t8("Neither sections nor function blocks have been created yet.") %]</p>
+
+ [%- ELSE %]
+
+  [%- SET at_least_one_function_block = 0 %]
+
+  <table class="time-cost-estimate time-cost-estimate-context-menu">
+   <tbody>
+    [%- FOREACH section = SELF.requirement_spec.sections %]
+     <tr class="listheading">
+      <th>[%- LxERP.t8("Function block") %]</th>
+      <th>[%- LxERP.t8("Complexity") %]</th>
+      <th>[%- LxERP.t8("Risk") %]</th>
+      <th align="right">[%- LxERP.t8("Time estimate") %]</th>
+      <th align="right">[%- LxERP.t8("Cost") %]</th>
+     </tr>
+
+     <tr class="listrow section">
+      <td colspan="5">[%- HTML.escape(section.fb_number) %]: [%- HTML.escape(section.title) %]</td>
+     </tr>
+
+     [%- IF section.children.size %]
+      [%- SET at_least_one_function_block = 1 %]
+      [%- FOREACH child = section.children %]
+       [%- INCLUDE 'requirement_spec/_show_time_and_cost_estimate_item.html'
+                   item  = child
+                   level = 1 %]
+      [%- END %]
+
+      <tr class="listrow subtotal">
+       <td style="padding-left: 50px" colspan="3" class="sum">[%- LxERP.t8("Sum for section") -%]:</td>
+       <td align="right" nowrap>[%- P.format_man_days(section.time_estimation, 'skip_zero'=1) -%]</td>
+       <td align="right" nowrap>[%- LxERP.format_amount(section.time_estimation * SELF.requirement_spec.hourly_rate, 2) -%] EUR</td>
+      </tr>
+     [%- END -%]
+    [%- END -%]
+   </tbody>
+
+   <tfoot>
+    <tr>
+     <td colspan="3">[%- LxERP.t8("Sum for #1", SELF.requirement_spec.type.description) -%]:</td>
+     <td align="right" nowrap>[%- P.format_man_days(SELF.requirement_spec.time_estimation) -%]</td>
+     <td align="right" nowrap>[%- LxERP.format_amount(SELF.requirement_spec.time_estimation * SELF.requirement_spec.hourly_rate, 2) -%] EUR</td>
+    </tr>
+   </tfoot>
+  </table>
+ [%- END %]
+</div>
diff --git a/templates/webpages/requirement_spec/_show_time_and_cost_estimate_item.html b/templates/webpages/requirement_spec/_show_time_and_cost_estimate_item.html
new file mode 100644 (file)
index 0000000..844afd0
--- /dev/null
@@ -0,0 +1,29 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE P -%]
+<tr class="listrow">
+ <td style="padding-left: [%- level * 50 -%]px">
+  [%- P.simple_format(item.fb_number _ ": " _ item.description) -%]
+ </td>
+ <td>[%- HTML.escape(item.complexity.description) -%]</td>
+ <td>[%- HTML.escape(item.risk.description) -%]</td>
+ [%- IF !item.children.size -%]
+  <td align="right" nowrap>[%- P.format_man_days(item.time_estimation, skip_zero=1) -%]</td>
+  <td align="right" nowrap>[%- LxERP.format_amount(item.time_estimation * SELF.requirement_spec.hourly_rate, 2) -%] EUR</td>
+ [%- ELSE -%]
+  <td>&nbsp;</td>
+  <td>&nbsp;</td>
+ [%- END -%]
+</tr>
+
+[%- IF item.children.size -%]
+ [%- FOREACH child = item.children -%]
+  [%- INCLUDE 'requirement_spec/_show_time_and_cost_estimate_item.html'
+              item  = child
+              level = level + 1 -%]
+ [%- END -%]
+
+ <tr class="listrow subtotal">
+  <td style="padding-left: [%- (level + 1) * 50 -%]px" colspan="3">[%- LxERP.t8("Sum for #1", item.fb_number) -%]:</td>
+  <td align="right" nowrap>[%- P.format_man_days(item.time_estimation, skip_zero=1) -%]</td>
+  <td align="right" nowrap>[%- LxERP.format_amount(item.time_estimation * SELF.requirement_spec.hourly_rate, 2) -%] EUR</td>
+ </tr>
+[%- END -%]
index f3e75e7..4cae971 100644 (file)
@@ -10,6 +10,7 @@
  <ul>
   <li><a href="#function-blocks-tab">[%- LxERP.t8("Content") %]</a></li>
   <li><a href="controller.pl?action=RequirementSpec/ajax_edit&id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Basic settings") %]</a></li>
+  <li><a href="controller.pl?action=RequirementSpec/ajax_show_time_and_cost_estimate&id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Time and cost estimate") %]</a></li>
  </ul>
 
  <div id="function-blocks-tab">