Pflichtenheftversionen anlegen und auflisten
authorMoritz Bunkus <m.bunkus@linet-services.de>
Fri, 19 Apr 2013 09:22:54 +0000 (11:22 +0200)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Tue, 1 Apr 2014 11:03:16 +0000 (13:03 +0200)
12 files changed:
SL/Controller/RequirementSpec.pm
SL/Controller/RequirementSpecItem.pm
SL/Controller/RequirementSpecVersion.pm [new file with mode: 0644]
SL/DB/RequirementSpec.pm
js/locale/de.js
js/requirement_spec.js
locale/de/all
templates/webpages/requirement_spec/_version.html
templates/webpages/requirement_spec/show.html
templates/webpages/requirement_spec_version/_form.html [new file with mode: 0644]
templates/webpages/requirement_spec_version/list.html [new file with mode: 0644]
templates/webpages/requirement_spec_version/new.html [new file with mode: 0644]

index 54518b7..68e060c 100644 (file)
@@ -4,8 +4,6 @@ use strict;
 
 use parent qw(SL::Controller::Base);
 
-use Rose::DB::Object::Helpers;
-
 use SL::ClientJS;
 use SL::Controller::Helper::GetModels;
 use SL::Controller::Helper::Paginated;
@@ -52,6 +50,8 @@ __PACKAGE__->make_sorted(
   type          => t8('Requirement Spec Type'),
   status        => t8('Requirement Spec Status'),
   projectnumber => t8('Project Number'),
+  version       => t8('Version'),
+  mtime         => t8('Last modification'),
 );
 
 #
