Pflichtenheftabschnittsvorlagen erzeugen, bearbeiten, löschen
authorMoritz Bunkus <m.bunkus@linet-services.de>
Fri, 10 May 2013 09:46:39 +0000 (11:46 +0200)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Tue, 1 Apr 2014 11:03:22 +0000 (13:03 +0200)
SL/Controller/RequirementSpec.pm
SL/DB/MetaSetup/RequirementSpec.pm
js/locale/de.js
js/requirement_spec.js
locale/de/all
menus/erp.ini
sql/Pg-upgrade2/requirement_specs_section_templates.sql [new file with mode: 0644]
templates/webpages/requirement_spec/_filter.html
templates/webpages/requirement_spec/_form.html
templates/webpages/requirement_spec/_header.html
templates/webpages/requirement_spec/show.html

index 804a5af..abcfbff 100644 (file)
@@ -25,7 +25,7 @@ use SL::Template::LaTeX;
 
 use Rose::Object::MakeMethods::Generic
 (
-  scalar                  => [ qw(requirement_spec_item customers types statuses db_args flat_filter is_template visible_item visible_section) ],
+  scalar                  => [ qw(requirement_spec_item customers types statuses db_args flat_filter visible_item visible_section) ],
   'scalar --get_set_init' => [ qw(requirement_spec complexities risks projects copy_source js) ],
 );
 
