--- /dev/null
+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;
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;
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(
# actions
#
+
sub action_list {
my ($self) = @_;
$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) = @_;
$::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
#
use Time::HiRes ();
use SL::Clipboard;
+use SL::Controller::Helper::RequirementSpec;
use SL::DB::RequirementSpec;
use SL::DB::RequirementSpecComplexity;
use SL::DB::RequirementSpecItem;
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) = @_;
--- /dev/null
+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;
/* 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 */
"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"
});
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,
, 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 }
+ }
+ });
}
'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"',
'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?',
'Full Access' => 'Vollzugriff',
'Full Preview' => 'Alles',
'Full access to all functions' => 'Vollzugriff auf alle Funktionen',
+ 'Function block' => 'Funktionsblock',
'Function/position' => 'Funktion/Position',
'Fwd' => 'Vorwärts',
'GL Transaction' => 'Dialogbuchung',
'Name and Street' => 'Name und Straße',
'National Expenses' => 'Aufwand Inland',
'National Revenues' => 'Erlö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)',
'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',
'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',
--- /dev/null
+[%- 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>
--- /dev/null
+[%- 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> </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 -%]
<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>
--- /dev/null
+[%- 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>
--- /dev/null
+[%- 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> </td>
+ <td> </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 -%]
<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">