@@ -191,7 +191,7 @@ sub action_reorder {
 sub setup {
   my ($self) = @_;
 
-  $::auth->assert('config');
+  $::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);
@@ -267,7 +267,7 @@ sub create_or_update {
     }
   })) {
     $::lxdebug->message(LXDebug::WARN(), "Error: " . $db->error);
-    @errors = ($::locale->text('Saving failed. Erro message from the database: #1'), $db->error);
+    @errors = ($::locale->text('Saving failed. Error message from the database: #1'), $db->error);
     return SL::ClientJS->new->error(@errors)->render($self) if $::request->is_ajax;
 
     $self->requirement_spec->id(undef) if $is_new;
@@ -300,7 +300,8 @@ sub setup_db_args_from_filter {
   $args{where} = [
     and => [
       @{ $args{where} || [] },
-      is_template => $self->is_template
+      working_copy_id => undef,
+      is_template     => $self->is_template
     ]];
 
   $self->db_args(\%args);
@@ -314,8 +315,8 @@ sub prepare_report {
   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
   $self->{report} = $report;
 
-  my @columns     = qw(title customer status type projectnumber);
-  my @sortable    = qw(title customer status type projectnumber);
+  my @columns     = qw(title customer status type projectnumber mtime version);
+  my @sortable    = qw(title customer status type projectnumber mtime);
 
   my %column_defs = (
     title         => { obj_link => sub { $self->url_for(action => 'show', id => $_[0]->id, callback => $callback) } },
@@ -325,6 +326,8 @@ sub prepare_report {
                        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->to_kivitendo(precision => 'minute') } },
   );
 
   map { $column_defs{$_}->{text} ||= $::locale->text( $self->get_sort_spec->{$_}->{title} ) } keys %column_defs;
index 7cad811..0a54637 100644 (file)
@@ -25,6 +25,7 @@ use Rose::Object::MakeMethods::Generic
   'scalar --get_set_init' => [ qw(complexities risks) ],
 );
 
+__PACKAGE__->run_before('check_auth');
 __PACKAGE__->run_before('load_requirement_spec_item', only => [ qw(dragged_and_dropped ajax_update ajax_edit ajax_delete ajax_flag ajax_copy) ]);
 __PACKAGE__->run_before('init_visible_section');
 
@@ -469,6 +470,11 @@ sub action_ajax_paste {
 # filters
 #
 
+sub check_auth {
+  my ($self) = @_;
+  $::auth->assert('sales_quotation_edit');
+}
+
 sub load_requirement_spec {
   my ($self) = @_;
   $self->requirement_spec(SL::DB::RequirementSpec->new(id => $::form->{requirement_spec_id})->load || die "No such requirement spec");
diff --git a/SL/Controller/RequirementSpecVersion.pm b/SL/Controller/RequirementSpecVersion.pm
new file mode 100644 (file)
index 0000000..0588847
--- /dev/null
@@ -0,0 +1,183 @@
+package SL::Controller::RequirementSpecVersion;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use Carp;
+use List::MoreUtils qw(any);
+
+use SL::ClientJS;
+use SL::DB::Customer;
+use SL::DB::Project;
+use SL::DB::RequirementSpec;
+use SL::DB::RequirementSpecVersion;
+use SL::Helper::Flash;
+use SL::Locale::String;
+
+use Rose::Object::MakeMethods::Generic
+(
+  'scalar --get_set_init' => [ qw(requirement_spec version js versioned_copies) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+
+#
+# actions
+#
+
+sub action_list {
+  my ($self, %params) = @_;
+
+  $self->render('requirement_spec_version/list', { layout => 0 });
+}
+
+sub action_new {
+  my ($self) = @_;
+
+  $self->version(SL::DB::RequirementSpecVersion->new);
+
+  my $previous_version = $self->requirement_spec->previous_version;
+  my %differences      = $self->calculate_differences(current => $self->requirement_spec, previous => $previous_version);
+
+  if (!$previous_version) {
+    $self->version->description(t8('Initial version.'));
+
+  } else {
+    my @lines;
+
+    my $fb_diff = $differences{function_blocks};
+    push @lines, t8('Added sections and function blocks: #1',   $::locale->language_join([ map { $_->fb_number         } @{ $fb_diff->{additions} } ])) if @{ $fb_diff->{additions} };
+    push @lines, t8('Changed sections and function blocks: #1', $::locale->language_join([ map { $_->fb_number         } @{ $fb_diff->{changes}   } ])) if @{ $fb_diff->{changes}   };
+    push @lines, t8('Removed sections and function blocks: #1', $::locale->language_join([ map { $_->fb_number         } @{ $fb_diff->{removals}  } ])) if @{ $fb_diff->{removals}  };
+
+    my $tb_diff = $differences{text_blocks};
+    push @lines, t8('Added text blocks: #1',                    $::locale->language_join([ map { '"' . $_->title . '"' } @{ $tb_diff->{additions} } ])) if @{ $tb_diff->{additions} };
+    push @lines, t8('Changed text blocks: #1',                  $::locale->language_join([ map { '"' . $_->title . '"' } @{ $tb_diff->{changes}   } ])) if @{ $tb_diff->{changes}   };
+    push @lines, t8('Removed text blocks: #1',                  $::locale->language_join([ map { '"' . $_->title . '"' } @{ $tb_diff->{removals}  } ])) if @{ $tb_diff->{removals}  };
+
+    $self->version->description(@lines ? join("\n", @lines) : t8('No changes since previous version.'));
+  }
+
+  $self->render('requirement_spec_version/new', { layout => 0 }, title => t8('Create a new version'));
+}
+
+sub action_create {
+  my ($self, %params) = @_;
+
+  my %attributes = %{ delete($::form->{rs_version}) || {} };
+  my @errors     = SL::DB::RequirementSpecVersion->new(%attributes, version_number => 1)->validate;
+
+  return $self->js->error(@errors)->render($self) if @errors;
+
+  my $db     = $self->requirement_spec->db;
+  my @result = $self->version($self->requirement_spec->create_version(%attributes));
+
+  if (!@result) {
+    $::lxdebug->message(LXDebug::WARN(), "Error: " . $db->error);
+    return $self->js->error($::locale->text('Saving failed. Error message from the database: #1'), $db->error)->render($self);
+  }
+
+  my $html = $self->render('requirement_spec/_version', { output => 0 }, requirement_spec => $self->requirement_spec);
+
+  $self->js
+    ->html('#requirement_spec_version', $html)
+    ->jqmClose('.jqmWindow')
+    ->render($self);
+}
+
+#
+# filters
+#
+
+sub check_auth {
+  my ($self, %params) = @_;
+  $::auth->assert('sales_quotation_edit');
+}
+
+#
+# helpers
+#
+
+sub init_requirement_spec {
+  my ($self) = @_;
+  $self->requirement_spec(SL::DB::RequirementSpec->new(id => $::form->{requirement_spec_id})->load) if $::form->{requirement_spec_id};
+}
+
+sub init_version {
+  my ($self) = @_;
+  $self->version(SL::DB::RequirementSpecVersion->new(id => $::form->{id})->load) if $::form->{id};
+}
+
+sub init_js {
+  my ($self, %params) = @_;
+  $self->js(SL::ClientJS->new);
+}
+
+sub init_versioned_copies {
+  my ($self) = @_;
+  $self->versioned_copies([
+    sort { $b->mtime <=> $a->mtime } @{ $self->requirement_spec->versioned_copies }
+  ]);
+}
+
+sub has_item_changed {
+  my ($previous, $current) = @_;
+  croak "Missing previous/current" if !$previous || !$current;
+  return any { ($previous->$_ || '') ne ($current->$_ || '') } qw(item_type parent_id fb_number title description complexity_id risk_id time_estimation net_sum);
+}
+
+sub has_text_block_changed {
+  my ($previous, $current) = @_;
+  croak "Missing previous/current" if !$previous || !$current;
+  return any { ($previous->$_ || '') ne ($current->$_ || '') } qw(title text);
+}
+
+sub compare_items {
+  return -1 if ($a->item_type eq 'section') && ($b->item_type ne 'section');
+  return +1 if ($a->item_type ne 'section') && ($b->item_type eq 'section');
+  return $a->fb_number cmp $b->fb_number;
+}
+
+sub calculate_differences {
+  my ($self, %params) = @_;
+
+  my %differences = (
+    function_blocks => {
+      additions => [],
+      changes   => [],
+      removals  => [],
+    },
+    text_blocks => {
+      additions => [],
+      changes   => [],
+      removals  => [],
+    },
+  );
+
+  return %differences if !$params{previous} || !$params{current};
+
+  my @previous_items                         = sort compare_items @{ $params{previous}->items };
+  my @current_items                          = sort compare_items @{ $params{current}->items  };
+
+  my @previous_text_blocks                   = sort { lc $a->title cmp lc $b->title } @{ $params{previous}->text_blocks };
+  my @current_text_blocks                    = sort { lc $a->title cmp lc $b->title } @{ $params{current}->text_blocks  };
+
+  my %previous_items_map                     = map { $_->fb_number => $_ } @previous_items;
+  my %current_items_map                      = map { $_->fb_number => $_ } @current_items;
+
+  my %previous_text_blocks_map               = map { $_->title     => $_ } @previous_text_blocks;
+  my %current_text_blocks_map                = map { $_->title     => $_ } @current_text_blocks;
+
+  $differences{function_blocks}->{additions} = [ grep { !$previous_items_map{ $_->fb_number }                                                                         } @current_items        ];
+  $differences{function_blocks}->{removals}  = [ grep { !$current_items_map{  $_->fb_number }                                                                         } @previous_items       ];
+  $differences{function_blocks}->{changes}   = [ grep {  $previous_items_map{ $_->fb_number }       && has_item_changed($previous_items_map{ $_->fb_number }, $_)     } @current_items        ];
+
+  $differences{text_blocks}->{additions}     = [ grep { !$previous_text_blocks_map{ $_->title }                                                                       } @current_text_blocks  ];
+  $differences{text_blocks}->{removals}      = [ grep { !$current_text_blocks_map{  $_->title }                                                                       } @previous_text_blocks ];
+  $differences{text_blocks}->{changes}       = [ grep {  $previous_text_blocks_map{ $_->title } && has_text_block_changed($previous_text_blocks_map{ $_->title }, $_) } @current_text_blocks  ];
+
+  return %differences;
+}
+
+1;
index f21e513..e2ac8b1 100644 (file)
@@ -10,15 +10,20 @@ use SL::DB::Manager::RequirementSpec;
 use SL::Locale::String;
 
 __PACKAGE__->meta->add_relationship(
-  items          => {
-    type         => 'one to many',
-    class        => 'SL::DB::RequirementSpecItem',
-    column_map   => { id => 'requirement_spec_id' },
+  items            => {
+    type           => 'one to many',
+    class          => 'SL::DB::RequirementSpecItem',
+    column_map     => { id => 'requirement_spec_id' },
   },
-  text_blocks    => {
-    type         => 'one to many',
-    class        => 'SL::DB::RequirementSpecTextBlock',
-    column_map   => { id => 'requirement_spec_id' },
+  text_blocks      => {
+    type           => 'one to many',
+    class          => 'SL::DB::RequirementSpecTextBlock',
+    column_map     => { id => 'requirement_spec_id' },
+  },
+  versioned_copies => {
+    type           => 'one to many',
+    class          => 'SL::DB::RequirementSpec',
+    column_map     => { id => 'working_copy_id' },
   },
 );
 
@@ -119,4 +124,53 @@ sub _create_copy {
   return $copy;
 }
 
+sub previous_version {
+  my ($self) = @_;
+
+  my $and    = $self->version_id ? " AND (version_id <> ?)" : "";
+  my $id     = $self->db->dbh->selectrow_array(<<SQL, undef, $self->id, ($self->version_id) x !!$self->version_id);
+   SELECT MAX(id)
+   FROM requirement_specs
+   WHERE (working_copy_id = ?) $and
+SQL
+
+  return $id ? SL::DB::RequirementSpec->new(id => $id)->load : undef;
+}
+
+sub is_working_copy {
+  my ($self) = @_;
+
+  return !$self->working_copy_id;
+}
+
+sub next_version_number {
+  my ($self) = @_;
+  my $max_number = $self->db->dbh->selectrow_array(<<SQL, undef, $self->id);
+    SELECT COALESCE(MAX(ver.version_number), 0)
+    FROM requirement_spec_versions ver
+    JOIN requirement_specs rs ON (rs.version_id = ver.id)
+    WHERE rs.working_copy_id = ?
+SQL
+
+  return $max_number + 1;
+}
+
+sub create_version {
+  my ($self, %attributes) = @_;
+
+  my ($copy, $version);
+  my $ok = $self->db->do_transaction(sub {
+    delete $attributes{version_number};
+
+    $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number)->save;
+    $copy    = $self->create_copy;
+    $copy->update_attributes(version_id => $version->id, working_copy_id => $self->id);
+    $self->update_attributes(version_id => $version->id);
+
+    1;
+  });
+
+  return $ok ? ($copy, $version) : ();
+}
+
 1;
index f7b85de..704abd7 100644 (file)
@@ -7,6 +7,7 @@ namespace("kivi").setupLocale({
 "Are you sure?":"Sind Sie sicher?",
 "Copy":"Kopieren",
 "Copy requirement spec":"Pflichtenheft kopieren",
+"Create new version":"Neue Version anlegen",
 "Database Connection Test":"Test der Datenbankverbindung",
 "Delete":"Löschen",
 "Delete requirement spec":"Pflichtenheft löschen",
index b157008..4ae545e 100644 (file)
@@ -313,20 +313,34 @@ function delete_reqspec(key, opt) {
   return true;
 }
 
+function disable_requirement_spec_commands(key, opt) {
+  if (key === "create_version")
+    return ($('#current_version_id').val() || '') == '' ? false : true;
+  return false;
+}
+
+// -------------------------------------------------------------------------
+// -------------------------------- versions -------------------------------
+// -------------------------------------------------------------------------
+
+function create_requirement_spec_version() {
+  open_jqm_window({ url:  'controller.pl',
+                    data: { action:              'RequirementSpecVersion/new',
+                            requirement_spec_id: $('#requirement_spec_id').val() },
+                    id:   'new_requirement_spec_version_window' });
+  return true;
+}
+
 // -------------------------------------------------------------------------
 // ----------------------------- context menus -----------------------------
 // -------------------------------------------------------------------------
 
 function create_requirement_spec_context_menus() {
-  var events = {
-    show: requirement_spec_text_block_popup_menu_shown,
-    hide: requirement_spec_text_block_popup_menu_hidden
-  };
-
   var general_actions = {
       sep98:           "---------"
     , general_actions: { name: kivi.t8('Requirement spec actions:') }
     , sep99:           "---------"
+    , create_version:  { name: kivi.t8('Create new version'),      icon: "new",    callback: create_requirement_spec_version, disabled: disable_requirement_spec_commands }
     , copy_reqspec:    { name: kivi.t8('Copy requirement spec'),   icon: "copy",   callback: copy_reqspec   }
     , delete_reqspec:  { name: kivi.t8('Delete requirement spec'), icon: "delete", callback: delete_reqspec }
   };
@@ -336,6 +350,11 @@ function create_requirement_spec_context_menus() {
     items:    general_actions
   });
 
+  var events = {
+    show: requirement_spec_text_block_popup_menu_shown,
+    hide: requirement_spec_text_block_popup_menu_hidden
+  };
+
   $.contextMenu({
     selector: '.text-block-context-menu',
     events:   {
index 0a80204..61c3d8e 100755 (executable)
@@ -179,6 +179,8 @@ $self->{texts} = {
   'Add sub function block'      => 'Unterfunktionsblock hinzufügen',
   'Add text block'              => 'Textblock erfassen',
   'Add unit'                    => 'Einheit hinzuf&uuml;gen',
+  'Added sections and function blocks: #1' => 'Hinzugefügte Abschnitte und Funktionsblöcke: #1',
+  'Added text blocks: #1'       => 'Hinzugefügte Textblöcke: #1',
   'Address'                     => 'Adresse',
   'Admin'                       => 'Administration',
   'Administration'              => 'Administration',
@@ -436,6 +438,9 @@ $self->{texts} = {
   'Cc E-mail'                   => 'CC (E-Mail)',
   'Change default bin for this parts' => 'Standardlagerplatz für diese Waren ändern',
   'Change kivitendo installation settings (most entries in the \'System\' menu)' => 'Verändern der kivitendo-Installationseinstellungen (die meisten Menüpunkte unterhalb von \'System\')',
+  'Change representative to'    => 'Vertreter ändern in',
+  'Changed sections and function blocks: #1' => 'Geänderte Abschnitte und Funktionsblöcke: #1',
+  'Changed text blocks: #1'     => 'Geänderte Textblöcke: #1',
   'Changes in this block are only sensible if the account is NOT a summary account AND there exists one valid taxkey. To select both Receivables and Payables only make sense for Payment / Receipt (i.e. account cash).' => 'Es ist nur sinnvoll Änderungen vorzunehmen, wenn das Konto KEIN Sammelkonto ist und wenn ein gültiger Steuerschlüssel für das Konto existiert. Gleichzeitig Haken bei Forderungen und Verbindlichkeiten zu setzen, macht auch NUR für den Zahlungsein- und Ausgang (bspw. Bank oder Kasse) Sinn.',
   'Changes to Receivables and Payables are only possible if no transactions to this account are posted yet.' => 'Änderungen bei Forderungen oder Verbindlichkeiten sind nur möglich, wenn dieses Konto noch nicht bebucht wurde.',
   'Charge Number'               => 'Chargennummer',
@@ -549,6 +554,8 @@ $self->{texts} = {
   'Create a new section'        => 'Neuen Abschnitt erstellen',
   'Create a new user'           => 'Einen neuen Benutzer anlegen',
   'Create a new user group'     => 'Eine neue Benutzergruppe erfassen',
+  'Create a new version'        => 'Eine neue Version anlegen',
+  'Create a standard group'     => 'Eine Standard-Benutzergruppe anlegen',
   'Create and edit RFQs'        => 'Lieferantenanfragen erfassen und bearbeiten',
   'Create and edit dunnings'    => 'Mahnungen erfassen und bearbeiten',
   'Create and edit invoices and credit notes' => 'Rechnungen und Gutschriften erfassen und bearbeiten',
@@ -576,6 +583,7 @@ $self->{texts} = {
   'Create new payment term'     => 'Neue Zahlungsbedingung anlegen',
   'Create new project type'     => 'Neuen Projekttypen anlegen',
   'Create new templates from master templates' => 'Neue Druckvorlagen aus Vorlagensatz erstellen',
+  'Create new version'          => 'Neue Version anlegen',
   'Create one from the context menu by right-clicking on this text.' => 'Erstellen Sie einen aus dem Kontextmenü, indem Sie auf diesen Text rechtsklicken.',
   'Create tables'               => 'Tabellen anlegen',
   'Created by'                  => 'Erstellt von',
@@ -1183,6 +1191,7 @@ $self->{texts} = {
   'Increase'                    => 'Erhöhen',
   'Individual Items'            => 'Einzelteile',
   'Information'                 => 'Information',
+  'Initial version.'            => 'Initiale Version.',
   'Insert'                      => 'Einfügen',
   'Insert with new customer/vendor number' => 'Mit neuer Kunden-/Lieferantennummer anlegen',
   'Insert with new database ID' => 'Neu anlegen mit neuer Datenbank-ID',
@@ -1190,6 +1199,8 @@ $self->{texts} = {
   'Interest'                    => 'Zinsen',
   'Interest Rate'               => 'Zinssatz',
   'Internal Notes'              => 'Interne Bemerkungen',
+  'Internal comment'            => 'Interne Bemerkungen',
+  'International'               => 'Ausland',
   'Internet'                    => 'Internet',
   'Introduction of clients'     => 'Einführung von Mandanten',
   'Inv. Duedate'                => 'Rg. Fälligkeit',
@@ -1282,6 +1293,7 @@ $self->{texts} = {
   'Last Transaction'            => 'Letzte Buchung',
   'Last Vendor Number'          => 'Letzte Lieferantennummer',
   'Last command output'         => 'Ausgabe des letzten Befehls',
+  'Last modification'           => 'Letzte Änderung',
   'Last opening balance or all transactions' => 'Letzte Eröffnungsbuchung oder alle Buchungen',
   'Last opening balance or start of year' => 'Letzte Eröffnungsbuchung oder Jahresanfang',
   'Last run at'                 => 'Letzte Ausführung um',
@@ -1439,6 +1451,7 @@ $self->{texts} = {
   'No bins have been added to this warehouse yet.' => 'Es wurden zu diesem Lager noch keine Lagerpl&auml;tze angelegt.',
   'No business has been created yet.' => 'Es wurden noch kein Kunden-/Lieferantentyp erfasst.',
   'No clients have been created yet.' => 'Es wurden noch keine Mandanten angelegt.',
+  'No changes since previous version.' => 'Keine Änderungen seit der letzten Version.',
   'No complexities has been created yet.' => 'Es wurden noch keine Komplexitätsgrade angelegt.',
   'No contact selected to delete' => 'Keine Ansprechperson zum Löschen ausgewählt',
   'No customer has been selected yet.' => 'Es wurde noch kein Kunde ausgewählt.',
@@ -1821,7 +1834,9 @@ $self->{texts} = {
   'Remove'                      => 'Entfernen',
   'Remove Draft'                => 'Entwurf l&ouml;schen',
   'Remove draft when posting'   => 'Entwurf beim Buchen l&ouml;schen',
+  'Removed sections and function blocks: #1' => 'Entfernte Abschnitte und Funktionsblöcke: #1',
   'Removed spoolfiles!'         => 'Druckdateien entfernt!',
+  'Removed text blocks: #1'     => 'Entfernte Textblöcke: #1',
   'Removing marked entries from queue ...' => 'Markierte Einträge werden von der Warteschlange entfernt ...',
   'Replace the orphaned currencies by other not orphaned currencies. To do so, please delete the currency in the textfields above and replace it by another currency. You could loose or change unintentionally exchangerates. Go on very carefully since you could destroy transactions.' => 'Ersetze die Währungen durch andere gültige Währungen. Wenn Sie sich hierfür entscheiden, ersetzen Sie bitte alle Währungen, die oben angegeben sind, durch Währungen, die in Ihrem System ordnungsgemäß eingetragen sind. Alle eingetragenen Wechselkurse für die verwaiste Währung werden dabei gelöscht. Bitte gehen Sie sehr vorsichtig vor, denn die betroffenen Buchungen können unter Umständen kaputt gehen.',
   'Report Positions'            => 'Berichte',
@@ -1928,7 +1943,7 @@ $self->{texts} = {
   'Save draft'                  => 'Entwurf speichern',
   'Save profile'                => 'Profil speichern',
   'Save settings as'            => 'Einstellungen speichern unter',
-  'Saving failed. Erro message from the database: #1' => 'Speichern schlug fehl. Fehlermeldung der Datenbank: #1',
+  'Saving failed. Error message from the database: #1' => 'Speichern schlug fehl. Fehlermeldung der Datenbank: #1',
   'Saving the file \'%s\' failed. OS error message: %s' => 'Das Speichern der Datei \'%s\' schlug fehl. Fehlermeldung des Betriebssystems: %s',
   'Screen'                      => 'Bildschirm',
   'Search'                      => 'Suchen',
@@ -2610,12 +2625,15 @@ $self->{texts} = {
   'Vendor missing!'             => 'Lieferant fehlt!',
   'Vendor not on file or locked!' => 'Dieser Lieferant existiert nicht oder ist gesperrt.',
   'Vendor not on file!'         => 'Lieferant ist nicht in der Datenbank!',
-  'Vendor saved'                => 'Lieferant gespeichert',
   'Vendor saved!'               => 'Lieferant gespeichert!',
+  'Vendor saved'                => 'Lieferant gespeichert',
   'Vendor type'                 => 'Lieferantentyp',
   'Vendors'                     => 'Lieferanten',
   'Verrechnungseinheit'         => 'Verrechnungseinheit',
+  'Version number'              => 'Versionsnummer',
   'Version'                     => 'Version',
+  'Versions'                    => 'Versionen',
+  'Vertreter'                   => 'Vertreter',
   'View SEPA export'            => 'SEPA-Export-Details ansehen',
   'View background job execution result' => 'Verlauf der Hintergrund-Job-Ausführungen anzeigen',
   'View background job history' => 'Hintergrund-Job-Verlauf anzeigen',
@@ -2652,7 +2670,9 @@ $self->{texts} = {
   'Workflow request_quotation'  => 'Workflow Preisanfrage',
   'Workflow sales_order'        => 'Workflow Auftrag',
   'Workflow sales_quotation'    => 'Workflow Angebot',
+  'Working copy identical to version number #1' => 'Mit Versionsnummer #1 identische Arbeitskopie',
   'Working copy without version' => 'Arbeitskopie ohne Version',
+  'Working copy; no description yet' => 'Arbeitskopie; noch keine Beschreibung',
   'Write bin to default bin in part?' => 'Diesen Lagerplatz als Standardlagerplatz im Artikel setzen?',
   'Wrong tax keys recorded'     => 'Gespeicherte Steuerschlüssel sind falsch',
   'Wrong taxes recorded'        => 'Gespeicherte Steuern passen nicht zum Steuerschlüssel',
index 8a73f54..f6d9ca3 100644 (file)
@@ -1,9 +1,9 @@
 [%- USE L -%][%- USE LxERP -%][%- USE HTML -%]
-[% L.stuff %]
+[% L.hidden_tag('current_version_id', requirement_spec.version_id) %]
 [% LxERP.t8("Current version") %]:
 [% IF !requirement_spec.version_id %]
  [% LxERP.t8("Working copy without version") %]
 [% ELSE %]
  [% LxERP.t8("Version") %] [% HTML.escape(requirement_spec.version.version_number) %]
- [% LxERP.t8("dated") %] [% HTML.escape(requirement_spec.version.itime.displayable_date) %]
+ [% LxERP.t8("dated") %] [% HTML.escape(requirement_spec.version.itime.to_kivitendo(precision='minute')) %]
 [%- END -%]
index 4cae971..395e394 100644 (file)
@@ -11,6 +11,7 @@
   <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>
  </ul>
 
  <div id="function-blocks-tab">
diff --git a/templates/webpages/requirement_spec_version/_form.html b/templates/webpages/requirement_spec_version/_form.html
new file mode 100644 (file)
index 0000000..a100c33
--- /dev/null
@@ -0,0 +1,44 @@
+[%- USE LxERP -%][%- USE L -%]
+[%- DEFAULT id_prefix = 'edit_version_form'
+            submit_as = 'post'
+%]
+<form method="post" action="controller.pl" id="[% id_prefix %]">
+ [% L.hidden_tag("requirement_spec_id", SELF.requirement_spec.id, id=id_prefix _ '_id') %]
+ [% L.hidden_tag("id", SELF.version.id, id=id_prefix _ '_id') %]
+
+ <table class="rs_input_field">
+  <tr>
+   <td>[% LxERP.t8("Version") %]</td>
+   <td>[% SELF.requirement_spec.next_version_number %]</td>
+  </tr>
+
+  <tr>
+   <td>[% LxERP.t8("Description") %]</td>
+   <td>[% L.textarea_tag("rs_version.description",  SELF.version.description, cols=80, rows=8, id=id_prefix _ '_description') %]</td>
+  </tr>
+
+  <tr>
+   <td>[% LxERP.t8("Internal comment") %]</td>
+   <td>[% L.textarea_tag("rs_version.comment",  SELF.version.comment, cols=80, rows=8, id=id_prefix _ '_comment') %]</td>
+  </tr>
+
+ </table>
+
+ <p>
+[%- IF SELF.version.id %]
+  [% L.ajax_submit_tag("controller.pl?action=RequirementSpecVersion/update",  "#" _ id_prefix, LxERP.t8("Save")) %]
+  <a href="[% SELF.url_for(action='list') %]">[%- LxERP.t8("Cancel") %]</a>
+[%- ELSE %]
+  [% L.ajax_submit_tag("controller.pl?action=RequirementSpecVersion/create",  "#" _ id_prefix, LxERP.t8("Save")) %]
+  <a href="#" onclick="$('.jqmWindow').jqmClose();">[% LxERP.t8("Cancel") %]</a>
+[%- END %]
+ </p>
+</form>
+
+<script type="text/javascript">
+<!--
+$(function() {
+  $('#[% id_prefix %]_description').focus();
+});
+ -->
+</script>
diff --git a/templates/webpages/requirement_spec_version/list.html b/templates/webpages/requirement_spec_version/list.html
new file mode 100644 (file)
index 0000000..7c4f4d5
--- /dev/null
@@ -0,0 +1,36 @@
+[% USE HTML %][% USE L %][% USE LxERP %][%- USE P -%]
+
+<table>
+ <thead>
+  <tr class="listheading">
+   <th>[%- LxERP.t8("Version number") %]</th>
+   <th>[%- LxERP.t8("Description") %]</th>
+   <th>[%- LxERP.t8("Internal comment") %]</th>
+   <th>[%- LxERP.t8("Last modification") %]</th>
+  </tr>
+ </thead>
+
+ <tbody>
+  <tr class="listrow">
+   <td>
+    [%- IF SELF.requirement_spec.version_id %]
+     [%- LxERP.t8("Working copy identical to version number #1", SELF.requirement_spec.version.version_number) %]
+    [%- ELSE %]
+     [%- LxERP.t8("Working copy without version") %]
+    [%- END -%]
+   </td>
+   <td>[%- LxERP.t8("Working copy; no description yet") %]</td>
+   <td>&nbsp;</td>
+   <td>[% SELF.requirement_spec.mtime.to_kivitendo(precision='minute') %]</td>
+  </tr>
+
+  [%- FOREACH versioned = SELF.versioned_copies %]
+   <tr class="listrow versioned_copy_context_menu">
+    <td>[% HTML.escape(versioned.version.version_number) %]</td>
+    <td>[% HTML.escape(P.truncate(versioned.description)) %]</td>
+    <td>[% HTML.escape(P.truncate(versioned.comment)) %]</td>
+    <td>[% versioned.mtime.to_kivitendo(precision='minute') %]</td>
+   </tr>
+  [%- END %]
+ </tbody>
+</table>
diff --git a/templates/webpages/requirement_spec_version/new.html b/templates/webpages/requirement_spec_version/new.html
new file mode 100644 (file)
index 0000000..ebcca87
--- /dev/null
@@ -0,0 +1,5 @@
+<h1>[% title %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+[%- INCLUDE 'requirement_spec_version/_form.html' %]