From: Moritz Bunkus Date: Wed, 30 Jul 2014 12:40:06 +0000 (+0200) Subject: Pflichtenhefte: zusätzliche Artikel zuweisen und bearbeiten können X-Git-Tag: release-3.2.0beta~333 X-Git-Url: http://wagnertech.de/git?a=commitdiff_plain;h=0c3193511e1ea50fad793887ede6ac5732d85074;p=kivitendo-erp.git Pflichtenhefte: zusätzliche Artikel zuweisen und bearbeiten können --- diff --git a/SL/Controller/RequirementSpecOrder.pm b/SL/Controller/RequirementSpecOrder.pm index 8c33d6ffd..7671ffb00 100644 --- a/SL/Controller/RequirementSpecOrder.pm +++ b/SL/Controller/RequirementSpecOrder.pm @@ -70,10 +70,7 @@ sub action_create { $order->db->with_transaction(sub { $order->save; - $self->requirement_spec->orders( - @{ $self->requirement_spec->orders }, - SL::DB::RequirementSpecOrder->new(order => $order, version => $self->requirement_spec->version) - ); + $self->requirement_spec->add_orders(SL::DB::RequirementSpecOrder->new(order => $order, version => $self->requirement_spec->version)); $self->requirement_spec->save; $self->requirement_spec->link_to_record($order); @@ -247,11 +244,6 @@ sub init_all_parts_time_unit { # helpers # -sub load_parts_for_sections { - my ($self, %params) = @_; - -} - sub create_order_item { my ($self, %params) = @_; diff --git a/SL/Controller/RequirementSpecPart.pm b/SL/Controller/RequirementSpecPart.pm new file mode 100644 index 000000000..b9df7f545 --- /dev/null +++ b/SL/Controller/RequirementSpecPart.pm @@ -0,0 +1,119 @@ +package SL::Controller::RequirementSpecPart; + +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::RequirementSpecPart; +use SL::Helper::Flash; +use SL::Locale::String; + +use Rose::Object::MakeMethods::Generic +( + 'scalar --get_set_init' => [ qw(requirement_spec js) ], +); + +__PACKAGE__->run_before('check_auth'); + +# +# actions +# + +sub action_show { + my ($self, %params) = @_; + + $self->render('requirement_spec_part/show', { layout => 0 }); +} + +sub action_ajax_edit { + my ($self, %params) = @_; + + my $html = $self->render('requirement_spec_part/_edit', { output => 0 }); + + $self->js + ->hide('#additional_parts_list_container') + ->after('#additional_parts_list_container', $html) + ->on('#edit_additional_parts_form INPUT[type=text]', 'keydown', 'kivi.requirement_spec.additional_parts_input_key_down') + ->focus('#additional_parts_add_part_id_name') + ->run('kivi.requirement_spec.prepare_edit_additional_parts_form') + ->reinit_widgets + ->render; +} + +sub action_ajax_add { + my ($self) = @_; + + my $part = SL::DB::Part->new(id => $::form->{part_id})->load(with_objects => [ qw(unit_obj) ]); + my $rs_part = SL::DB::RequirementSpecPart->new( + part => $part, + qty => 1, + unit => $part->unit_obj, + description => $part->description, + ); + my $row = $self->render('requirement_spec_part/_part', { output => 0 }, part => $rs_part); + + $self->js + ->val( '#additional_parts_add_part_id', '') + ->val( '#additional_parts_add_part_id_name', '') + ->focus('#additional_parts_add_part_id_name') + ->append('#edit_additional_parts_list tbody', $row) + ->hide('#edit_additional_parts_list_empty') + ->show('#edit_additional_parts_list') + ->render; +} + +sub action_ajax_save { + my ($self) = @_; + + my $db = $self->requirement_spec->db; + $db->do_transaction(sub { + # Make Emacs happy + 1; + my $parts = $::form->{additional_parts} || []; + my $position = 1; + $_->{position} = $position++ for @{ $parts }; + + $self->requirement_spec->update_attributes(parts => $parts)->load; + + 1; + }) or do { + return $self->js->error(t8('Saving failed. Error message from the database: #1', $db->error))->render; + }; + + my $html = $self->render('requirement_spec_part/show', { output => 0 }, initially_hidden => !!$::form->{keep_open}); + + $self->js + ->replaceWith('#additional_parts_list_container', $html) + ->action_if(!$::form->{keep_open}, 'remove', '#additional_parts_form_container') + ->render; +} + +# +# filters +# + +sub check_auth { + my ($self, %params) = @_; + $::auth->assert('requirement_spec_edit'); +} + +# +# helpers +# + +sub init_js { SL::ClientJS->new(controller => $_[0]) } + +sub init_requirement_spec { + SL::DB::RequirementSpec->new(id => $::form->{requirement_spec_id})->load( + with_objects => [ qw(parts parts.part parts.unit) ], + ); +} + +1; diff --git a/SL/DB/Helper/ALL.pm b/SL/DB/Helper/ALL.pm index d0cb1cb8b..b60ddf66b 100644 --- a/SL/DB/Helper/ALL.pm +++ b/SL/DB/Helper/ALL.pm @@ -83,6 +83,7 @@ use SL::DB::RequirementSpecComplexity; use SL::DB::RequirementSpecDependency; use SL::DB::RequirementSpecItem; use SL::DB::RequirementSpecOrder; +use SL::DB::RequirementSpecPart; use SL::DB::RequirementSpecPicture; use SL::DB::RequirementSpecPredefinedText; use SL::DB::RequirementSpecRisk; diff --git a/SL/DB/Helper/Mappings.pm b/SL/DB/Helper/Mappings.pm index 5acc1c7f4..9695077e7 100644 --- a/SL/DB/Helper/Mappings.pm +++ b/SL/DB/Helper/Mappings.pm @@ -163,6 +163,7 @@ my %kivitendo_package_names = ( requirement_spec_item_dependencies => 'RequirementSpecDependency', requirement_spec_items => 'RequirementSpecItem', requirement_spec_orders => 'RequirementSpecOrder', + requirement_spec_parts => 'RequirementSpecPart', requirement_spec_pictures => 'RequirementSpecPicture', requirement_spec_predefined_texts => 'RequirementSpecPredefinedText', requirement_spec_risks => 'RequirementSpecRisk', diff --git a/SL/DB/Manager/RequirementSpecPart.pm b/SL/DB/Manager/RequirementSpecPart.pm new file mode 100644 index 000000000..aa8253433 --- /dev/null +++ b/SL/DB/Manager/RequirementSpecPart.pm @@ -0,0 +1,15 @@ +# This file has been auto-generated only because it didn't exist. +# Feel free to modify it at will; it will not be overwritten automatically. + +package SL::DB::Manager::RequirementSpecPart; + +use strict; + +use SL::DB::Helper::Manager; +use base qw(SL::DB::Helper::Manager); + +sub object_class { 'SL::DB::RequirementSpecPart' } + +__PACKAGE__->make_manager_methods; + +1; diff --git a/SL/DB/MetaSetup/RequirementSpecPart.pm b/SL/DB/MetaSetup/RequirementSpecPart.pm new file mode 100644 index 000000000..b29ae4204 --- /dev/null +++ b/SL/DB/MetaSetup/RequirementSpecPart.pm @@ -0,0 +1,41 @@ +# This file has been auto-generated. Do not modify it; it will be overwritten +# by rose_auto_create_model.pl automatically. +package SL::DB::RequirementSpecPart; + +use strict; + +use base qw(SL::DB::Object); + +__PACKAGE__->meta->table('requirement_spec_parts'); + +__PACKAGE__->meta->columns( + id => { type => 'serial', not_null => 1 }, + description => { type => 'text', not_null => 1 }, + part_id => { type => 'integer', not_null => 1 }, + position => { type => 'integer', not_null => 1 }, + qty => { type => 'numeric', not_null => 1, precision => 15, scale => 5 }, + requirement_spec_id => { type => 'integer', not_null => 1 }, + unit_id => { type => 'integer', not_null => 1 }, +); + +__PACKAGE__->meta->primary_key_columns([ 'id' ]); + +__PACKAGE__->meta->foreign_keys( + part => { + class => 'SL::DB::Part', + key_columns => { part_id => 'id' }, + }, + + requirement_spec => { + class => 'SL::DB::RequirementSpec', + key_columns => { requirement_spec_id => 'id' }, + }, + + unit => { + class => 'SL::DB::Unit', + key_columns => { unit_id => 'id' }, + }, +); + +1; +; diff --git a/SL/DB/RequirementSpec.pm b/SL/DB/RequirementSpec.pm index e5dae7ebd..235444047 100644 --- a/SL/DB/RequirementSpec.pm +++ b/SL/DB/RequirementSpec.pm @@ -43,6 +43,11 @@ __PACKAGE__->meta->add_relationship( class => 'SL::DB::RequirementSpecOrder', column_map => { id => 'requirement_spec_id' }, }, + parts => { + type => 'one to many', + class => 'SL::DB::RequirementSpecPart', + column_map => { id => 'requirement_spec_id' }, + }, ); __PACKAGE__->meta->initialize; @@ -119,6 +124,14 @@ sub versioned_copies_sorted { return \@copies; } +sub parts_sorted { + my ($self, @rest) = @_; + + croak "This sub is not a writer" if @rest; + + return [ sort { $a->position <=> $b->position } @{ $self->parts } ]; +} + sub create_copy { my ($self, %params) = @_; @@ -519,6 +532,11 @@ column in ascending order. If the C parameter is given then only the text blocks belonging to that C are returned. +=item C + +Returns an array reference of additional parts sorted by their +positional column in ascending order. + =item C Validate values before saving. Returns list or human-readable error diff --git a/SL/DB/RequirementSpecPart.pm b/SL/DB/RequirementSpecPart.pm new file mode 100644 index 000000000..a511d0ba2 --- /dev/null +++ b/SL/DB/RequirementSpecPart.pm @@ -0,0 +1,11 @@ +package SL::DB::RequirementSpecPart; + +use strict; + +use SL::DB::MetaSetup::RequirementSpecPart; +use SL::DB::Manager::RequirementSpecPart; +use SL::DB::Helper::ActsAsList; + +__PACKAGE__->meta->initialize; + +1; diff --git a/js/locale/de.js b/js/locale/de.js index 3b7e86524..e00362dba 100644 --- a/js/locale/de.js +++ b/js/locale/de.js @@ -7,6 +7,7 @@ namespace("kivi").setupLocale({ "Add section":"Abschnitt hinzufügen", "Add sub function block":"Unterfunktionsblock hinzufügen", "Add text block":"Textblock erfassen", +"Additional articles actions":"Aktionen zu zusätzlichen Artikeln", "Are you sure?":"Sind Sie sicher?", "Basic settings actions":"Aktionen zu Grundeinstellungen", "Cancel":"Abbrechen", @@ -44,6 +45,7 @@ namespace("kivi").setupLocale({ "Paste template":"Vorlage einfügen", "Project link actions":"Projektverknüpfungs-Aktionen", "Quotations/Orders actions":"Aktionen für Angebote/Aufträge", +"Remove article":"Artikel entfernen", "Requirement spec actions":"Pflichtenheftaktionen", "Requirement spec template actions":"Pflichtenheftvorlagen-Aktionen", "Revert to version":"Auf Version zurücksetzen", diff --git a/js/requirement_spec.js b/js/requirement_spec.js index 507897167..b1d5ce85f 100644 --- a/js/requirement_spec.js +++ b/js/requirement_spec.js @@ -644,6 +644,121 @@ ns.revert_to_versioned_copy_ajax_call = function(key, opt) { return true; }; +// ------------------------------------------------------------------------- +// -------------------------- time/cost estimate --------------------------- +// ------------------------------------------------------------------------- + +ns.standard_time_cost_estimate_ajax_call = function(key, opt) { + if (key == 'cancel') { + if (confirm(kivi.t8('Do you really want to cancel?'))) { + $('#time_cost_estimate').show(); + $('#time_cost_estimate_form_container').remove(); + } + return true; + } + + var add_data = ''; + if (key == 'save_keep_open') { + key = 'save'; + add_data = 'keep_open=1&'; + } + + var data = "action=RequirementSpec/ajax_" + key + "_time_and_cost_estimate&" + add_data; + + 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, kivi.eval_json_result); + + return true; +}; + +ns.time_cost_estimate_input_key_down = function(event) { + if(event.keyCode == 13) { + event.preventDefault(); + ns.standard_time_cost_estimate_ajax_call('save'); + return false; + } +}; + +// ------------------------------------------------------------------------- +// -------------------------- additional parts ----------------------------- +// ------------------------------------------------------------------------- + +ns.standard_additional_parts_ajax_call = function(key, opt) { + var add_data = ''; + if (key == 'save_keep_open') { + key = 'save'; + add_data = 'keep_open=1&'; + } + + var data = "action=RequirementSpecPart/ajax_" + key + "&" + add_data + 'requirement_spec_id=' + encodeURIComponent($('#requirement_spec_id').val()) + '&'; + + if (key == 'save') + data += $('#edit_additional_parts_form').serialize(); + + $.post("controller.pl", data, kivi.eval_json_result); + + return true; +}; + +ns.prepare_edit_additional_parts_form = function() { + $("#edit_additional_parts_list tbody").sortable({ + distance: 5, + handle: '.dragdrop', + helper: function(event, ui) { + ui.children().each(function() { + $(this).width($(this).width()); + }); + return ui; + } + + }); +}; + +ns.cancel_edit_additional_parts_form = function() { + if (confirm(kivi.t8('Do you really want to cancel?'))) { + $('#additional_parts_list_container').show(); + $('#additional_parts_form_container').remove(); + } + return true; +}; + +ns.additional_parts_input_key_down = function(event) { + if(event.keyCode == 13) { + event.preventDefault(); + ns.standard_additional_parts_ajax_call('save'); + return false; + } +}; + +ns.add_additional_part = function() { + var part_id = $('#additional_parts_add_part_id').val(); + if (!part_id || (part_id == '')) + return false; + + var rspec_id = $('#requirement_spec_id').val(); + var data = 'action=RequirementSpecPart/ajax_add&requirement_spec_id=' + encodeURIComponent(rspec_id) + '&part_id=' + encodeURIComponent(part_id); + + $.post("controller.pl", data, kivi.eval_json_result); + + return true; +}; + +ns.delete_additional_part = function(key, opt) { + opt.$trigger.remove(); + if (!$('#edit_additional_parts_list tbody tr').size()) { + $('#edit_additional_parts_list_empty').show(); + $('#edit_additional_parts_list').hide(); + } + + return true; +}; + // ------------------------------------------------------------------------- // ------------------------------- tab widget ------------------------------ // ------------------------------------------------------------------------- @@ -651,8 +766,9 @@ var content_div_ids_for_tab_headers = { 'tab-header-function-block': 'function-blocks-tab' , 'tab-header-basic-settings': 'ui-tabs-1' , 'tab-header-time-cost-estimate': 'ui-tabs-2' - , 'tab-header-versions': 'ui-tabs-3' - , 'tab-header-quotations-orders': 'ui-tabs-4' + , 'tab-header-additional-parts': 'ui-tabs-3' + , 'tab-header-versions': 'ui-tabs-4' + , 'tab-header-quotations-orders': 'ui-tabs-5' }; ns.tabs_before_activate = function(event, ui) { @@ -804,6 +920,35 @@ ns.create_context_menus = function(is_template) { }, general_actions) }); + $.contextMenu({ + selector: '.additional-parts-context-menu', + items: $.extend({ + heading: { name: kivi.t8('Additional articles actions'), className: 'context-menu-heading' } + , edit: { name: kivi.t8('Edit'), icon: "edit", callback: kivi.requirement_spec.standard_additional_parts_ajax_call } + }, general_actions) + }); + + var additional_parts_actions = { + save: { name: kivi.t8('Save'), icon: "save", callback: kivi.requirement_spec.standard_additional_parts_ajax_call } + , save_keep_open: { name: kivi.t8('Save and keep open'), icon: "save", callback: kivi.requirement_spec.standard_additional_parts_ajax_call } + , cancel: { name: kivi.t8('Cancel'), icon: "close", callback: kivi.requirement_spec.cancel_edit_additional_parts_form } + }; + + $.contextMenu({ + selector: '.edit-additional-parts-context-menu', + items: $.extend({ + heading: { name: kivi.t8('Additional articles actions'), className: 'context-menu-heading' } + }, additional_parts_actions, general_actions) + }); + + $.contextMenu({ + selector: '.edit-additional-parts-row-context-menu', + items: $.extend({ + heading: { name: kivi.t8('Additional articles actions'), className: 'context-menu-heading' } + , delete: { name: kivi.t8('Remove article'), icon: "delete", callback: kivi.requirement_spec.delete_additional_part } + }, additional_parts_actions, general_actions) + }); + $.contextMenu({ selector: '.quotations-and-orders-context-menu,.quotations-and-orders-order-context-menu', items: $.extend({ diff --git a/locale/de/all b/locale/de/all index 2c31b4009..d6664b5fa 100755 --- a/locale/de/all +++ b/locale/de/all @@ -176,6 +176,7 @@ $self->{texts} = { 'Add new currency' => 'Neue Währung hinzufügen', 'Add new custom variable' => 'Neue benutzerdefinierte Variable erfassen', 'Add note' => 'Notiz erfassen', + 'Add part' => 'Artikel hinzufügen', 'Add picture' => 'Bild hinzufügen', 'Add picture to text block' => 'Bild dem Textblock hinzufügen', 'Add section' => 'Abschnitt hinzufügen', @@ -185,6 +186,8 @@ $self->{texts} = { 'Add unit' => 'Einheit hinzufügen', 'Added sections and function blocks: #1' => 'Hinzugefügte Abschnitte und Funktionsblöcke: #1', 'Added text blocks: #1' => 'Hinzugefügte Textblöcke: #1', + 'Additional articles' => 'Zusätzliche Artikel', + 'Additional articles actions' => 'Aktionen zu zusätzlichen Artikeln', 'Address' => 'Adresse', 'Admin' => 'Administration', 'Administration' => 'Administration', @@ -933,6 +936,7 @@ $self->{texts} = { 'Edit Vendor Invoice' => 'Einkaufsrechnung bearbeiten', 'Edit Warehouse' => 'Lager bearbeiten', 'Edit acceptance status' => 'Abnahmestatus bearbeiten', + 'Edit additional articles' => 'Zusätzliche Artikel bearbeiten', 'Edit article/section assignments' => 'Zuweisung Artikel/Abschnitte bearbeiten', 'Edit assignment of articles to sections' => 'Zuweisung Artikel zu Abschnitten bearbeiten', 'Edit background job' => 'Hintergrund-Job bearbeiten', @@ -1520,6 +1524,7 @@ $self->{texts} = { 'No Vendor was found matching the search parameters.' => 'Zu dem Suchbegriff wurde kein Händler gefunden', 'No acceptance statuses has been created yet.' => 'Es wurde noch kein Abnahmestatus angelegt.', 'No action defined.' => 'Keine Aktion definiert.', + 'No articles have been added yet.' => 'Es wurden noch keine Artikel hinzugefügt.', 'No background job has been created yet.' => 'Es wurden noch keine Hintergrund-Jobs angelegt.', 'No bank information has been entered in this customer\'s master data entry. You cannot create bank collections unless you enter bank information.' => 'Für diesen Kunden wurden in seinen Stammdaten keine Kontodaten hinterlegt. Solange dies nicht geschehen ist, können Sie keine Überweisungen für den Lieferanten anlegen.', 'No bank information has been entered in this vendor\'s master data entry. You cannot create bank transfers unless you enter bank information.' => 'Für diesen Lieferanten wurden in seinen Stammdaten keine Kontodaten hinterlegt. Solange dies nicht geschehen ist, können Sie keine Überweisungen für den Lieferanten anlegen.', @@ -1938,6 +1943,7 @@ $self->{texts} = { 'Removal qty' => 'Entnahmemenge', 'Remove' => 'Entfernen', 'Remove Draft' => 'Entwurf löschen', + 'Remove article' => 'Artikel entfernen', 'Remove draft when posting' => 'Entwurf beim Buchen löschen', 'Removed sections and function blocks: #1' => 'Entfernte Abschnitte und Funktionsblöcke: #1', 'Removed spoolfiles!' => 'Druckdateien entfernt!', diff --git a/sql/Pg-upgrade2/requirement_spec_parts.sql b/sql/Pg-upgrade2/requirement_spec_parts.sql new file mode 100644 index 000000000..723eab630 --- /dev/null +++ b/sql/Pg-upgrade2/requirement_spec_parts.sql @@ -0,0 +1,17 @@ +-- @tag: requirement_spec_parts +-- @description: Artikelzuweisung zu Pflichtenheften +-- @depends: release_3_1_0 +CREATE TABLE requirement_spec_parts ( + id SERIAL NOT NULL, + requirement_spec_id INTEGER NOT NULL, + part_id INTEGER NOT NULL, + unit_id INTEGER NOT NULL, + qty NUMERIC(15, 5) NOT NULL, + description TEXT NOT NULL, + position INTEGER NOT NULL, + + PRIMARY KEY (id), + FOREIGN KEY (requirement_spec_id) REFERENCES requirement_specs (id), + FOREIGN KEY (part_id) REFERENCES parts (id), + FOREIGN KEY (unit_id) REFERENCES units (id) +); diff --git a/templates/webpages/requirement_spec/show.html b/templates/webpages/requirement_spec/show.html index c41e87d84..756fbc85c 100644 --- a/templates/webpages/requirement_spec/show.html +++ b/templates/webpages/requirement_spec/show.html @@ -12,6 +12,7 @@
  • [%- LxERP.t8("Content") %]
  • [%- LxERP.t8("Basic settings") %]
  • [%- LxERP.t8("Time and cost estimate") %]
  • +
  • [%- LxERP.t8("Additional articles") %]
  • [%- UNLESS SELF.requirement_spec.is_template %]
  • [%- LxERP.t8("Versions") %]
  • [%- LxERP.t8("Quotations and orders") %]
  • diff --git a/templates/webpages/requirement_spec_part/_edit.html b/templates/webpages/requirement_spec_part/_edit.html new file mode 100644 index 000000000..c1b2795ba --- /dev/null +++ b/templates/webpages/requirement_spec_part/_edit.html @@ -0,0 +1,38 @@ +[%- USE LxERP -%][%- USE L -%][%- USE P -%] +[% SET parts = SELF.requirement_spec.parts_sorted %] + +
    + +

    [% LxERP.t8("Edit additional articles") %]

    + +
    + [% LxERP.t8("Add part") %]: + [% P.part_picker('additional_parts_add_part_id', '', style="width: 300px") %] + [% L.button_tag('kivi.requirement_spec.add_additional_part()', LxERP.t8('Add part')) %] +
    + +
    + + + + + + + + + + + + + + [%- FOREACH part = parts %] + [%- INCLUDE 'requirement_spec_part/_part.html' part=part %] + [%- END %] + + + + [% L.button_tag("kivi.requirement_spec.standard_additional_parts_ajax_call('save')", LxERP.t8("Save")) %] +
    +
    diff --git a/templates/webpages/requirement_spec_part/_part.html b/templates/webpages/requirement_spec_part/_part.html new file mode 100644 index 000000000..6136de007 --- /dev/null +++ b/templates/webpages/requirement_spec_part/_part.html @@ -0,0 +1,14 @@ +[%- USE HTML -%][%- USE L -%][%- USE LxERP -%] + + + [% L.hidden_tag("additional_parts[+].part_id", part.part.id) %] + [% L.hidden_tag("additional_parts[].id", part.id) %] + [% L.img_tag(src="image/updown.png", alt=LxERP.t8("reorder item"), class="dragdrop") %] + + [% HTML.escape(part.part.partnumber) %] + [% L.input_tag("additional_parts[].description", part.description, size="30") %] + + [% L.input_tag("additional_parts[].qty_as_number", part.qty_as_number, size="10") %] + [% L.select_tag("additional_parts[].unit_id", part.unit.convertible_units, title_key="name", default=part.unit.id) %] + + diff --git a/templates/webpages/requirement_spec_part/show.html b/templates/webpages/requirement_spec_part/show.html new file mode 100644 index 000000000..128c7e0c6 --- /dev/null +++ b/templates/webpages/requirement_spec_part/show.html @@ -0,0 +1,31 @@ +[%- USE LxERP -%][%- USE L -%][%- USE P -%][%- USE HTML -%] +[% SET parts = SELF.requirement_spec.parts_sorted %] + +