$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);
# helpers
#
-sub load_parts_for_sections {
- my ($self, %params) = @_;
-
-}
-
sub create_order_item {
my ($self, %params) = @_;
--- /dev/null
+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;
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;
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',
--- /dev/null
+# 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;
--- /dev/null
+# 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;
+;
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;
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) = @_;
given then only the text blocks belonging to that C<output_position>
are returned.
+=item C<parts_sorted>
+
+Returns an array reference of additional parts sorted by their
+positional column in ascending order.
+
=item C<validate>
Validate values before saving. Returns list or human-readable error
--- /dev/null
+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;
"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",
"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",
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 ------------------------------
// -------------------------------------------------------------------------
'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) {
}, 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({
'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',
'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',
'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',
'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.',
'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!',
--- /dev/null
+-- @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)
+);
<li id="tab-header-function-block"><a href="#function-blocks-tab">[%- LxERP.t8("Content") %]</a></li>
<li id="tab-header-basic-settings"><a href="controller.pl?action=RequirementSpec/ajax_show_basic_settings&id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Basic settings") %]</a></li>
<li id="tab-header-time-cost-estimate"><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 id="tab-header-additional-parts"><a href="controller.pl?action=RequirementSpecPart/show&requirement_spec_id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Additional articles") %]</a></li>
[%- UNLESS SELF.requirement_spec.is_template %]
<li id="tab-header-versions"><a href="controller.pl?action=RequirementSpecVersion/list&requirement_spec_id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Versions") %]</a></li>
<li id="tab-header-quotations-orders"><a href="[% SELF.url_for(controller='RequirementSpecOrder', action='list', requirement_spec_id=SELF.requirement_spec.id) %]">[%- LxERP.t8("Quotations and orders") %]</a></li>
--- /dev/null
+[%- USE LxERP -%][%- USE L -%][%- USE P -%]
+[% SET parts = SELF.requirement_spec.parts_sorted %]
+
+<div id="additional_parts_form_container" class="edit-additional-parts-context-menu">
+
+ <h2>[% LxERP.t8("Edit additional articles") %]</h2>
+
+ <div>
+ [% 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')) %]
+ </div>
+
+ <form method="post" id="edit_additional_parts_form">
+ <div id="edit_additional_parts_list_empty"[% IF parts.size %] style="display: none;"[% END %]>
+ [% LxERP.t8("No articles have been added yet.") %]
+ </div>
+
+ <table id="edit_additional_parts_list"[% IF !parts.size %] style="display: none;"[% END %]>
+ <thead>
+ <tr class="listheading">
+ <th></th>
+ <th>[%- LxERP.t8("Part Number") %]</th>
+ <th>[%- LxERP.t8("Description") %]</th>
+ <th>[%- LxERP.t8("Qty") %]</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ [%- FOREACH part = parts %]
+ [%- INCLUDE 'requirement_spec_part/_part.html' part=part %]
+ [%- END %]
+ </tbody>
+ </table>
+
+ [% L.button_tag("kivi.requirement_spec.standard_additional_parts_ajax_call('save')", LxERP.t8("Save")) %]
+ </form>
+</div>
--- /dev/null
+[%- USE HTML -%][%- USE L -%][%- USE LxERP -%]
+<tr class="listrow edit-additional-parts-row-context-menu">
+ <td align="center">
+ [% 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") %]
+ </td>
+ <td>[% HTML.escape(part.part.partnumber) %]</td>
+ <td>[% L.input_tag("additional_parts[].description", part.description, size="30") %]</td>
+ <td>
+ [% 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) %]
+ </td>
+</tr>
--- /dev/null
+[%- USE LxERP -%][%- USE L -%][%- USE P -%][%- USE HTML -%]
+[% SET parts = SELF.requirement_spec.parts_sorted %]
+
+<div id="additional_parts_list_container" class="additional-parts-context-menu"[% IF initially_hidden %] style="display: none;"[% END %]>
+
+ <h2>[% LxERP.t8("Additional articles") %]</h2>
+
+ <div id="additional_parts_list_empty"[% IF parts.size %] style="display: none;"[% END %]>
+ [% LxERP.t8("No articles have been added yet.") %]
+ </div>
+
+ <table id="additional_parts_list"[% IF !parts.size %] style="display: none;"[% END %]>
+ <thead>
+ <tr class="listheading">
+ <th>[%- LxERP.t8("Part Number") %]</th>
+ <th>[%- LxERP.t8("Description") %]</th>
+ <th>[%- LxERP.t8("Qty") %]</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ [% FOREACH part = parts %]
+ <tr class="listrow">
+ <td>[% HTML.escape(part.part.partnumber) %]</td>
+ <td>[% HTML.escape(part.description) %]</td>
+ <td valign="right">[% HTML.escape(part.qty_as_number) %] [% HTML.escape(part.unit.name) %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+</div>