@@ -77,13 +77,13 @@ sub action_list {
 sub action_new {
   my ($self) = @_;
 
-  $self->requirement_spec(SL::DB::RequirementSpec->new);
+  $self->requirement_spec(SL::DB::RequirementSpec->new(is_template => $::form->{is_template}));
 
   if ($self->copy_source) {
     $self->requirement_spec->$_($self->copy_source->$_) for qw(type_id status_id customer_id title hourly_rate)
   }
 
-  $self->render('requirement_spec/new', title => t8('Create a new requirement spec'));
+  $self->render('requirement_spec/new', title => $self->requirement_spec->is_template ? t8('Create a new requirement spec section template') : t8('Create a new requirement spec'));
 }
 
 sub action_ajax_edit {
@@ -220,7 +220,6 @@ sub setup {
   $::auth->assert('sales_quotation_edit');
   $::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;
@@ -275,10 +274,14 @@ sub create_or_update {
   my $self   = shift;
   my $is_new = !$self->requirement_spec->id;
   my $params = delete($::form->{requirement_spec}) || { };
-  my $title  = $is_new ? t8('Create a new requirement spec') : t8('Edit requirement spec');
 
   $self->requirement_spec->assign_attributes(%{ $params });
 
+  my $title  = $is_new && $self->requirement_spec->is_template ? t8('Create a new requirement spec section template')
+             : $is_new                                         ? t8('Create a new requirement spec')
+             :            $self->requirement_spec->is_template ? t8('Edit section template')
+             :                                                   t8('Edit requirement spec');
+
   my @errors = $self->requirement_spec->validate;
 
   if (@errors) {
@@ -306,15 +309,17 @@ sub create_or_update {
     return $self->render('requirement_spec/new', title => $title);
   }
 
+  my $info = $self->requirement_spec->is_template ? t8('The section template has been saved.') : t8('The requirement spec has been saved.');
+
   if ($::request->is_ajax) {
     my $html = $self->render('requirement_spec/_header', { output => 0 });
     return $self->invalidate_version
       ->replaceWith('#requirement-spec-header', $html)
-      ->flash('info', t8('The requirement spec has been saved.'))
+      ->flash('info', $info)
       ->render($self);
   }
 
-  flash_later('info', $is_new ? t8('The requirement spec has been created.') : t8('The requirement spec has been saved.'));
+  flash_later('info', $info);
   $self->redirect_to(action => 'show', id => $self->requirement_spec->id);
 }
 
@@ -332,7 +337,7 @@ sub setup_db_args_from_filter {
     and => [
       @{ $args{where} || [] },
       working_copy_id => undef,
-      is_template     => $self->is_template
+      is_template     => $::form->{is_template} ? 1 : 0,
     ]];
 
   $self->db_args(\%args);
@@ -343,33 +348,40 @@ sub prepare_report {
 
   my $callback    = $self->get_callback;
 
+  my $is_template = $::form->{is_template};
   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
   $self->{report} = $report;
 
-  my @columns     = qw(title customer status type projectnumber mtime version);
-  my @sortable    = qw(title customer status type projectnumber mtime);
+  my @columns     = $is_template ? qw(title mtime) : qw(title customer status type projectnumber mtime version);
+  my @sortable    = $is_template ? qw(title mtime) : qw(title customer status type projectnumber mtime);
 
   my %column_defs = (
     title         => { obj_link => sub { $self->url_for(action => 'show', id => $_[0]->id, callback => $callback) } },
-    customer      => { raw_data => sub { $self->presenter->customer($_[0]->customer, display => 'table-cell', callback => $callback) },
-                       sub      => sub { $_[0]->customer->name } },
-    projectnumber => { raw_data => sub { $self->presenter->project($_[0]->project, display => 'table-cell', callback => $callback) },
-                       sub      => sub { $_[0]->project_id ? $_[0]->project->projectnumber : '' } },
-    status        => { sub      => sub { $_[0]->status->description } },
-    type          => { sub      => sub { $_[0]->type->description } },
-    version       => { sub      => sub { $_[0]->version_id ? $_[0]->version->version_number : t8('Working copy without version') } },
     mtime         => { sub      => sub { ($_[0]->mtime || $_[0]->itime)->to_kivitendo(precision => 'minute') } },
   );
 
+  if (!$is_template) {
+    %column_defs = (
+      %column_defs,
+      customer      => { raw_data => sub { $self->presenter->customer($_[0]->customer, display => 'table-cell', callback => $callback) },
+                         sub      => sub { $_[0]->customer->name } },
+      projectnumber => { raw_data => sub { $self->presenter->project($_[0]->project, display => 'table-cell', callback => $callback) },
+                         sub      => sub { $_[0]->project_id ? $_[0]->project->projectnumber : '' } },
+      status        => { sub      => sub { $_[0]->status->description } },
+      type          => { sub      => sub { $_[0]->type->description } },
+      version       => { sub      => sub { $_[0]->version_id ? $_[0]->version->version_number : t8('Working copy without version') } },
+    );
+  }
+
   map { $column_defs{$_}->{text} ||= $::locale->text( $self->get_sort_spec->{$_}->{title} ) } keys %column_defs;
 
   $report->set_options(
     std_column_visibility => 1,
     controller_class      => 'RequirementSpec',
     output_format         => 'HTML',
-    raw_top_info_text     => $self->render('requirement_spec/report_top',    { output => 0 }),
+    raw_top_info_text     => $self->render('requirement_spec/report_top',    { output => 0 }, is_template => $is_template),
     raw_bottom_info_text  => $self->render('requirement_spec/report_bottom', { output => 0 }),
-    title                 => $::locale->text('Requirement Specs'),
+    title                 => $is_template ? t8('Requirement Spec Section Templates') : t8('Requirement Specs'),
     allow_pdf_export      => 1,
     allow_csv_export      => 1,
   );
@@ -386,11 +398,12 @@ sub invalidate_version {
   my ($self) = @_;
 
   my $rspec  = SL::DB::RequirementSpec->new(id => $self->requirement_spec->id)->load;
+  return $self->js if $rspec->is_template;
+
   $rspec->invalidate_version;
 
   my $html = $self->render('requirement_spec/_version', { output => 0 }, requirement_spec => $rspec);
   return $self->js->html('#requirement_spec_version', $html);
 }
 
-
 1;
index 9971f86..a6a2c4f 100644 (file)
@@ -11,10 +11,10 @@ __PACKAGE__->meta->setup(
 
   columns => [
     id                      => { type => 'serial', not_null => 1 },
-    type_id                 => { type => 'integer', not_null => 1 },
-    status_id               => { type => 'integer', not_null => 1 },
+    type_id                 => { type => 'integer' },
+    status_id               => { type => 'integer' },
     version_id              => { type => 'integer' },
-    customer_id             => { type => 'integer', not_null => 1 },
+    customer_id             => { type => 'integer' },
     project_id              => { type => 'integer' },
     title                   => { type => 'text', not_null => 1 },
     hourly_rate             => { type => 'numeric', default => '0', not_null => 1, precision => 2, scale => 8 },
index 22bd93f..bb0c3a9 100644 (file)
@@ -7,11 +7,13 @@ namespace("kivi").setupLocale({
 "Are you sure?":"Sind Sie sicher?",
 "Copy":"Kopieren",
 "Copy requirement spec":"Pflichtenheft kopieren",
+"Copy section template":"Abschnittsvorlage kopieren",
 "Create PDF":"PDF erzeugen",
 "Create new version":"Neue Version anlegen",
 "Database Connection Test":"Test der Datenbankverbindung",
 "Delete":"Löschen",
 "Delete requirement spec":"Pflichtenheft löschen",
+"Delete section template":"Abschnittsvorlage löschen",
 "Delete text block":"Textblock löschen",
 "Do you really want to cancel?":"Wollen Sie wirklich abbrechen?",
 "Do you really want to revert to this version?":"Wollen Sie wirklich auf diese Version zurücksetzen?",
@@ -26,6 +28,7 @@ namespace("kivi").setupLocale({
 "Requirement spec actions":"Pflichtenheftaktionen",
 "Revert to version":"Auf Version zurücksetzen",
 "Save":"Speichern",
+"Section template actions":"Abschnittsvorlagen-Aktionen",
 "Section/Function block actions":"Abschnitts-/Funktionsblockaktionen",
 "Text block actions":"Textblockaktionen",
 "The description is missing.":"Die Beschreibung fehlt.",
index d44bf9f..e9f9277 100644 (file)
@@ -372,46 +372,84 @@ ns.revert_to_versioned_copy_ajax_call = function(key, opt) {
 // ----------------------------- context menus -----------------------------
 // -------------------------------------------------------------------------
 
-ns.create_context_menus = function() {
-  var general_actions = {
-      sep98:           "---------"
-    , general_actions: { name: kivi.t8('Requirement spec actions'), className: 'context-menu-heading' }
-    // , sep99:           "---------"
-    , create_pdf:      { name: kivi.t8('Create PDF'),              icon: "pdf",    callback: kivi.requirement_spec.create_reqspec_pdf }
-    , create_version:  { name: kivi.t8('Create new version'),      icon: "new",    callback: kivi.requirement_spec.create_version, disabled: kivi.requirement_spec.disable_commands }
-    , copy_reqspec:    { name: kivi.t8('Copy requirement spec'),   icon: "copy",   callback: kivi.requirement_spec.copy_reqspec   }
-    , delete_reqspec:  { name: kivi.t8('Delete requirement spec'), icon: "delete", callback: kivi.requirement_spec.delete_reqspec }
-  };
+ns.create_context_menus = function(is_template) {
+  if (is_template) {
+    var general_actions = {
+        sep98:           "---------"
+      , general_actions: { name: kivi.t8('Section template actions'), className: 'context-menu-heading' }
+      // , sep99:           "---------"
+      , copy_reqspec:    { name: kivi.t8('Copy section template'),   icon: "copy",   callback: kivi.requirement_spec.copy_reqspec   }
+      , delete_reqspec:  { name: kivi.t8('Delete section template'), icon: "delete", callback: kivi.requirement_spec.delete_reqspec }
+    };
+    var events = {};
+
+  } else {                      // if (is_template)
+    var general_actions = {
+        sep98:           "---------"
+      , general_actions: { name: kivi.t8('Requirement spec actions'), className: 'context-menu-heading' }
+      // , sep99:           "---------"
+      , create_pdf:      { name: kivi.t8('Create PDF'),              icon: "pdf",    callback: kivi.requirement_spec.create_reqspec_pdf }
+      , create_version:  { name: kivi.t8('Create new version'),      icon: "new",    callback: kivi.requirement_spec.create_version, disabled: kivi.requirement_spec.disable_commands }
+      , copy_reqspec:    { name: kivi.t8('Copy requirement spec'),   icon: "copy",   callback: kivi.requirement_spec.copy_reqspec   }
+      , delete_reqspec:  { name: kivi.t8('Delete requirement spec'), icon: "delete", callback: kivi.requirement_spec.delete_reqspec }
+    };
+
+    var events = {
+        show: kivi.requirement_spec.text_block_popup_menu_shown
+      , hide: kivi.requirement_spec.text_block_popup_menu_hidden
+    };
+
+    $.contextMenu({
+      selector: '.text-block-context-menu',
+      events:   {
+          show: kivi.requirement_spec.text_block_popup_menu_shown
+        , hide: kivi.requirement_spec.text_block_popup_menu_hidden
+      },
+      items:    $.extend({
+          heading: { name: kivi.t8('Text block actions'),    className: 'context-menu-heading' }
+        , add:     { name: kivi.t8('Add text block'),        icon: "add",    callback: kivi.requirement_spec.standard_text_block_ajax_call }
+        , edit:    { name: kivi.t8('Edit text block'),       icon: "edit",   callback: kivi.requirement_spec.standard_text_block_ajax_call, disabled: kivi.requirement_spec.disable_edit_text_block_commands }
+        , delete:  { name: kivi.t8('Delete text block'),     icon: "delete", callback: kivi.requirement_spec.ask_delete_text_block,         disabled: kivi.requirement_spec.disable_edit_text_block_commands }
+        , sep1:    "---------"
+        , flag:    { name: kivi.t8('Toggle marker'),         icon: "flag",   callback: kivi.requirement_spec.standard_text_block_ajax_call, disabled: kivi.requirement_spec.disable_edit_text_block_commands }
+        , sep2:    "---------"
+        , copy:    { name: kivi.t8('Copy'),                  icon: "copy",   callback: kivi.requirement_spec.standard_text_block_ajax_call, disabled: kivi.requirement_spec.disable_edit_text_block_commands }
+        , paste:   { name: kivi.t8('Paste'),                 icon: "paste",  callback: kivi.requirement_spec.standard_text_block_ajax_call  }
+      }, general_actions)
+    });
+
+    $.contextMenu({
+      selector: '.time-cost-estimate-context-menu',
+      items:    $.extend({
+          heading: { name: kivi.t8('Time/cost estimate actions'), className: 'context-menu-heading' }
+        , edit:    { name: kivi.t8('Edit'), icon: "edit", callback: kivi.requirement_spec.standard_time_cost_estimate_ajax_call }
+      }, general_actions)
+    });
+
+    $.contextMenu({
+      selector: '.edit-time-cost-estimate-context-menu',
+      items:    $.extend({
+          heading: { name: kivi.t8('Time/cost estimate actions'), className: 'context-menu-heading' }
+        , save:    { name: kivi.t8('Save'),   icon: "save",  callback: kivi.requirement_spec.standard_time_cost_estimate_ajax_call }
+        , cancel:  { name: kivi.t8('Cancel'), icon: "close", callback: kivi.requirement_spec.standard_time_cost_estimate_ajax_call }
+      }, general_actions)
+    });
+
+    $.contextMenu({
+      selector: '.versioned-copy-context-menu',
+      items:    $.extend({
+          heading:            { name: kivi.t8('Version actions'), className: 'context-menu-heading' }
+        , create_version_pdf: { name: kivi.t8('Create PDF'),        icon: "pdf",    callback: kivi.requirement_spec.create_pdf_for_versioned_copy_ajax_call                                                                      }
+        , revert_to_version:  { name: kivi.t8('Revert to version'), icon: "revert", callback: kivi.requirement_spec.revert_to_versioned_copy_ajax_call,     disabled: kivi.requirement_spec.disable_versioned_copy_item_commands }
+      }, general_actions)
+    });
+  }                             // if (is_template) ... else ...
 
   $.contextMenu({
     selector: '#content',
     items:    general_actions
   });
 
-  var events = {
-      show: kivi.requirement_spec.text_block_popup_menu_shown
-    , hide: kivi.requirement_spec.text_block_popup_menu_hidden
-  };
-
-  $.contextMenu({
-    selector: '.text-block-context-menu',
-    events:   {
-        show: kivi.requirement_spec.text_block_popup_menu_shown
-      , hide: kivi.requirement_spec.text_block_popup_menu_hidden
-    },
-    items:    $.extend({
-        heading: { name: kivi.t8('Text block actions'),    className: 'context-menu-heading' }
-      , add:     { name: kivi.t8('Add text block'),        icon: "add",    callback: kivi.requirement_spec.standard_text_block_ajax_call }
-      , edit:    { name: kivi.t8('Edit text block'),       icon: "edit",   callback: kivi.requirement_spec.standard_text_block_ajax_call, disabled: kivi.requirement_spec.disable_edit_text_block_commands }
-      , delete:  { name: kivi.t8('Delete text block'),     icon: "delete", callback: kivi.requirement_spec.ask_delete_text_block,         disabled: kivi.requirement_spec.disable_edit_text_block_commands }
-      , sep1:    "---------"
-      , flag:    { name: kivi.t8('Toggle marker'),         icon: "flag",   callback: kivi.requirement_spec.standard_text_block_ajax_call, disabled: kivi.requirement_spec.disable_edit_text_block_commands }
-      , sep2:    "---------"
-      , copy:    { name: kivi.t8('Copy'),                  icon: "copy",   callback: kivi.requirement_spec.standard_text_block_ajax_call, disabled: kivi.requirement_spec.disable_edit_text_block_commands }
-      , paste:   { name: kivi.t8('Paste'),                 icon: "paste",  callback: kivi.requirement_spec.standard_text_block_ajax_call  }
-    }, general_actions)
-  });
-
   events = {
       show: kivi.requirement_spec.item_popup_menu_shown
     , hide: kivi.requirement_spec.item_popup_menu_hidden
@@ -452,32 +490,6 @@ ns.create_context_menus = function() {
       , paste:                  { name: kivi.t8('Paste'),                  icon: "paste",  callback: kivi.requirement_spec.standard_item_ajax_call }
     }, general_actions)
   });
-
-  $.contextMenu({
-    selector: '.time-cost-estimate-context-menu',
-    items:    $.extend({
-        heading: { name: kivi.t8('Time/cost estimate actions'), className: 'context-menu-heading' }
-      , edit:    { name: kivi.t8('Edit'), icon: "edit", callback: kivi.requirement_spec.standard_time_cost_estimate_ajax_call }
-    }, general_actions)
-  });
-
-  $.contextMenu({
-    selector: '.edit-time-cost-estimate-context-menu',
-    items:    $.extend({
-        heading: { name: kivi.t8('Time/cost estimate actions'), className: 'context-menu-heading' }
-      , save:    { name: kivi.t8('Save'),   icon: "save",  callback: kivi.requirement_spec.standard_time_cost_estimate_ajax_call }
-      , cancel:  { name: kivi.t8('Cancel'), icon: "close", callback: kivi.requirement_spec.standard_time_cost_estimate_ajax_call }
-    }, general_actions)
-  });
-
-  $.contextMenu({
-    selector: '.versioned-copy-context-menu',
-    items:    $.extend({
-        heading:            { name: kivi.t8('Version actions'), className: 'context-menu-heading' }
-      , create_version_pdf: { name: kivi.t8('Create PDF'),        icon: "pdf",    callback: kivi.requirement_spec.create_pdf_for_versioned_copy_ajax_call                                                                      }
-      , revert_to_version:  { name: kivi.t8('Revert to version'), icon: "revert", callback: kivi.requirement_spec.revert_to_versioned_copy_ajax_call,     disabled: kivi.requirement_spec.disable_versioned_copy_item_commands }
-    }, general_actions)
-  });
 };
 
 });                             // end of namespace(...., function() {...
index 5c15430..50f6627 100755 (executable)
@@ -152,6 +152,7 @@ $self->{texts} = {
   'Add RFQ'                     => 'Preisanfrage erfassen',
   'Add Request for Quotation'   => 'Anfrage erfassen',
   'Add Requirement Spec'        => 'Pflichtenheft erfassen',
+  'Add Requirement Spec Section Template' => 'Pflichtenheft-Abschnittsvorlage erfassen',
   'Add Sales Delivery Order'    => 'Lieferschein (Verkauf) erfassen',
   'Add Sales Invoice'           => 'Rechnung erfassen',
   'Add Sales Order'             => 'Auftrag erfassen',
@@ -521,6 +522,7 @@ $self->{texts} = {
   'Copy file from #1 to #2 failed: #3' => 'Kopieren der Datei von #1 nach #2 schlug fehl: #3',
   'Copy'                        => 'Kopieren',
   'Copy requirement spec'       => 'Pflichtenheft kopieren',
+  'Copy section template'       => 'Abschnittsvorlage kopieren',
   'Correct taxkey'              => 'Richtiger Steuerschlüssel',
   'Cost'                        => 'Kosten',
   'Costs'                       => 'Kosten',
@@ -551,6 +553,7 @@ $self->{texts} = {
   'Create a new project'        => 'Neues Projekt anlegen',
   'Create a new project type'   => 'Einen neuen Projekttypen anlegen',
   'Create a new requirement spec' => 'Ein neues Pflichtenheft anlegen',
+  'Create a new requirement spec section template' => 'Eine neue Pflichtenheft-Abschnittsvorlage erfassen',
   'Create a new requirement spec status' => 'Einen neuen Pflichtenheftstatus anlegen',
   'Create a new requirement spec type' => 'Einen neuen Pflichtenhefttypen anlegen',
   'Create a new risk level'     => 'Einen neuen Risikograd anlegen',
@@ -727,6 +730,7 @@ $self->{texts} = {
   'Delete links'                => 'Verknüpfungen löschen',
   'Delete profile'              => 'Profil löschen',
   'Delete requirement spec'     => 'Pflichtenheft löschen',
+  'Delete section template'     => 'Abschnittsvorlage löschen',
   'Delete text block'           => 'Textblock löschen',
   'Delete transaction'          => 'Buchung löschen',
   'Deleted'                     => 'Gelöscht',
@@ -910,6 +914,7 @@ $self->{texts} = {
   'Edit requirement spec type'  => 'Pflichtenhefttypen bearbeiten',
   'Edit risk level'             => 'Risikograd bearbeiten',
   'Edit section #1'             => 'Abschnitt #1 bearbeiten',
+  'Edit section template'       => 'Abschnittsvorlage bearbeiten',
   'Edit templates'              => 'Vorlagen bearbeiten',
   'Edit text block'             => 'Textblock bearbeiten',
   'Edit text block \'#1\''      => 'Textblock \'#1\' bearbeiten',
@@ -1865,6 +1870,7 @@ $self->{texts} = {
   'Requested execution date to' => 'Gewünschtes Ausführungsdatum bis',
   'Requests for Quotation'      => 'Preisanfragen',
   'Required by'                 => 'Lieferdatum',
+  'Requirement Spec Section Templates' => 'Pflichtenheft-Abschnittsvorlagen',
   'Requirement Spec Status'     => 'Pflichtenheftstatus',
   'Requirement Spec Statuses'   => 'Pflichtenheftstatus',
   'Requirement Spec Type'       => 'Pflichtenhefttyp',
@@ -1873,6 +1879,7 @@ $self->{texts} = {
   'Requirement spec actions'    => 'Pflichtenheftaktionen',
   'Requirement spec function block #1 with #2 sub function blocks; description: "#3"' => 'Pflichtenheft-Funktionsblock #1 mit #2 Unterfunktionsblöcken; Beschreibung: "#3"',
   'Requirement spec section #1 "#2" with #3 function blocks and a total of #4 sub function blocks; preamble: "#5"' => 'Pflichtenheftabschnitt #1 "#2" mit #3 Funktionsblöcken und insgesamt #4 Unterfunktionsblöcken; Einleitung: "#5"',
+  'Requirement spec section template \'#1\'' => 'Pflichtenheft_Abschnittsvorlage \'#1\'',
   'Requirement spec sub function block #1; description: "#2"' => 'Pflichtenheft-Unterfunktionsblock #1; Beschreibung: "#2"',
   'Requirement spec text block "#1"; content: "#2"' => 'Pflichtenheft-Textblock "1"; Inhalt: "#2"',
   'Requirement specs'           => 'Pflichtenhefte',
@@ -1962,6 +1969,7 @@ $self->{texts} = {
   'Searchable'                  => 'Durchsuchbar',
   'Secondary sorting'           => 'Untersortierung',
   'Section "#1"'                => 'Abschnitt "#1"',
+  'Section template actions'    => 'Abschnittsvorlagen-Aktionen',
   'Section/Function block actions' => 'Abschnitts-/Funktionsblockaktionen',
   'Sections'                    => 'Abschnitte',
   'Select'                      => 'auswählen',
@@ -2389,6 +2397,7 @@ $self->{texts} = {
   'The risk level is in use and cannot be deleted.' => 'Der Risikograd wird verwendet und kann nicht gelöscht werden.',
   'The second reason is that kivitendo allowed the user to enter the tax amount manually regardless of the taxkey used.' => 'Der zweite Grund war, dass kivitendo zuließ, dass die Benutzer beliebige, von den tatsächlichen Steuerschlüsseln unabhängige Steuerbeträge eintrugen.',
   'The second way is to use Perl\'s CPAN module and let it download and install the module for you.' => 'Die zweite Variante besteht darin, Perls CPAN-Modul zu benutzen und es das Modul f&uuml;r Sie installieren zu lassen.',
+  'The section template has been saved.' => 'Die Abschnittsvorlage wurde gespeichert.',
   'The selected bank account does not exist anymore.' => 'Das ausgewählte Bankkonto existiert nicht mehr.',
   'The selected bin does not exist.' => 'Der ausgew&auml;hlte Lagerplatz existiert nicht.',
   'The selected currency'       => 'Die ausgewählte Währung',
index a03f150..41a1ca6 100644 (file)
@@ -35,6 +35,12 @@ ACCESS=project_edit
 module=controller.pl
 action=Project/new
 
+[Master Data--Add Requirement Spec Section Template]
+ACCESS=project_edit
+module=controller.pl
+action=RequirementSpec/new
+is_template=1
+
 [Master Data--Update Prices]
 ACCESS=part_service_assembly_edit
 module=ic.pl
@@ -89,6 +95,11 @@ action=Project/list
 filter.active=active
 filter.valid=valid
 
+[Master Data--Reports--Requirement Spec Section Templates]
+module=controller.pl
+action=RequirementSpec/list
+is_template=1
+
 [AR]
 
 [AR--Add Quotation]
diff --git a/sql/Pg-upgrade2/requirement_specs_section_templates.sql b/sql/Pg-upgrade2/requirement_specs_section_templates.sql
new file mode 100644 (file)
index 0000000..a3bc1e6
--- /dev/null
@@ -0,0 +1,17 @@
+-- @tag: requirement_specs_section_templates
+-- @description: requirement_specs_section_templates
+-- @depends: release_3_0_0 requirement_specs
+-- @charset: utf-8
+
+ALTER TABLE requirement_specs ALTER COLUMN type_id     DROP NOT NULL;
+ALTER TABLE requirement_specs ALTER COLUMN status_id   DROP NOT NULL;
+ALTER TABLE requirement_specs ALTER COLUMN customer_id DROP NOT NULL;
+
+ALTER TABLE requirement_specs
+ADD CONSTRAINT requirement_specs_is_template_or_has_customer_status_type
+CHECK (
+    is_template
+ OR (    (type_id     IS NOT NULL)
+     AND (status_id   IS NOT NULL)
+     AND (customer_id IS NOT NULL))
+);
index 33fc863..a2e5702 100644 (file)
@@ -8,6 +8,7 @@
  <a href="#" onClick="javascript:$('.filter_toggle').toggle()">[% LxERP.t8("Hide Filter") %]</a>
 
  <form method="post" action="controller.pl">
+  [%- L.hidden_tag("is_template", is_template) %]
 
   <p>
    <table class="rs_input_field">
@@ -16,6 +17,7 @@
      <td>[% L.input_tag('filter.title:substr::ilike', filter.title_substr__ilike) %]</td>
     </tr>
 
+[%- UNLESS is_template %]
     <tr>
      <th align="right">[% LxERP.t8("Customer") %]</th>
      <td>[% L.input_tag('filter.customer.name:substr::ilike', filter.customer.name_substr__ilike) %]</td>
@@ -40,6 +42,7 @@
      <th align="right">[% LxERP.t8("Project") %]</th>
      <td>[% L.select_tag('filter.project_id', SELF.projects, default=filter.project_id, title_key="full_description", with_empty=1) %]</td>
     </tr>
+[%- END %]
    </table>
   </p>
 
index 18e99ad..800bb15 100644 (file)
@@ -4,6 +4,7 @@
 %]
 <form method="post" action="controller.pl" id="[% id_prefix %]">
  [% L.hidden_tag("id", SELF.requirement_spec.id, id=id_prefix _ '_id') %]
+ [% L.hidden_tag("requirement_spec.is_template", SELF.requirement_spec.is_template, id=id_prefix _ '_is_template') %]
 
  <table class="rs_input_field">
   <tr>
@@ -11,6 +12,8 @@
    <td>[% L.input_tag("requirement_spec.title", SELF.requirement_spec.title, id=id_prefix _ '_title') %]</td>
   </tr>
 
+[%- UNLESS SELF.requirement_spec.is_template %]
+
   <tr>
    <td>[% LxERP.t8("Requirement Spec Type") %]</td>
    <td>[% L.select_tag("requirement_spec.type_id",  SELF.types, default=SELF.requirement_spec.type_id, title_key="description", id=id_prefix _ '_type_id') %]</td>
@@ -32,6 +35,8 @@
    <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>
 
+[%- END %]
+
  </table>
 
 [%- IF SELF.copy_source %]
@@ -46,7 +51,7 @@
 [% IF submit_as == 'post' %]
   [% L.hidden_tag("action", "RequirementSpec/dispatch", id=id_prefix _ '_action') %]
   [% L.submit_tag("action_" _ (SELF.requirement_spec.id ? "update" : "create"), LxERP.t8('Save'), id=id_prefix _ '_action_update') %]
-  <a href="[% SELF.url_for(action="list") %]">[% LxERP.t8('Abort') %]</a>
+  <a href="[% SELF.url_for(action="list", is_template=SELF.requirement_spec.is_template) %]">[% LxERP.t8('Abort') %]</a>
 [% ELSE %]
   [% L.ajax_submit_tag("controller.pl?action=RequirementSpec/update",  "#" _ id_prefix, LxERP.t8("Save")) %]
 [% END %]
index 0171e5c..5a86a22 100644 (file)
@@ -1,8 +1,12 @@
 [%- USE HTML -%][%- USE LxERP -%]
 <div id="requirement-spec-header">
  <h1>
-  [%- HTML.escape(SELF.requirement_spec.displayable_name('format', 'with_customer')) %]
-  [% LxERP.t8("for") %]
-  [% HTML.escape(SELF.requirement_spec.customer.displayable_name) -%]
+  [%- IF SELF.requirement_spec.is_template %]
+   [%- HTML.escape(LxERP.t8("Requirement spec section template '#1'", SELF.requirement_spec.title)) %]
+  [%- ELSE %]
+   [%- HTML.escape(SELF.requirement_spec.displayable_name('format', 'with_customer')) %]
+   [% LxERP.t8("for") %]
+   [% HTML.escape(SELF.requirement_spec.customer.displayable_name) -%]
+  [%- END %]
  </h1>
 </div>
index 8442d05..c4819fe 100644 (file)
@@ -4,20 +4,24 @@
 
 [%- INCLUDE 'requirement_spec/_header.html' %]
 
-[%- L.hidden_tag('requirement_spec_id', SELF.requirement_spec.id) -%]
+[%- L.hidden_tag('requirement_spec_id', SELF.requirement_spec.id, 'data-is-template'=(SELF.requirement_spec.is_template ? 1 : 0)) -%]
 
 <div class="tabwidget">
  <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>
-  <li><a href="controller.pl?action=RequirementSpecVersion/list&requirement_spec_id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Versions") %]</a></li>
+  [%- UNLESS SELF.requirement_spec.is_template %]
+   <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>
+   <li><a href="controller.pl?action=RequirementSpecVersion/list&requirement_spec_id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Versions") %]</a></li>
+  [%- END %]
  </ul>
 
  <div id="function-blocks-tab">
-  <div id="requirement_spec_version">
+  [%- UNLESS SELF.requirement_spec.is_template %]
+   <div id="requirement_spec_version">
     [%- INCLUDE 'requirement_spec/_version.html' requirement_spec=SELF.requirement_spec -%]
-  </div>
+   </div>
+  [%- END %]
 
   <div id="column-container" class="clearfix">
    <div id="tree-column">
 <script type="text/javascript">
  <!--
 $(function() {
-  var tree_data = [
-    { data:     [% JSON.json(LxERP.t8("Text blocks front")) %],
-      metadata: { type: "text-blocks-front" },
-      attr:     { id: "tb-front", class: "text-block-context-menu" },
-      children: [
-[% FOREACH tb = SELF.requirement_spec.text_blocks_sorted(output_position=0) %]
- [% P.requirement_spec_text_block_jstree_data(tb).json %][% IF !loop.last %],[% END %]
-[% END %]
-      ]},
+  var initially_open = ['sections'
+[%- FOREACH section = SELF.requirement_spec.sections -%]
+        , "fb-[% section.id %]"
+ [%- FOREACH function_block = section.children -%]
+        , "fb-[% function_block.id -%]"
+ [%- END -%]
+[%- END -%]
+  ];
 
-    { data:     [% JSON.json(LxERP.t8("Sections")) %],
+  var tree_data = [{
+      data:     [% JSON.json(LxERP.t8("Sections")) %],
       metadata: { type: "sections" },
       attr:     { id: "sections", class: "section-context-menu" },
       children: [
 [% FOREACH section = SELF.requirement_spec.sections %]
  [% P.requirement_spec_item_jstree_data(section).json %][% IF !loop.last %],[% END %]
 [% END %]
-      ]},
+      ]}];
+
+[% UNLESS SELF.requirement_spec.is_template %]
+  tree_data.unshift({
+      data:     [% JSON.json(LxERP.t8("Text blocks front")) %],
+      metadata: { type: "text-blocks-front" },
+      attr:     { id: "tb-front", class: "text-block-context-menu" },
+      children: [
+[% FOREACH tb = SELF.requirement_spec.text_blocks_sorted(output_position=0) %]
+ [% P.requirement_spec_text_block_jstree_data(tb).json %][% IF !loop.last %],[% END %]
+[% END %]
+      ]});
 
-    { data:     [% JSON.json(LxERP.t8("Text blocks back")) %],
+  tree_data.push({
+      data:     [% JSON.json(LxERP.t8("Text blocks back")) %],
       metadata: { type: "text-blocks-back" },
       attr:     { id: "tb-back", class: "text-block-context-menu" },
       children: [
 [% FOREACH tb = SELF.requirement_spec.text_blocks_sorted(output_position=1) %]
  [% P.requirement_spec_text_block_jstree_data(tb).json %][% IF !loop.last %],[% END %]
 [% END %]
-      ]}];
+      ]});
+
+  initially_open = initially_open.concat(['tb-front', 'tb-back']);
+[% END %]
 
   $('#tree').jstree({
     core: {
       animation: 0,
-      initially_open: [ "tb-front", "tb-back", "sections"
-[%- FOREACH section = SELF.requirement_spec.sections -%]
-        , "fb-[% section.id %]"
- [%- FOREACH function_block = section.children -%]
-        , "fb-[% function_block.id -%]"
- [%- END -%]
-[%- END -%]
-    ]},
+      initially_open: initially_open,
+    },
     json_data: {
       data: tree_data
     },
@@ -102,7 +115,7 @@ $(function() {
   $.jstree._reference("#tree").select_node('#fb-[% SELF.requirement_spec_item.id %]', true);
 [% END %]
 
-  kivi.requirement_spec.create_context_menus();
+  kivi.requirement_spec.create_context_menus([% SELF.requirement_spec.is_template ? 'true' : 'false' %]);
 });
 
   -->