partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit 8786e5a3c9163eb69eb3845de56e317c52440198
parent af4ef0a0c7630e7bca26ee07d79226f73be69107
Author: Felicia Hummel <felicitus@felicitus.org>
Date:   Fri, 13 Jan 2017 21:03:32 +0100

Merge pull request #784 from partkeepr/PartKeepr-776

Part keepr 776
Diffstat:
Mapp/SymfonyRequirements.php | 151++++++++++++++++++++++++++-----------------------------------------------------
Mapp/check.php | 18+++++++++---------
Mapp/config/config_partkeepr.yml | 50+++++++++++++++++++++++++++++++++++++++++++++++++-
Mcomposer.json | 4++++
Mcomposer.lock | 15+++++++++------
Asrc/PartKeepr/CoreBundle/DoctrineMigrations/Version20170108122512.php | 31+++++++++++++++++++++++++++++++
Asrc/PartKeepr/CoreBundle/DoctrineMigrations/Version20170108143802.php | 31+++++++++++++++++++++++++++++++
Asrc/PartKeepr/CoreBundle/DoctrineMigrations/Version20170113203042.php | 31+++++++++++++++++++++++++++++++
Msrc/PartKeepr/DoctrineReflectionBundle/Filter/AdvancedSearchFilter.php | 38+++++++++++---------------------------
Msrc/PartKeepr/DoctrineReflectionBundle/Resources/config/services.xml | 23+++++++++++++++--------
Asrc/PartKeepr/DoctrineReflectionBundle/Services/FilterService.php | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/CategoryEditor/CategoryEditorForm.js | 2+-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/MenuBar.js | 1+
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/MetaPartEditor.js | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/MetaPartEditorWindow.js | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditor.js | 20++++++++++++++++++--
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditorWindow.js | 4++--
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartParameterValueEditor.js | 13+++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartFilterPanel.js | 5++++-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsGrid.js | 40++++++++++++++++++++++++++++++++++++----
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsManager.js | 42++++++++++++++++++++++++++++++++++++++----
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Project/ProjectPartGrid.js | 51+++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Project/ProjectReport.js | 370++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/ProjectRun/ProjectRunEditor.js | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/ProjectRun/ProjectRunEditorComponent.js | 30++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/ProjectRun/ProjectRunGrid.js | 22++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FieldSelectorWindow.js | 3+++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FilterExpression.js | 22+++++++++-------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FilterExpressionWindow.js | 7+++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/OperatorComboBox.js | 31+++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PartParameterComboBox.js | 77++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PartParameterSearch.js | 231+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PartParameterSearchWindow.js | 28++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/SiUnitField.js | 13++++++++++---
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/SiUnitList.js | 21---------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Data/store/OperatorStore.js | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Models/ProjectReport.js | 10++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Models/ProjectReportPart.js | 8++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/PartKeepr.js | 2+-
Msrc/PartKeepr/FrontendBundle/Resources/views/index.html.twig | 8++++++++
Asrc/PartKeepr/PartBundle/Action/GetPartsAction.php | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/PartBundle/Controller/PartController.php | 76+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Asrc/PartKeepr/PartBundle/Entity/MetaPartParameterCriteria.php | 257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/PartBundle/Entity/Part.php | 269+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/PartKeepr/PartBundle/Entity/PartParameter.php | 194++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Asrc/PartKeepr/PartBundle/Exceptions/NotAMetaPartException.php | 12++++++++++++
Msrc/PartKeepr/PartBundle/Resources/config/actions.xml | 6++++++
Msrc/PartKeepr/PartBundle/Resources/config/services.xml | 8+++++---
Msrc/PartKeepr/PartBundle/Services/PartService.php | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/ProjectBundle/Controller/ProjectReportController.php | 56+++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/PartKeepr/ProjectBundle/DataFixtures/ProjectFixtureLoader.php | 4++++
Msrc/PartKeepr/ProjectBundle/Entity/ProjectPart.php | 67++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Asrc/PartKeepr/ProjectBundle/Entity/ProjectRun.php | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/ProjectBundle/Entity/ProjectRunPart.php | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/ProjectBundle/Tests/ProjectTest.php | 38++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/SetupBundle/Services/ConfigSetupService.php | 1+
Msrc/PartKeepr/SetupBundle/Services/FootprintSetupService.php | 2+-
Msrc/PartKeepr/SetupBundle/Services/UnitSetupService.php | 2+-
Msrc/PartKeepr/StatisticBundle/Services/StatisticService.php | 2+-
59 files changed, 2927 insertions(+), 459 deletions(-)

diff --git a/app/SymfonyRequirements.php b/app/SymfonyRequirements.php @@ -33,13 +33,9 @@ class Requirement { private $fulfilled; - private $testMessage; - private $helpText; - private $helpHtml; - private $optional; /** @@ -48,8 +44,7 @@ class Requirement * @param bool $fulfilled Whether the requirement is fulfilled * @param string $testMessage The message for testing the requirement * @param string $helpHtml The help text formatted in HTML for resolving the problem - * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from - * HTML tags) + * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) * @param bool $optional Whether this is only an optional recommendation not a mandatory requirement */ public function __construct($fulfilled, $testMessage, $helpHtml, $helpText = null, $optional = false) @@ -123,32 +118,18 @@ class PhpIniRequirement extends Requirement * Constructor that initializes the requirement. * * @param string $cfgName The configuration name used for ini_get() - * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to - * true or false, or a callback function receiving the configuration value - * as parameter to determine the fulfillment of the requirement - * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration - * option does not exist, i.e. ini_get() returns false. This is helpful for - * abandoned configs in later PHP versions or configs of an optional - * extension, like Suhosin. Example: You require a config to be true but - * PHP later removes this config and defaults it to true internally. - * @param string|null $testMessage The message for testing the requirement (when null and $evaluation is a - * boolean a default message is derived) - * @param string|null $helpHtml The help text formatted in HTML for resolving the problem (when null and - * $evaluation is a boolean a default help is derived) - * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. - * stripped from HTML tags) - * @param bool $optional Whether this is only an optional recommendation not a mandatory - * requirement + * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false, + * or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement + * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. + * This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. + * Example: You require a config to be true but PHP later removes this config and defaults it to true internally. + * @param string|null $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived) + * @param string|null $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) + * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) + * @param bool $optional Whether this is only an optional recommendation not a mandatory requirement */ - public function __construct( - $cfgName, - $evaluation, - $approveCfgAbsence = false, - $testMessage = null, - $helpHtml = null, - $helpText = null, - $optional = false - ) { + public function __construct($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null, $optional = false) + { $cfgValue = ini_get($cfgName); if (is_callable($evaluation)) { @@ -176,8 +157,7 @@ class PhpIniRequirement extends Requirement $fulfilled = $evaluation == $cfgValue; } - parent::__construct($fulfilled || ($approveCfgAbsence && false === $cfgValue), $testMessage, $helpHtml, - $helpText, $optional); + parent::__construct($fulfilled || ($approveCfgAbsence && false === $cfgValue), $testMessage, $helpHtml, $helpText, $optional); } } @@ -188,7 +168,7 @@ class PhpIniRequirement extends Requirement */ class RequirementCollection implements IteratorAggregate { - private $requirements = []; + private $requirements = array(); /** * Gets the current RequirementCollection as an Iterator. @@ -216,8 +196,7 @@ class RequirementCollection implements IteratorAggregate * @param bool $fulfilled Whether the requirement is fulfilled * @param string $testMessage The message for testing the requirement * @param string $helpHtml The help text formatted in HTML for resolving the problem - * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from - * HTML tags) + * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) */ public function addRequirement($fulfilled, $testMessage, $helpHtml, $helpText = null) { @@ -230,8 +209,7 @@ class RequirementCollection implements IteratorAggregate * @param bool $fulfilled Whether the recommendation is fulfilled * @param string $testMessage The message for testing the recommendation * @param string $helpHtml The help text formatted in HTML for resolving the problem - * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from - * HTML tags) + * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) */ public function addRecommendation($fulfilled, $testMessage, $helpHtml, $helpText = null) { @@ -242,62 +220,36 @@ class RequirementCollection implements IteratorAggregate * Adds a mandatory requirement in form of a php.ini configuration. * * @param string $cfgName The configuration name used for ini_get() - * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to - * true or false, or a callback function receiving the configuration value - * as parameter to determine the fulfillment of the requirement - * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration - * option does not exist, i.e. ini_get() returns false. This is helpful for - * abandoned configs in later PHP versions or configs of an optional - * extension, like Suhosin. Example: You require a config to be true but - * PHP later removes this config and defaults it to true internally. - * @param string $testMessage The message for testing the requirement (when null and $evaluation is a - * boolean a default message is derived) - * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and - * $evaluation is a boolean a default help is derived) - * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. - * stripped from HTML tags) + * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false, + * or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement + * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. + * This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. + * Example: You require a config to be true but PHP later removes this config and defaults it to true internally. + * @param string $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived) + * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) + * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) */ - public function addPhpIniRequirement( - $cfgName, - $evaluation, - $approveCfgAbsence = false, - $testMessage = null, - $helpHtml = null, - $helpText = null - ) { - $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, - false)); + public function addPhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null) + { + $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, false)); } /** * Adds an optional recommendation in form of a php.ini configuration. * * @param string $cfgName The configuration name used for ini_get() - * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to - * true or false, or a callback function receiving the configuration value - * as parameter to determine the fulfillment of the requirement - * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration - * option does not exist, i.e. ini_get() returns false. This is helpful for - * abandoned configs in later PHP versions or configs of an optional - * extension, like Suhosin. Example: You require a config to be true but - * PHP later removes this config and defaults it to true internally. - * @param string $testMessage The message for testing the requirement (when null and $evaluation is a - * boolean a default message is derived) - * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and - * $evaluation is a boolean a default help is derived) - * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. - * stripped from HTML tags) + * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false, + * or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement + * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. + * This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. + * Example: You require a config to be true but PHP later removes this config and defaults it to true internally. + * @param string $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived) + * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) + * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) */ - public function addPhpIniRecommendation( - $cfgName, - $evaluation, - $approveCfgAbsence = false, - $testMessage = null, - $helpHtml = null, - $helpText = null - ) { - $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, - true)); + public function addPhpIniRecommendation($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null) + { + $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, true)); } /** @@ -327,7 +279,7 @@ class RequirementCollection implements IteratorAggregate */ public function getRequirements() { - $array = []; + $array = array(); foreach ($this->requirements as $req) { if (!$req->isOptional()) { $array[] = $req; @@ -344,7 +296,7 @@ class RequirementCollection implements IteratorAggregate */ public function getFailedRequirements() { - $array = []; + $array = array(); foreach ($this->requirements as $req) { if (!$req->isFulfilled() && !$req->isOptional()) { $array[] = $req; @@ -361,7 +313,7 @@ class RequirementCollection implements IteratorAggregate */ public function getRecommendations() { - $array = []; + $array = array(); foreach ($this->requirements as $req) { if ($req->isOptional()) { $array[] = $req; @@ -378,7 +330,7 @@ class RequirementCollection implements IteratorAggregate */ public function getFailedRecommendations() { - $array = []; + $array = array(); foreach ($this->requirements as $req) { if (!$req->isFulfilled() && $req->isOptional()) { $array[] = $req; @@ -441,8 +393,7 @@ class SymfonyRequirements extends RequirementCollection sprintf('You are running PHP version "<strong>%s</strong>", but Symfony needs at least PHP "<strong>%s</strong>" to run. Before using Symfony, upgrade your PHP installation, preferably to the latest version.', $installedPhpVersion, self::REQUIRED_PHP_VERSION), - sprintf('Install PHP %s or newer (installed version is %s)', self::REQUIRED_PHP_VERSION, - $installedPhpVersion) + sprintf('Install PHP %s or newer (installed version is %s)', self::REQUIRED_PHP_VERSION, $installedPhpVersion) ); $this->addRequirement( @@ -455,7 +406,7 @@ class SymfonyRequirements extends RequirementCollection is_dir(__DIR__.'/../vendor/composer'), 'Vendor libraries must be installed', 'Vendor libraries are missing. Install composer following instructions from <a href="http://getcomposer.org/">http://getcomposer.org/</a>. '. - 'Then run "<strong>php composer.phar install</strong>" to install them.' + 'Then run "<strong>php composer.phar install</strong>" to install them.' ); $cacheDir = is_dir(__DIR__.'/../var/cache') ? __DIR__.'/../var/cache' : __DIR__.'/cache'; @@ -481,7 +432,7 @@ class SymfonyRequirements extends RequirementCollection ); if (version_compare($installedPhpVersion, self::REQUIRED_PHP_VERSION, '>=')) { - $timezones = []; + $timezones = array(); foreach (DateTimeZone::listAbbreviations() as $abbreviations) { foreach ($abbreviations as $abbreviation) { $timezones[$abbreviation['timezone_id']] = true; @@ -490,8 +441,7 @@ class SymfonyRequirements extends RequirementCollection $this->addRequirement( isset($timezones[@date_default_timezone_get()]), - sprintf('Configured default timezone "%s" must be supported by your installation of PHP', - @date_default_timezone_get()), + sprintf('Configured default timezone "%s" must be supported by your installation of PHP', @date_default_timezone_get()), 'Your default timezone is not supported by PHP. Check for typos in your <strong>php.ini</strong> file and have a look at the list of deprecated timezones at <a href="http://php.net/manual/en/timezones.others.php">http://php.net/manual/en/timezones.others.php</a>.' ); } @@ -640,8 +590,7 @@ class SymfonyRequirements extends RequirementCollection ); $this->addRecommendation( - (version_compare($installedPhpVersion, '5.3.18', '>=') && version_compare($installedPhpVersion, '5.4.0', - '<')) + (version_compare($installedPhpVersion, '5.3.18', '>=') && version_compare($installedPhpVersion, '5.4.0', '<')) || version_compare($installedPhpVersion, '5.4.8', '>='), 'You should use PHP 5.3.18+ or PHP 5.4.8+ to always get nice error messages for fatal errors in the development environment due to PHP bug #61767/#60909', @@ -748,7 +697,8 @@ class SymfonyRequirements extends RequirementCollection || (extension_loaded('xcache') && ini_get('xcache.cacher')) || - (extension_loaded('wincache') && ini_get('wincache.ocenabled')); + (extension_loaded('wincache') && ini_get('wincache.ocenabled')) + ; $this->addRecommendation( $accelerator, @@ -782,8 +732,7 @@ class SymfonyRequirements extends RequirementCollection $drivers = PDO::getAvailableDrivers(); $this->addRecommendation( count($drivers) > 0, - sprintf('PDO should have some drivers installed (currently available: %s)', - count($drivers) ? implode(', ', $drivers) : 'none'), + sprintf('PDO should have some drivers installed (currently available: %s)', count($drivers) ? implode(', ', $drivers) : 'none'), 'Install <strong>PDO drivers</strong> (mandatory for Doctrine).' ); } diff --git a/app/check.php b/app/check.php @@ -19,7 +19,7 @@ echo PHP_EOL.PHP_EOL; echo '> Checking Symfony requirements:'.PHP_EOL.' '; -$messages = []; +$messages = array(); foreach ($symfonyRequirements->getRequirements() as $req) { /** @var $req Requirement */ if ($helpText = get_error_message($req, $lineSize)) { @@ -99,15 +99,15 @@ function echo_title($title, $style = null) function echo_style($style, $message) { // ANSI color codes - $styles = [ - 'reset' => "\033[0m", - 'red' => "\033[31m", - 'green' => "\033[32m", - 'yellow' => "\033[33m", - 'error' => "\033[37;41m", + $styles = array( + 'reset' => "\033[0m", + 'red' => "\033[31m", + 'green' => "\033[32m", + 'yellow' => "\033[33m", + 'error' => "\033[37;41m", 'success' => "\033[37;42m", - 'title' => "\033[34m", - ]; + 'title' => "\033[34m", + ); $supports = has_color_support(); echo($supports ? $styles[$style] : '').$message.($supports ? $styles['reset'] : ''); diff --git a/app/config/config_partkeepr.yml b/app/config/config_partkeepr.yml @@ -347,6 +347,17 @@ services: - "partkeepr.part.post" - "PartPost" + resource.part.collection_operation.custom_get: + class: "Dunglas\ApiBundle\Api\Operation\Operation" + public: false + factory: [ "@api.operation_factory", "createCollectionOperation" ] + arguments: + - "@resource.part" + - [ "GET" ] + - "/parts" + - "partkeepr.parts.collection_get" + - "PartsGet" + resource.part.item_operation.get: class: "Dunglas\ApiBundle\Api\Operation\Operation" public: false @@ -412,7 +423,7 @@ services: - method: "initItemOperations" arguments: [ [ "@resource.part.item_operation.get", "@resource.part.item_operation.custom_put", "@resource.part.item_operation.delete", "@resource.part.item_operation.add_stock", "@resource.part.item_operation.remove_stock", "@resource.part.item_operation.set_stock" ] ] - method: "initCollectionOperations" - arguments: [ [ "@resource.part.collection_operation.get", "@resource.part.collection_operation.custom_post" ] ] + arguments: [ [ "@resource.part.collection_operation.custom_get", "@resource.part.collection_operation.custom_post" ] ] - method: "initFilters" arguments: [ [ "@doctrine_reflection_service.search_filter" ] ] - method: "initNormalizationContext" @@ -575,6 +586,17 @@ services: arguments: - { groups: [ "default" ] } + resource.meta_part_parameter_criteria: + parent: "api.resource" + arguments: [ "PartKeepr\\PartBundle\\Entity\\MetaPartParameterCriteria" ] + tags: [ { name: "api.resource" } ] + calls: + - method: "initNormalizationContext" + arguments: [ { groups: [ "default" ] } ] + - method: "initDenormalizationContext" + arguments: + - { groups: [ "default" ] } + resource.manufacturer: parent: "api.resource" arguments: [ "PartKeepr\\ManufacturerBundle\\Entity\\Manufacturer" ] @@ -1287,6 +1309,32 @@ services: arguments: - { groups: [ "default" ] } + resource.project_run: + parent: "api.resource" + arguments: [ "PartKeepr\\ProjectBundle\\Entity\\ProjectRun" ] + tags: [ { name: "api.resource" } ] + calls: + - method: "initFilters" + arguments: [ [ "@doctrine_reflection_service.search_filter" ] ] + - method: "initNormalizationContext" + arguments: [ { groups: [ "default" ] } ] + - method: "initDenormalizationContext" + arguments: + - { groups: [ "default" ] } + + resource.project_run_part: + parent: "api.resource" + arguments: [ "PartKeepr\\ProjectBundle\\Entity\\ProjectRunPart" ] + tags: [ { name: "api.resource" } ] + calls: + - method: "initFilters" + arguments: [ [ "@doctrine_reflection_service.search_filter" ] ] + - method: "initNormalizationContext" + arguments: [ { groups: [ "default" ] } ] + - method: "initDenormalizationContext" + arguments: + - { groups: [ "default" ] } + resource.system_notice.item_operation.get: class: "Dunglas\ApiBundle\Api\Operation\Operation" public: false diff --git a/composer.json b/composer.json @@ -27,6 +27,10 @@ }, { "type": "vcs", + "url": "https://github.com/partkeepr/extjs6.git" + }, + { + "type": "vcs", "url": "https://github.com/partkeepr/FR3DLdapBundle" }, { diff --git a/composer.lock b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "15d2b8b3ed791b4888ca0687288e6ca4", + "content-hash": "d821dadac3d5144dcd8a009c95089e5e", "packages": [ { "name": "atelierspierrot/famfamfam-silk-sprite", @@ -2874,19 +2874,18 @@ "source": { "type": "git", "url": "https://github.com/partkeepr/extjs6.git", - "reference": "6e6492ba3a0ba40753805cff4c2aeac069b15426" + "reference": "e65af313cf71633e76b310b0a9981494b2dd392b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/partkeepr/extjs6/zipball/6e6492ba3a0ba40753805cff4c2aeac069b15426", - "reference": "6e6492ba3a0ba40753805cff4c2aeac069b15426", + "url": "https://api.github.com/repos/partkeepr/extjs6/zipball/e65af313cf71633e76b310b0a9981494b2dd392b", + "reference": "e65af313cf71633e76b310b0a9981494b2dd392b", "shasum": "" }, "require": { "composer/installers": "~1.0" }, "type": "zend-extra", - "notification-url": "https://packagist.org/downloads/", "license": [ "GPLv3" ], @@ -2895,7 +2894,11 @@ "PartKeepr", "extjs6" ], - "time": "2015-07-01 20:21:32" + "support": { + "source": "https://github.com/partkeepr/extjs6/tree/master", + "issues": "https://github.com/partkeepr/extjs6/issues" + }, + "time": "2017-01-10 21:51:17" }, { "name": "partkeepr/remote-file-loader", diff --git a/src/PartKeepr/CoreBundle/DoctrineMigrations/Version20170108122512.php b/src/PartKeepr/CoreBundle/DoctrineMigrations/Version20170108122512.php @@ -0,0 +1,31 @@ +<?php + +namespace PartKeepr\CoreBundle\DoctrineMigrations; + +use Doctrine\DBAL\Migrations\AbstractMigration; +use Doctrine\DBAL\Schema\Schema; + +/** + * Ensures that all existing parts to be no meta parts since prior to that version, no meta parts existed. + */ +class Version20170108122512 extends BaseMigration +{ +/** + * @param Schema $schema + */ + public function up(Schema $schema) + { + $this->performDatabaseUpgrade(); + $noMetaPartSQL = 'UPDATE Part SET metaPart = false'; + $this->addSql($noMetaPartSQL); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + // this down() migration is auto-generated, please modify it to your needs + + } +} diff --git a/src/PartKeepr/CoreBundle/DoctrineMigrations/Version20170108143802.php b/src/PartKeepr/CoreBundle/DoctrineMigrations/Version20170108143802.php @@ -0,0 +1,31 @@ +<?php + +namespace PartKeepr\CoreBundle\DoctrineMigrations; + +use Doctrine\DBAL\Migrations\AbstractMigration; +use Doctrine\DBAL\Schema\Schema; + +/** + * Updates the value type to "numeric" where no value type is set + */ +class Version20170108143802 extends BaseMigration +{ +/** + * @param Schema $schema + */ + public function up(Schema $schema) + { + $this->performDatabaseUpgrade(); + $adjustValueTypesSQL = 'UPDATE PartParameter SET valueType = "numeric" where valueType = ""'; + $this->addSql($adjustValueTypesSQL); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + // this down() migration is auto-generated, please modify it to your needs + + } +} diff --git a/src/PartKeepr/CoreBundle/DoctrineMigrations/Version20170113203042.php b/src/PartKeepr/CoreBundle/DoctrineMigrations/Version20170113203042.php @@ -0,0 +1,31 @@ +<?php + +namespace PartKeepr\CoreBundle\DoctrineMigrations; + +use Doctrine\DBAL\Migrations\AbstractMigration; +use Doctrine\DBAL\Schema\Schema; + +/** + * Auto-generated Migration: Please modify to your needs! + */ +class Version20170113203042 extends BaseMigration +{ +/** + * @param Schema $schema + */ + public function up(Schema $schema) + { + $this->performDatabaseUpgrade(); + $adjustValueTypesSQL = 'UPDATE ProjectPart SET overageType = "absolute" where overageType = ""'; + $this->addSql($adjustValueTypesSQL); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + // this down() migration is auto-generated, please modify it to your needs + + } +} diff --git a/src/PartKeepr/DoctrineReflectionBundle/Filter/AdvancedSearchFilter.php b/src/PartKeepr/DoctrineReflectionBundle/Filter/AdvancedSearchFilter.php @@ -7,6 +7,7 @@ use Doctrine\ORM\QueryBuilder; use Dunglas\ApiBundle\Api\IriConverterInterface; use Dunglas\ApiBundle\Api\ResourceInterface; use Dunglas\ApiBundle\Doctrine\Orm\Filter\AbstractFilter; +use PartKeepr\DoctrineReflectionBundle\Services\FilterService; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -36,6 +37,11 @@ class AdvancedSearchFilter extends AbstractFilter */ private $propertyAccessor; + /** + * @var FilterService + */ + private $filterService; + private $aliases = []; private $parameterCount = 0; @@ -49,19 +55,21 @@ class AdvancedSearchFilter extends AbstractFilter * @param IriConverterInterface $iriConverter * @param PropertyAccessorInterface $propertyAccessor * @param RequestStack $requestStack - * @param null|array $properties Null to allow filtering on all properties with the exact strategy - * or a map of property name with strategy. + * @param null|array $properties Null to allow filtering on all properties with the exact + * strategy or a map of property name with strategy. */ public function __construct( ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, PropertyAccessorInterface $propertyAccessor, RequestStack $requestStack, + FilterService $filterService, array $properties = null ) { parent::__construct($managerRegistry, $properties); $this->requestStack = $requestStack; $this->iriConverter = $iriConverter; + $this->filterService = $filterService; $this->propertyAccessor = $propertyAccessor; } @@ -248,31 +256,7 @@ class AdvancedSearchFilter extends AbstractFilter $this->parameterCount++; $queryBuilder->setParameter($paramName, $filter->getValue()); - switch (strtolower($filter->getOperator())) { - case Filter::OPERATOR_EQUALS: - return $queryBuilder->expr()->eq($alias, $paramName); - break; - case Filter::OPERATOR_GREATER_THAN: - return $queryBuilder->expr()->gt($alias, $paramName); - break; - case Filter::OPERATOR_GREATER_THAN_EQUALS: - return $queryBuilder->expr()->gte($alias, $paramName); - break; - case Filter::OPERATOR_LESS_THAN: - return $queryBuilder->expr()->lt($alias, $paramName); - break; - case Filter::OPERATOR_LESS_THAN_EQUALS: - return $queryBuilder->expr()->lte($alias, $paramName); - break; - case Filter::OPERATOR_NOT_EQUALS: - return $queryBuilder->expr()->neq($alias, $paramName); - break; - case Filter::OPERATOR_LIKE: - return $queryBuilder->expr()->like($alias, $paramName); - break; - default: - throw new \Exception('Unknown operator '.$filter->getOperator()); - } + return $this->filterService->getExpressionForFilter($filter, $alias, $paramName); } } diff --git a/src/PartKeepr/DoctrineReflectionBundle/Resources/config/services.xml b/src/PartKeepr/DoctrineReflectionBundle/Resources/config/services.xml @@ -1,8 +1,8 @@ <?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="doctrine_reflection_service" class="PartKeepr\DoctrineReflectionBundle\Services\ReflectionService"> @@ -11,14 +11,21 @@ <argument type="service" id="annotation_reader"/> </service> - <service id="doctrine_reflection_service.search_filter" class="PartKeepr\DoctrineReflectionBundle\Filter\AdvancedSearchFilter" public="false"> - <argument type="service" id="doctrine" /> - <argument type="service" id="api.iri_converter" /> - <argument type="service" id="property_accessor" /> - <argument type="service" id="request_stack" /> + <service id="doctrine_filter_service" class="PartKeepr\DoctrineReflectionBundle\Services\FilterService"> + <argument type="service" id="doctrine"/> + </service> + + <service id="doctrine_reflection_service.search_filter" + class="PartKeepr\DoctrineReflectionBundle\Filter\AdvancedSearchFilter" public="false"> + <argument type="service" id="doctrine"/> + <argument type="service" id="api.iri_converter"/> + <argument type="service" id="property_accessor"/> + <argument type="service" id="request_stack"/> + <argument type="service" id="doctrine_filter_service"/> </service> - <service id="partkeepr.exceptionwrapper" class="PartKeepr\DoctrineReflectionBundle\Exception\ExceptionWrapperHandler"> + <service id="partkeepr.exceptionwrapper" + class="PartKeepr\DoctrineReflectionBundle\Exception\ExceptionWrapperHandler"> </service> </services> diff --git a/src/PartKeepr/DoctrineReflectionBundle/Services/FilterService.php b/src/PartKeepr/DoctrineReflectionBundle/Services/FilterService.php @@ -0,0 +1,58 @@ +<?php +namespace PartKeepr\DoctrineReflectionBundle\Services; + +use Doctrine\Bundle\DoctrineBundle\Registry; +use Doctrine\ORM\EntityManager; +use PartKeepr\DoctrineReflectionBundle\Filter\Filter; + +class FilterService +{ + /** + * @var EntityManager + */ + var $em; + + public function __construct(Registry $registry) + { + $this->em = $registry->getManager(); + } + + /** + * Returns a DQL expression for the given filter and alias + * + * @param Filter $filter The filter to build the expression for + * @param string $alias The field alias to search in + * @param string $paramName The parameter name you use to bind the value to + * + * @return \Doctrine\ORM\Query\Expr\Comparison + * @throws \Exception + */ + public function getExpressionForFilter(Filter $filter, $alias, $paramName) + { + switch (strtolower($filter->getOperator())) { + case Filter::OPERATOR_EQUALS: + return $this->em->getExpressionBuilder()->eq($alias, $paramName); + break; + case Filter::OPERATOR_GREATER_THAN: + return $this->em->getExpressionBuilder()->gt($alias, $paramName); + break; + case Filter::OPERATOR_GREATER_THAN_EQUALS: + return $this->em->getExpressionBuilder()->gte($alias, $paramName); + break; + case Filter::OPERATOR_LESS_THAN: + return $this->em->getExpressionBuilder()->lt($alias, $paramName); + break; + case Filter::OPERATOR_LESS_THAN_EQUALS: + return $this->em->getExpressionBuilder()->lte($alias, $paramName); + break; + case Filter::OPERATOR_NOT_EQUALS: + return $this->em->getExpressionBuilder()->neq($alias, $paramName); + break; + case Filter::OPERATOR_LIKE: + return $this->em->getExpressionBuilder()->like($alias, $paramName); + break; + default: + throw new \Exception('Unknown operator '.$filter->getOperator()); + } + } +} diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/CategoryEditor/CategoryEditorForm.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/CategoryEditor/CategoryEditorForm.js @@ -3,7 +3,7 @@ Ext.define('PartKeepr.CategoryEditorForm', { layout: 'anchor', border: false, frame: false, - bodyStyle: 'background:#DBDBDB;padding: 10px;', + ui: 'default-framed', xtype: "CategoryEditorForm", items: [ { diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/MenuBar.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/MenuBar.js @@ -66,6 +66,7 @@ Ext.define('PartKeepr.MenuBar', { "PartKeepr.StatisticsChartPanel", "PartKeepr.SystemInformationGrid", "PartKeepr.ProjectReportView", + 'PartKeepr.ProjectRunEditorComponent', "PartKeepr.SystemNoticeEditorComponent", "PartKeepr.StockHistoryGrid", ]; diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/MetaPartEditor.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/MetaPartEditor.js @@ -0,0 +1,193 @@ +/** + * @class PartKeepr.Components.Part.Editor.MetaPartEditor + + * <p>The MetaPartEditor provides an editing form for meta parts</p> + */ +Ext.define('PartKeepr.Components.Part.Editor.MetaPartEditor', { + extend: 'PartKeepr.Editor', + + // Assigned model + model: 'PartKeepr.PartBundle.Entity.Part', + bodyPadding: '0 0 0 0', + // Layout stuff + border: false, + editAfterSave: false, + + ui: 'default-framed', + + layout: { + type: 'vbox', + align: 'stretch', + pack: 'start' + }, + + /** + * Initializes the editor fields + */ + initComponent: function () + { + this.operatorStore = Ext.create("PartKeepr.Data.store.OperatorStore"); + + this.nameField = Ext.create("Ext.form.field.Text", { + name: 'name', + fieldLabel: i18n("Name"), + allowBlank: false, + labelWidth: 150, + height: 20 + }); + + this.parameterCriterias = Ext.create("Ext.grid.Panel", { + columns: [ + { + header: i18n("Parameter Name"), + dataIndex: 'partParameterName', + flex: 1 + }, { + header: i18n("Operator"), + renderer: this.renderOperator, + scope: this, + flex: 0.5 + }, { + header: i18n("Value"), + renderer: this.renderValue, + scope: this, + flex: 1 + } + ], + bbar: [ + { + xtype: 'button', + itemId: 'addParameterCriteria', + handler: this.addParameterCriteriaClick, + iconCls: "fugue-icon eye--plus", + text: i18n("Add…"), + scope: this + }, { + xtype: 'button', + itemId: 'deleteParameterCriteria', + handler: this.onDeleteParameterCriteriaClick, + text: i18n("Delete"), + iconCls: "fugue-icon eye--minus", + scope: this + } + ] + }) + ; + + this.items = [ + this.nameField, + { + xtype: 'CategoryComboBox', + fieldLabel: i18n("Category"), + name: 'category', + displayField: "name", + returnObject: true + }, + { + xtype: 'PartUnitComboBox', + fieldLabel: i18n("Measurement Unit"), + columnWidth: 0.5, + returnObject: true, + name: 'partUnit' + }, + { + xtype: 'fieldcontainer', + fieldLabel: i18n("Parameter Criterias"), + flex: 1, + layout: 'fit', + items: this.parameterCriterias + } + ]; + + + this.on("startEdit", this.onEditStart, this, {delay: 200}); + this.on("itemSaved", this._onItemSaved, this); + this.callParent(); + + this.parameterCriterias.getSelectionModel().on('selectionchange', this.onSelectChange, this); + }, + onCancelEdit: function () { + this.record.metaPartParameterCriterias().rejectChanges(); + this.callParent(arguments); + }, + setTitle: function (title) + { + var tmpTitle; + + if (this.record.phantom) { + tmpTitle = i18n("Add Meta-Part"); + } else { + tmpTitle = i18n("Edit Meta-Part"); + } + + if (title !== "") { + tmpTitle = tmpTitle + ": " + title; + } + + this.fireEvent("_titleChange", tmpTitle); + }, + _onItemSaved: function () + { + this.fireEvent("partSaved", this.record); + + this.fireEvent("editorClose", this); + }, + onSelectChange: function (selModel, selections) + { + this.down("#deleteParameterCriteria").setDisabled(selections.length === 0); + }, + addParameterCriteriaClick: function () + { + var j = Ext.create("PartKeepr.Components.Widgets.PartParameterSearchWindow"); + + j.on("apply", this.onAddParameterCriteria, this); + j.show(); + }, + onAddParameterCriteria: function (rec) + { + this.parameterCriterias.getStore().add(rec); + }, + onDeleteParameterCriteriaClick: function () + { + this.parameterCriterias.getStore().remove(this.parameterCriterias.getSelection()); + }, + renderValue: function (v, m, rec) + { + var unit = "", symbol = ""; + + switch (rec.get("valueType")) { + case "string": + return rec.get("stringValue"); + break; + case "numeric": + if (rec.getUnit() !== null) { + unit = rec.getUnit().get("symbol"); + } + + if (rec.getSiPrefix() !== null) { + symbol = rec.getSiPrefix().get("symbol"); + } + + + return rec.get("value") + symbol + unit; + break; + default: + return ""; + } + }, + renderOperator: function (v, m, rec) + { + var operator = this.operatorStore.findRecord("operator", rec.get("operator"), 0, false, true, true); + + if (operator instanceof Ext.data.Model) { + return operator.get("symbol"); + } else { + return ""; + } + }, + onEditStart: function () + { + this.parameterCriterias.bindStore(this.record.metaPartParameterCriterias()); + this.nameField.focus(); + }, +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/MetaPartEditorWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/MetaPartEditorWindow.js @@ -0,0 +1,118 @@ +/** + * @class PartKeepr.Components.Part.Editor.MetaPartEditorWindow + + * <p>The MetaPartEditorWindow encapsulates the PartKeepr.Components.Part.Editor.MetaPartEditor within a window.</p> + */ +Ext.define('PartKeepr.Components.Part.Editor.MetaPartEditorWindow', { + extend: 'Ext.window.Window', + + /* Constrain the window to fit the viewport */ + constrainHeader: true, + + /* Fit the editor within the window */ + layout: 'fit', + + /* Width and height settings */ + width: 600, + minWidth: 600, + minHeight: 415, + height: 415, + + border: false, + + saveText: i18n("Save"), + cancelText: i18n("Cancel"), + + title: i18n("Add/Edit Meta-Part"), + + saveButtonReenableTask: null, + + /** + * Creates the part editor and put it into the window. + */ + initComponent: function () + { + this.editor = Ext.create("PartKeepr.Components.Part.Editor.MetaPartEditor", { + border: false, + enableButtons: false + }); + + this.items = [this.editor]; + + this.editor.on("editorClose", function () + { + this.close(); + }, this, {delay: 200}); + + this.editor.on("_titleChange", function (val) + { + this.setTitle(val); + }, this); + this.editor.on("itemSaved", this.onItemSaved, this); + + this.saveButton = Ext.create("Ext.button.Button", { + text: this.saveText, + iconCls: 'fugue-icon disk', + handler: Ext.bind(this.onItemSave, this) + }); + + this.cancelButton = Ext.create("Ext.button.Button", { + text: this.cancelText, + iconCls: 'web-icon cancel', + handler: Ext.bind(this.onCancelEdit, this) + }); + + this.bottomToolbar = Ext.create("Ext.toolbar.Toolbar", { + enableOverflow: true, + defaults: {minWidth: 100}, + dock: 'bottom', + ui: 'footer', + pack: 'start', + items: [this.saveButton, this.cancelButton] + }); + + this.dockedItems = [this.bottomToolbar]; + + this.callParent(); + }, + onCancelEdit: function () + { + this.editor.onCancelEdit(); + }, + /** + * Called when the save button was clicked + */ + onItemSave: function () + { + if (!this.editor.getForm().isValid()) { + return; + } + + // Disable the save button to indicate progress + this.saveButton.disable(); + + // Sanity: If the save process fails, re-enable the button after 30 seconds + if (this.saveButtonReenableTask === null) { + this.saveButtonReenableTask = new Ext.util.DelayedTask(function () + { + this.saveButton.enable(); + }, this); + this.on('destroy', function () + { + this.saveButtonReenableTask.cancel(); + }, this); + } + this.saveButtonReenableTask.delay(30000); + + if (!this.editor._onItemSave()) { + this.saveButton.enable(); + } + }, + /** + * Called when the item was saved + */ + onItemSaved: function () + { + this.saveButton.enable(); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditor.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditor.js @@ -20,7 +20,7 @@ Ext.define('PartKeepr.PartEditor', { initComponent: function () { // Defines the overall height of all fields, used to calculate the anchoring for the description field - var overallHeight = (this.partMode == "create") ? '-300' : '-245'; + var overallHeight = (this.partMode == "create") ? 320: 265; this.nameField = Ext.create("Ext.form.field.Text", { name: 'name', @@ -90,6 +90,7 @@ Ext.define('PartKeepr.PartEditor', { name: 'description' }, { layout: 'column', + xtype: 'fieldcontainer', margin: { bottom: "0 5px 5px 0" }, @@ -140,9 +141,14 @@ Ext.define('PartKeepr.PartEditor', { fieldLabel: i18n("Comment"), name: 'comment', allowBlank: this.isOptional("comment"), - anchor: '100% ' + overallHeight + anchor: '100% ' + (-overallHeight).toString() }, { + xtype: 'textfield', + fieldLabel: i18n("Production Remarks"), + name: 'productionRemarks', + allowBlank: this.isOptional("productionRemarks"), + },{ xtype: 'fieldcontainer', layout: 'hbox', fieldLabel: i18n("Status"), @@ -257,6 +263,7 @@ Ext.define('PartKeepr.PartEditor', { }); basicEditorFields.push({ + xtype: 'container', layout: 'column', border: false, items: [ @@ -279,6 +286,7 @@ Ext.define('PartKeepr.PartEditor', { }); basicEditorFields.push({ + xtype: 'container', layout: 'column', border: false, items: [ @@ -297,6 +305,7 @@ Ext.define('PartKeepr.PartEditor', { items: [ { iconCls: 'web-icon brick', + ui: 'default-framed', xtype: 'panel', autoScroll: false, layout: 'anchor', @@ -477,6 +486,13 @@ Ext.define('PartKeepr.PartEditor', { this.partAttachmentGrid.bindStore(this.record.attachments()); this.partParameterGrid.bindStore(this.record.parameters()); }, + onCancelEdit: function () { + this.record.distributors().rejectChanges(); + this.record.manufacturers().rejectChanges(); + this.record.attachments().rejectChanges(); + this.record.parameters().rejectChanges(); + this.callParent(arguments); + }, setTitle: function (title) { var tmpTitle; diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditorWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditorWindow.js @@ -15,8 +15,8 @@ Ext.define('PartKeepr.PartEditorWindow', { /* Width and height settings */ width: 600, minWidth: 600, - minHeight: 415, - height: 415, + minHeight: 435, + height: 435, saveText: i18n("Save"), cancelText: i18n("Cancel"), diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartParameterValueEditor.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartParameterValueEditor.js @@ -11,6 +11,7 @@ Ext.define("PartKeepr.PartParameterValueEditor", { { fieldLabel: i18n("Parameter Name"), name: 'name', + itemId: 'partParameter', xtype: 'PartParameterComboBox' }, { @@ -105,9 +106,21 @@ Ext.define("PartKeepr.PartParameterValueEditor", { value: [] }); this.down("#valueType").on("change", this.onTypeChange, this); + this.down("#partParameter").on("select", this.onPartParameterSelect, this); this.down("#save").on("click", this.onSave, this); this.down("#unit").on("change", this.onUnitChange, this); }, + onPartParameterSelect: function (combo, record) { + + if (record.get("unitName") !== null) { + var unit = this.down("#unit").getStore().findRecord("name", record.get("unitName"), 0, false, true, true); + + if (unit instanceof PartKeepr.UnitBundle.Entity.Unit) { + this.down("#unit").select(unit); + this.down("#valueType").setValue({valueType: "numeric"}); + } + } + }, onUnitChange: function (combo, newValue) { var prefixes,j, unitFilter = []; diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartFilterPanel.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartFilterPanel.js @@ -30,7 +30,9 @@ Ext.define('PartKeepr.PartFilterPanel', { /** * Fixed body background color style */ - bodyStyle: 'background:#DBDBDB;', + //bodyStyle: 'background:#DBDBDB;', + + ui: 'default-framed', partManager: null, storageLocationFilter: null, @@ -136,6 +138,7 @@ Ext.define('PartKeepr.PartFilterPanel', { xtype: 'toolbar', enableOverflow: true, dock: 'bottom', + ui: 'footer', defaults: {minWidth: 100}, items: [this.applyButton, this.resetButton] } diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsGrid.js @@ -164,6 +164,18 @@ Ext.define('PartKeepr.PartsGrid', { this.topToolbar.insert(2, this.addFromTemplateButton); } + this.createMetaPartButton = Ext.create("Ext.button.Button", { + iconCls: 'web-icon bricks', + text: i18n("Add Meta-Part"), + handler: function () + { + this.fireEvent("addMetaPart"); + }, + scope: this + }); + + this.topToolbar.insert(1, this.createMetaPartButton); + this.mapSearchHotkey(); }, @@ -247,6 +259,12 @@ Ext.define('PartKeepr.PartsGrid', { tooltip: i18n("Needs Review?"), renderer: this.reviewRenderer }, { + text: '<span class="web-icon bricks"></span>', + dataIndex: "metaPart", + width: 30, + tooltip: i18n("Meta Part"), + renderer: this.metaPartRenderer + }, { header: i18n("Name"), dataIndex: 'name', flex: 1, @@ -302,12 +320,13 @@ Ext.define('PartKeepr.PartsGrid', { }, { header: i18n("Internal ID"), dataIndex: '@id', - renderer: function (value) { + renderer: function (value) + { var values = value.split("/"); - var idstr = values[values.length - 1]; - var idint = parseInt(idstr); + var idstr = values[values.length - 1]; + var idint = parseInt(idstr); - return idstr + " (#"+idint.toString(36)+")"; + return idstr + " (#" + idint.toString(36) + ")"; } } @@ -385,6 +404,19 @@ Ext.define('PartKeepr.PartsGrid', { return ret; }, /** + * Used as renderer for the meta part column. + */ + metaPartRenderer: function (val, q, rec) + { + var ret = ""; + + if (rec.get("metaPart") === true) { + ret += '<span class="web-icon bricks" title="' + i18n("Meta Part") + '"></span>'; + } + + return ret; + }, + /** * Sets the category. Triggers a store reload with a category filter. */ setCategory: function (category) diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsManager.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsManager.js @@ -96,6 +96,7 @@ Ext.define('PartKeepr.PartManager', { this.grid.on("itemDeselect", this.onItemSelect, this); this.grid.on("itemAdd", this.onItemAdd, this); this.grid.on("itemDelete", this.onItemDelete, this); + this.grid.on("addMetaPart", this.onAddMetaPart, this); this.grid.on("duplicateItemWithBasicData", this.onDuplicateItemWithBasicData, this); this.grid.on("duplicateItemWithAllData", this.onDuplicateItemWithAllData, this); this.tree.on("syncCategory", this.onSyncCategory, this); @@ -399,6 +400,32 @@ Ext.define('PartKeepr.PartManager', { j.editor.editItem(newItem); j.show(); }, + onAddMetaPart: function () { + var defaults; + var j = Ext.create("PartKeepr.Components.Part.Editor.MetaPartEditorWindow", { + + }); + + var defaultPartUnit = PartKeepr.getApplication().getPartUnitStore().findRecord("default", true); + + Ext.apply(defaults, {metaPart: true}); + + var record = Ext.create("PartKeepr.PartBundle.Entity.Part", { + metaPart: true + }); + + if (this.getSelectedCategory() !== null) { + record.setCategory(this.getSelectedCategory()); + } else { + record.setCategory(this.tree.getRootNode().firstChild); + } + + record.setPartUnit(defaultPartUnit); + + j.editor.editItem(record); + j.editor.on("partSaved", this.onNewPartSaved, this); + j.show(); + }, /** * Creates a part duplicate from the given record and opens the editor window. * @param rec The record to duplicate @@ -470,10 +497,17 @@ Ext.define('PartKeepr.PartManager', { */ onEditPart: function (part) { - var j = Ext.create("PartKeepr.PartEditorWindow"); - j.editor.on("partSaved", this.onPartSaved, this); - j.editor.editItem(part); - j.show(); + var editorWindow; + + if (part.get("metaPart") === true) { + editorWindow = Ext.create("PartKeepr.Components.Part.Editor.MetaPartEditorWindow"); + } else { + editorWindow = Ext.create("PartKeepr.PartEditorWindow"); + } + + editorWindow.editor.on("partSaved", this.onPartSaved, this); + editorWindow.editor.editItem(part); + editorWindow.show(); }, onNewPartSaved: function () { diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Project/ProjectPartGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Project/ProjectPartGrid.js @@ -15,6 +15,47 @@ Ext.define('PartKeepr.ProjectPartGrid', { minValue: 1 } }, { + header: i18n("Overage Type"), dataIndex: 'overageType', + wdith: 50, + editor: { + xtype: 'combobox', + store: { + fields: ["overageType", "description"], + data: [{overageType: 'percent', description: i18n("Percent")}, + {overageType: 'absolute', description: i18n("Absolute")}] + }, + displayField: 'description', + valueField: 'overageType', + queryMode: 'local', + editable: false, + forceSelection: true, + allowBlank: false, + }, + renderer: function (v) { + if (v === "percent") { + return i18n("Percent"); + } else { + return i18n("Absolute"); + } + } + },{ + header: i18n("Overage"), dataIndex: 'overage', + wdith: 50, + editor: { + xtype: 'numberfield', + allowBlank: false, + minValue: 1 + }, + renderer: function (v,m,rec) { + if (rec.get("overageType") === "percent") { + return v + " %"; + } else { + if (rec.getPart() !== null) { + return v + " " + rec.getPart().getPartUnit().get("shortName"); + } + } + } + },{ header: i18n("Part"), dataIndex: 'part', flex: 1, @@ -23,10 +64,16 @@ Ext.define('PartKeepr.ProjectPartGrid', { }, renderer: function (val, p, rec) { - var part = rec.getPart(); + var part = rec.getPart(), icon; if (part !== null) { - return Ext.util.Format.htmlEncode(part.get("name")); + if (part.get("metaPart")) { + icon = "bricks"; + } else { + icon = "brick"; + } + return '<span class="web-icon ' + icon + '"></span> ' + Ext.util.Format.htmlEncode( + part.get("name")); } } }, { diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Project/ProjectReport.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Project/ProjectReport.js @@ -1,3 +1,13 @@ +Ext.define('PartKeepr.Foo', { + extend: "Ext.grid.Panel", + xtype: 'gridfoo', + + parentRecord: null, + setParentRecord: function (v) + { + this.parentRecord = v; + } +}); /** * Represents the project report view */ @@ -14,6 +24,8 @@ Ext.define('PartKeepr.ProjectReportView', { layout: 'border', + reportedProjects: [], + initComponent: function () { this.createStores(); @@ -33,7 +45,8 @@ Ext.define('PartKeepr.ProjectReportView', { header: i18n("Quantity"), dataIndex: 'quantity', width: 50, editor: { - xtype: 'numberfield' + xtype: 'numberfield', + minValue: 0 } }, { header: i18n("Project Name"), dataIndex: 'name', @@ -56,104 +69,210 @@ Ext.define('PartKeepr.ProjectReportView', { } }); - this.reportResult = Ext.create("PartKeepr.BaseGrid", { - flex: 1, - features: [ - { - ftype: 'summary' - } - ], + this.subGridEditing = Ext.create('Ext.grid.plugin.CellEditing', { + clicksToEdit: 1, + listeners: { + edit: this.onAfterSubGridEdit, + scope: this + } + }); + + this.subGrid = { + xtype: 'gridfoo', + autoLoad: false, + bind: { + store: '{record.subParts}', + parentRecord: '{record}' + }, + plugins: [this.subGridEditing], columns: [ { - header: i18n("Quantity"), dataIndex: 'quantity', - width: 50 - }, { - header: i18n("Part Name"), - renderer: function (val, p, rec) - { - return rec.getPart().get("name"); + text: i18n("Use"), + xtype: 'checkcolumn', + listeners: { + checkchange: this.onCheckStateChange, + scope: this }, - flex: 1 - }, { - header: i18n("Part Description"), - renderer: function (val, p, rec) - { - return rec.getPart().get("description"); - }, - flex: 1 - }, { - header: i18n("Remarks"), - dataIndex: 'remarks', - flex: 1 - }, { - header: i18n("Projects"), - dataIndex: 'projects', - flex: 1 - }, { - header: i18n("Storage Location"), dataIndex: 'storageLocation_name', - width: 100 + dataIndex: 'use' + }, + { + text: i18n("Part Name"), + dataIndex: "name" }, { - header: i18n("Available"), dataIndex: 'available', - width: 75 + text: i18n("Description"), + dataIndex: "description" }, { - header: i18n("Distributor"), - dataIndex: 'distributor', - renderer: function (val, p, rec) + text: i18n("Production Remarks"), + dataIndex: "productionRemarks" + },{ + text: i18n("Storage Location"), + renderer: function (v, m, r) { - if (rec.getDistributor() !== null) { - return rec.getDistributor().get("name"); - } - }, - flex: 1, - editor: { - xtype: 'DistributorComboBox', - returnObject: true, - triggerAction: 'query', - ignoreQuery: true, - forceSelection: true, - editable: false + return r.get("storageLocation.name"); } }, { - header: i18n("Distributor Order Number"), dataIndex: 'distributor_order_number', - flex: 1, + text: i18n("Stock Level"), + dataIndex: 'stockLevel' + }, { + text: i18n("Stock to use"), + dataIndex: 'stockToUse', editor: { - xtype: 'textfield' + field: { + xtype: 'numberfield' + } } - }, { - header: i18n("Price per Item"), dataIndex: 'price', - renderer: PartKeepr.getApplication().formatCurrency, - width: 100 - }, { - header: i18n("Sum"), - dataIndex: 'sum', - renderer: PartKeepr.getApplication().formatCurrency, - summaryType: 'sum', - summaryRenderer: PartKeepr.getApplication().formatCurrency, - width: 100 - }, { - header: i18n("Amount to Order"), dataIndex: 'missing', - width: 100 - }, { - header: i18n("Sum (Order)"), - dataIndex: 'sum_order', - renderer: PartKeepr.getApplication().formatCurrency, - summaryType: 'sum', - summaryRenderer: PartKeepr.getApplication().formatCurrency, - width: 100 } ], - store: this.projectReportStore, - plugins: [this.editing], bbar: [ - Ext.create("PartKeepr.Exporter.GridExporterButton", { - itemId: 'export', - genericExporter: false, - tooltip: i18n("Export"), - iconCls: "fugue-icon application-export", - disabled: this.store.isLoading() - }) + { + xtype: 'button', + text: i18n("Apply Parts"), + disabled: true, + handler: this.onApplyMetaPartsClick, + scope: this, + itemId: 'applyPartsButton' + } ] - }); + }; + + this.reportResult = Ext.create("PartKeepr.BaseGrid", { + flex: 1, + features: [ + { + ftype: 'summary' + } + ], + plugins: [ + { + ptype: 'rowwidget', + widget: this.subGrid + + }, this.editing + ], + + columns: [ + { + header: i18n("Quantity"), dataIndex: 'quantity', + width: 50, + renderer: function (v, s, rec) + { + var i, total; + + if (rec.get("metaPart")) { + total = 0; + for (i = 0; i < rec.subParts().getCount(); i++) { + if (rec.subParts().getAt(i).get("use")) { + total += rec.subParts().getAt(i).get("stockToUse"); + } + } + + return total + " / " + v; + } else { + return v; + } + } + }, { + header: i18n("Part Name"), + renderer: function (val, p, rec) + { + var part = rec.getPart(), icon; + + if (part !== null) { + if (part.get("metaPart")) { + icon = "bricks"; + } else { + icon = "brick"; + } + return '<span class="web-icon ' + icon + '"></span> ' + Ext.util.Format.htmlEncode( + part.get("name")); + } + }, + flex: 1 + }, { + header: i18n("Part Description"), + renderer: function (val, p, rec) + { + return rec.getPart().get("description"); + }, + flex: 1 + }, { + header: i18n("Remarks"), + dataIndex: 'remarks', + flex: 1 + }, { + header: i18n("Production Remarks"), + dataIndex: 'productionRemarks', + flex: 1 + },{ + header: i18n("Projects"), + dataIndex: 'projectNames', + flex: 1 + }, { + header: i18n("Storage Location"), dataIndex: 'storageLocation_name', + width: 100 + }, { + header: i18n("Available"), dataIndex: 'available', + width: 75 + }, { + header: i18n("Distributor"), + dataIndex: 'distributor', + renderer: function (val, p, rec) + { + if (rec.getDistributor() !== null) { + return rec.getDistributor().get("name"); + } + }, + flex: 1, + editor: { + xtype: 'DistributorComboBox', + returnObject: true, + triggerAction: 'query', + ignoreQuery: true, + forceSelection: true, + editable: false + } + }, { + header: i18n("Distributor Order Number"), dataIndex: 'distributor_order_number', + flex: 1, + editor: { + xtype: 'textfield' + } + }, { + header: i18n("Price per Item"), dataIndex: 'price', + renderer: PartKeepr.getApplication().formatCurrency, + width: 100 + }, { + header: i18n("Sum"), + dataIndex: 'sum', + renderer: PartKeepr.getApplication().formatCurrency, + summaryType: 'sum', + summaryRenderer: PartKeepr.getApplication().formatCurrency, + width: 100 + }, { + header: i18n("Amount to Order"), dataIndex: 'missing', + width: 100 + }, { + header: i18n("Sum (Order)"), + dataIndex: 'sum_order', + renderer: PartKeepr.getApplication().formatCurrency, + summaryType: 'sum', + summaryRenderer: PartKeepr.getApplication().formatCurrency, + width: 100 + } + ], + store: this.projectReportStore, + bbar: [ + Ext.create("PartKeepr.Exporter.GridExporterButton", { + itemId: 'export', + genericExporter: false, + tooltip: i18n("Export"), + iconCls: "fugue-icon application-export", + disabled: this.store.isLoading() + }) + ] + } + ) + ; this.createReportButton = Ext.create('Ext.button.Button', { xtype: 'button', @@ -235,6 +354,75 @@ Ext.define('PartKeepr.ProjectReportView', { this.callParent(); }, + onApplyMetaPartsClick: function (button) { + var parentRecord = button.up("grid").parentRecord; + + this.convertMetaPartsToParts(parentRecord); + }, + convertMetaPartsToParts: function (record) { + var i, projectReportItem, subPart; + + for (i=0;i<record.subParts().getCount();i++) { + subPart = record.subParts().getAt(i); + + if (subPart.get("use")) { + projectReportItem = Ext.create("PartKeepr.ProjectBundle.Entity.ProjectReport"); + projectReportItem.set("quantity", subPart.get("stockToUse")); + projectReportItem.set("storageLocation_name", subPart.getStorageLocation().get("name")); + projectReportItem.set("available", subPart.get("stockLevel")); + projectReportItem.set("missing", subPart.get("stockLevel") - subPart.get("stockToUse")); + projectReportItem.set("projects", record.get("projects")); + projectReportItem.set("projectNames", record.get("projectNames")); + projectReportItem.set("remarks", record.get("remarks")); + projectReportItem.set("productionRemarks", subPart.get("productionRemarks")); + projectReportItem.setPart(subPart); + + this.reportResult.getStore().add(projectReportItem); + } + } + + this.reportResult.getStore().remove(record); + this.reportResult.getView().refresh(); + }, + onCheckStateChange: function (check, rowIndex, checked, record) + { + if (checked) { + if (record.get("stockToUse") == 0 || record.get("stockToUse") === undefined) { + record.set("stockToUse", record.get("stockLevel")); + } + } + + Ext.defer(this.updateSubGrid, 100, this, [check.up("grid")]); + this.reportResult.getView().refresh(); + }, + onAfterSubGridEdit: function (editor, context) + { + if (context.value > context.record.get("stockLevel")) { + context.record.set("stockToUse", context.originalValue); + } else { + context.record.set("stockToUse", context.value); + } + + Ext.defer(this.updateSubGrid, 100, this, [context.grid]); + this.reportResult.getView().refresh(); + }, + updateSubGrid: function (grid) { + var subParts = grid.parentRecord.subParts(); + var i, total; + + total = 0; + for (i = 0; i < subParts.getCount(); i++) { + if (subParts.getAt(i).get("use")) { + total += subParts.getAt(i).get("stockToUse"); + } + } + + if (total >= grid.parentRecord.get("quantity")) { + grid.down("#applyPartsButton").enable(); + } else { + grid.down("#applyPartsButton").disable(); + } + }, /** * Called when the distributor field is about to be edited. * @@ -282,15 +470,18 @@ Ext.define('PartKeepr.ProjectReportView', { for (var i = 0; i < store.count(); i++) { var item = store.getAt(i); + + removals.push({ part: item.getPart().getId(), amount: item.get("quantity"), - comment: item.get("projects") + comment: item.get("projectNames"), + projects: item.get("projects") }); } PartKeepr.PartBundle.Entity.Part.callPostCollectionAction("massRemoveStock", - {"removals": Ext.encode(removals)}); + {"removals": Ext.encode(removals), "projects": Ext.encode(this.reportedProjects)}); } }, onEdit: function (editor, context) @@ -369,6 +560,8 @@ Ext.define('PartKeepr.ProjectReportView', { projects.push({project: selection[i].getId(), quantity: selection[i].get("quantity")}); } + this.reportedProjects = projects; + this.projectReportStore.load({ params: { projects: Ext.encode(projects) @@ -399,4 +592,5 @@ Ext.define('PartKeepr.ProjectReportView', { closable: true, menuPath: [{text: i18n("View")}] } -}); +}) +; diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/ProjectRun/ProjectRunEditor.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/ProjectRun/ProjectRunEditor.js @@ -0,0 +1,104 @@ +/** + * Represents the project editor view + */ +Ext.define('PartKeepr.ProjectRunEditor', { + extend: 'PartKeepr.Editor', + alias: 'widget.ProjectRunEditor', + + defaults: { + anchor: '100%', + labelWidth: 110 + }, + enableButtons: false, + layout: { + type: 'vbox', + align: 'stretch', + pack: 'start' + }, + + /** + * Initializes the component + */ + initComponent: function () + { + /** + * Due to an ExtJS issue, we need to delay the event + * for a bit. + * + * @todo Fix this in a cleaner way + */ + this.on("startEdit", this.onEditStart, this, { + delay: 200 + }); + + var config = {}; + + // Build the initial (empty) store for the project parts + Ext.Object.merge(config, { + autoLoad: false, + model: "PartKeepr.ProjectBundle.Entity.ProjectRunPart", + autoSync: false, // Do not change. If true, new (empty) records would be immediately committed to the database. + remoteFilter: false, + remoteSort: false + }); + + this.store = Ext.create('Ext.data.Store', config); + + this.partGrid = Ext.create("Ext.grid.Panel", { + columns: [ + { + header: i18n("Part Name"), + renderer: function (r, v, rec) + { + console.log(rec); + if (rec.getPart() !== null) { + return rec.getPart().get("name"); + } + return ""; + } + }, { + header: i18n("Qty"), + dataIndex: 'quantity' + } + ], + store: this.store, + }); + + var container = Ext.create("Ext.form.FieldContainer", { + fieldLabel: i18n("Project Parts"), + labelWidth: 110, + layout: 'fit', + flex: 1, + items: this.partGrid + }); + + this.items = [ + { + xtype: 'displayfield', + name: 'project.name', + height: 20, + fieldLabel: i18n("Project Name") + }, { + xtype: 'datefield', + name: 'runDateTime', + fieldLabel: i18n("Run Date/Time"), + readOnly: true + }, + container + ]; + this.callParent(); + + }, + /** + * Bind the store as soon as the view was rendered. + * + * @todo This is a hack, because invocation of this method is delayed. + */ + onEditStart: function () + { + var store = this.record.parts(); + this.partGrid.bindStore(store); + + } +}) +; diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/ProjectRun/ProjectRunEditorComponent.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/ProjectRun/ProjectRunEditorComponent.js @@ -0,0 +1,30 @@ +/** + * Represents the project editor component + */ +Ext.define('PartKeepr.ProjectRunEditorComponent', { + extend: 'PartKeepr.EditorComponent', + alias: 'widget.ProjectRunEditorComponent', + navigationClass: 'PartKeepr.ProjectRunGrid', + editorClass: 'PartKeepr.ProjectRunEditor', + titleProperty: 'project.name', + model: 'PartKeepr.ProjectBundle.Entity.ProjectRun', + initComponent: function () + { + this.createStore({ + sorters: [ + { + property: 'runDateTime', + direction: 'DESC' + } + ] + }); + + this.callParent(); + }, + statics: { + iconCls: 'fugue-icon drill', + title: i18n('Project Runs'), + closable: true, + menuPath: [{ text: i18n("View")}] + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/ProjectRun/ProjectRunGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/ProjectRun/ProjectRunGrid.js @@ -0,0 +1,22 @@ +/** + * Represents the project grid + */ +Ext.define('PartKeepr.ProjectRunGrid', { + extend: 'PartKeepr.EditorGrid', + alias: 'widget.ProjectRunGrid', + columns: [ + {header: i18n("Project Run"), dataIndex: 'runDateTime', flex: 1, xtype: 'datecolumn'}, + { + header: i18n("Project"), renderer: function (r, v, rec) + { + if (rec.getProject() !== null) { + return rec.getProject().get("name"); + } + return ""; + }, flex: 1 + } + ], + automaticPageSize: true, + enableEditing: false +}) +; diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FieldSelectorWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FieldSelectorWindow.js @@ -6,6 +6,9 @@ Ext.define("PartKeepr.Components.Widgets.FieldSelectorWindow", { height: 600, title: i18n("Select Field"), + /* Constrain the window to fit the viewport */ + constrainHeader: true, + config: { sourceModel: null }, diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FilterExpression.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FilterExpression.js @@ -8,10 +8,10 @@ Ext.define("PartKeepr.Widgets.FilterExpression", { pack: 'start' }, - minHeight: 150, - minWidth: 400, - width: 400, - height: 150, + minHeight: 100, + minWidth: 380, + width: 380, + shrinkWrap: 2, requires: [], bbar: [{ @@ -48,14 +48,9 @@ Ext.define("PartKeepr.Widgets.FilterExpression", { items: [ { itemId: "operator", - xtype: 'combobox', - displayField: 'operator', - emptyText: i18n("Select an operator"), - editable: false, - forceSelection: true, - valueField: 'operator', + disabled: true, + xtype: 'OperatorComboBox', flex: 1, - returnObject: true } ] }, { @@ -88,6 +83,7 @@ Ext.define("PartKeepr.Widgets.FilterExpression", { { itemId: 'values', xtype: "grid", + minHeight: 200, store: { fields: ['value'], data: [] @@ -123,9 +119,8 @@ Ext.define("PartKeepr.Widgets.FilterExpression", { initComponent: function () { this.callParent(arguments); - var j = Ext.create("PartKeepr.Data.store.OperatorStore"); + this.down("#operator").on("change", this.onOperatorChange, this); - this.down("#operator").setStore(j); this.down("#selectField").on("click", this.onFieldSelectClick, this); this.down("#selectEntity").on("click", this.onEntitySelectClick, this); this.down("#values").on("selectionchange", this.onValuesSelectionChange, this); @@ -246,6 +241,7 @@ Ext.define("PartKeepr.Widgets.FilterExpression", { }); this.modelFieldSelectorWindow.on("fieldSelect", function (field) { this.updateValueFieldState(field); + this.down("#operator").enable(); this.down("#field").setValue(field.data.data.name); }, this); this.modelFieldSelectorWindow.show(); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FilterExpressionWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FilterExpressionWindow.js @@ -5,6 +5,13 @@ Ext.define("PartKeepr.Widgets.FilterExpressionWindow", { title: i18n("Add Filter Expression"), sourceModel: null, + minHeight: 150, + minWidth: 400, + + + /* Constrain the window to fit the viewport */ + constrainHeader: true, + initComponent: function () { this.items = { diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/OperatorComboBox.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/OperatorComboBox.js @@ -0,0 +1,31 @@ +Ext.define("PartKeepr.Widgets.Components.OperatorComboBox", { + extend: "Ext.form.field.ComboBox", + + xtype: 'OperatorComboBox', + + displayField: 'operator', + emptyText: i18n("Select an operator"), + editable: false, + forceSelection: true, + valueField: 'operator', + returnObject: true, + + tpl: Ext.create('Ext.XTemplate', + '<ul class="x-list-plain"><tpl for=".">', + '<li role="option" class="x-boundlist-item">', + '<span style="display: inline-block; width: 20px; text-align: center; ">{symbol}</span> <small>{description}</small>', + '</li>', + '</tpl></ul>' + ), + + displayTpl: Ext.create('Ext.XTemplate', + '<tpl for=".">', + '{symbol}', + '</tpl>' + ), + + initComponent: function () { + this.callParent(arguments); + this.setStore(Ext.create("PartKeepr.Data.store.OperatorStore")); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PartParameterComboBox.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PartParameterComboBox.js @@ -1,5 +1,5 @@ -Ext.define("PartKeepr.PartParameterComboBox",{ - extend:"Ext.form.field.ComboBox", +Ext.define("PartKeepr.PartParameterComboBox", { + extend: "Ext.form.field.ComboBox", xtype: 'PartParameterComboBox', displayField: 'name', valueField: 'name', @@ -9,7 +9,22 @@ Ext.define("PartKeepr.PartParameterComboBox",{ triggerAction: 'all', forceSelection: false, editable: true, - triggers: { + tpl: Ext.create('Ext.XTemplate', + '<ul class="x-list-plain"><tpl for=".">', + '<li role="option" class="x-boundlist-item">', + '<span style="float: left;" class="web-icon fugue-icon ', + '<tpl if="valueType == \'numeric\'">', + 'edit-number', + '<tpl else>', + 'layer-shape-text', + '</tpl>', + '"></span>{name}', + '<tpl if="unitSymbol != null"> ({unitSymbol})</tpl>', + '<tpl if="description != \'\'"><br/><span style="margin-left: 16px;"><small>{description}</small></span></tpl>', + '</li>', + '</tpl></ul>' + ), + triggers: { reload: { cls: "x-form-reload-trigger", weight: -1, @@ -20,36 +35,40 @@ Ext.define("PartKeepr.PartParameterComboBox",{ scope: 'this' } }, - initComponent: function () { + initComponent: function () + { - this.store = Ext.create("Ext.data.Store", { - fields: [{ name: 'name' }], - autoLoad: false, - proxy: { - type: 'ajax', - url: PartKeepr.getBasePath() + "/api/parts/getPartParameterNames", - reader: { - type: 'json' - } - } - }); + this.store = Ext.create("Ext.data.Store", { + fields: [{name: 'name'}, {name: 'description'}, {name: 'valueType'}, {name: 'unitName'},{name: 'unitSymbol'}], + autoLoad: false, + proxy: { + type: 'ajax', + url: PartKeepr.getBasePath() + "/api/parts/getPartParameterNames", + reader: { + type: 'json' + } + } + }); - /* Workaround to remember the value when loading */ - this.store.on("beforeload", function () { - this._oldValue = this.getValue(); - }, this); + /* Workaround to remember the value when loading */ + this.store.on("beforeload", function () + { + this._oldValue = this.getValue(); + }, this); - /* Set the old value when load is complete */ - this.store.on("load", function () { - this.setValue(this._oldValue); - }, this); - - this.callParent(); - this.store.load(); + /* Set the old value when load is complete */ + this.store.on("load", function () + { + this.setValue(this._oldValue); + }, this); + + this.callParent(); + this.store.load(); }, - setValue: function (val) { - this._oldValue = val; - this.callParent(arguments); + setValue: function (val) + { + this._oldValue = val; + this.callParent(arguments); } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PartParameterSearch.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PartParameterSearch.js @@ -0,0 +1,231 @@ +Ext.define("PartKeepr.Components.Widgets.PartParameterSearch", { + extend: "Ext.panel.Panel", + + layout: { + type: 'vbox', + align: 'stretch', + pack: 'start' + }, + + shrinkWrap: 2, + minHeight: 110, + minWidth: 380, + items: [ + { + xtype: 'PartParameterComboBox', + fieldLabel: i18n("Parameter"), + itemId: 'parameter' + }, + { + xtype: 'fieldcontainer', + fieldLabel: i18n("Operator"), + layout: 'hbox', + items: [ + { + itemId: "operator", + xtype: 'OperatorComboBox', + flex: 1, + disabled: true + } + ] + }, { + xtype: 'fieldcontainer', + fieldLabel: i18n("Value"), + layout: 'card', + flex: 1, + itemId: 'valueCards', + items: [ + { + itemId: 'stringValue', + layout: 'hbox', + border: false, + items: [ + { + disabled: true, + itemId: "textValueField", + xtype: 'combobox', + displayField: 'value', + valueField: 'value', + flex: 1 + } + ] + }, { + itemId: 'value', + layout: 'hbox', + border: false, + items: [ + { + disabled: true, + xtype: 'SiUnitField', + siUnitItemId: 'siPrefix', + itemId: 'valueField', + siFieldName: 'siPrefix', + flex: 1 + }, + { + xtype: 'displayfield', + itemId: 'unitDisplay', + name: 'unitName', + value: '' + } + ] + }, + ] + }, + ], + + bbar: [ + { + xtype: 'button', + itemId: 'apply', + disabled: true, + text: i18n("Apply") + } + ], + + valueType: "", + operatorType: null, + filter: null, + unit: null, + + initComponent: function () + { + this.callParent(); + + this.unitFilter = Ext.create("PartKeepr.util.Filter", { + property: "@id", + operator: "in", + value: [] + }); + + + this.down("#operator").on("change", this.onOperatorChange, this); + this.down("#parameter").on("select", this.onParameterSelect, this); + this.down("#apply").on("click", this.onApplyClick, this); + + this.partParameterValueStore = Ext.create("Ext.data.Store", { + fields: [{name: 'value'}], + autoLoad: false, + proxy: { + extraParams: { + name: "", + value: "" + }, + type: 'ajax', + url: PartKeepr.getBasePath() + "/api/parts/getPartParameterValues", + reader: { + type: 'json' + } + } + }); + + this.down("#textValueField").setStore(this.partParameterValueStore); + }, + onApplyClick: function () + { + var operator; + + if (this.down("#operator").getValue() instanceof Ext.data.Model) { + operator = this.down("#operator").getValue().get("operator"); + } + + var j = Ext.create("PartKeepr.PartBundle.Entity.MetaPartParameterCriteria"); + j.set("partParameterName", this.down("#parameter").getValue()); + j.set("valueType", this.valueType); + j.setSiPrefix(this.down("#siPrefix").getValue()); + j.setUnit(this.unit); + j.set("stringValue", this.down("#textValueField").getValue()); + j.set("operator", operator); + j.set("value", this.down("#valueField").getValue()); + + this.fireEvent("apply", j); + }, + onParameterSelect: function (combo, value) + { + var prefixes, j, unitFilter = []; + + this.valueType = value.get("valueType"); + this.down("#operator").getStore().clearFilter(); + + if (value.get("valueType") === "string") { + this.down("#operator").getStore().filter("string", true); + this.down("#operator").getStore().filter("type", "scalar"); + } else { + this.down("#operator").getStore().filter("numeric", true); + this.down("#operator").getStore().filter("type", "scalar"); + } + + this.down("#siPrefix").getStore().removeFilter(this.unitFilter); + + var unitStore = PartKeepr.getApplication().getUnitStore(); + var unit = unitStore.findRecord("name", value.get("unitName"), 0, false, true, true); + if (unit instanceof PartKeepr.UnitBundle.Entity.Unit) { + this.unit = unit; + prefixes = unit.prefixes().getData(); + + for (j = 0; j < prefixes.getCount(); j++) { + unitFilter.push(prefixes.getAt(j).get("@id")); + } + + this.down("#unitDisplay").setValue(unit.get("name")); + } else { + this.unit = null; + } + + this.unitFilter.setValue(unitFilter); + + this.down("#siPrefix").getStore().addFilter(this.unitFilter); + + this.down("#operator").enable(); + + + this.partParameterValueStore.getProxy().setExtraParams({ + name: value.get("name"), + valueType: value.get("valueType") + }); + this.partParameterValueStore.load(); + this.switchValueCard(); + }, + onOperatorChange: function (combo, value) + { + if (value === null) { + this.operatorType = null; + return; + } + + this.operatorType = value.get("type"); + this.switchValueCard(); + }, + switchValueCard: function () + { + switch (this.operatorType) { + case "scalar": + if (this.valueType === "string") { + this.down("#valueCards").setActiveItem(this.down("#stringValue")); + this.down("#textValueField").enable(); + } else { + this.down("#valueCards").setActiveItem(this.down("#value")); + this.down("#siPrefix").enable(); + this.down("#valueField").enable(); + } + break; + default: + this.down("#valueCards").setActiveItem(this.down("#value")); + this.down("#siPrefix").disable(); + this.down("#valueField").disable(); + } + + this.validateApplyButton(); + }, + validateApplyButton: function () + { + var applyButton = this.down("#apply"); + + if (this.down("#operator").getValue() === null) { + applyButton.setDisabled(true); + return; + } + + applyButton.enable(); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PartParameterSearchWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PartParameterSearchWindow.js @@ -0,0 +1,28 @@ +Ext.define("PartKeepr.Components.Widgets.PartParameterSearchWindow", { + extend: "Ext.window.Window", + modal: true, + layout: 'fit', + title: i18n("Add Parametric Search Expression"), + sourceModel: null, + + minHeight: 150, + minWidth: 400, + + + /* Constrain the window to fit the viewport */ + constrainHeader: true, + + initComponent: function () + { + this.partParameterSearch = Ext.create("PartKeepr.Components.Widgets.PartParameterSearch"); + this.partParameterSearch.on("apply", this.onApply, this); + this.items = this.partParameterSearch; + this.callParent(arguments); + }, + + onApply: function (rec) + { + this.fireEvent("apply", rec); + this.close(); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/SiUnitField.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/SiUnitField.js @@ -15,20 +15,24 @@ Ext.define("PartKeepr.SiUnitField", { extend: "Ext.form.FieldContainer", alias: 'widget.SiUnitField', + xtype: 'SiUnitField', + layout: { type: 'hbox' }, initComponent: function () { - this.items = [ - { + this.numberField = Ext.create({ xtype: 'numberfield', hideTrigger: true, emptyText: i18n("Value"), decimalPrecision: 20, name: this.name, flex: 1 - }, { + }); + + this.items = [ + this.numberField, { xtype: 'SiUnitCombo', itemId: this.siUnitItemId, returnObject: true, @@ -45,5 +49,8 @@ Ext.define("PartKeepr.SiUnitField", { pageSize: 99999999, autoLoad: true })); + }, + getValue: function () { + return this.numberField.getValue(); } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/SiUnitList.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/SiUnitList.js @@ -14,27 +14,6 @@ Ext.define('PartKeepr.SiUnitList', { if (record !== null) { this.pickerField.collapse(); } - console.log(record); return; - // The selection change events won't fire when clicking on the selected element. Detect it here. - var me = this, - pickerField = me.pickerField, - valueField = pickerField.valueField, - selected = me.getSelectionModel().getSelection(); - - console.log(valueField); - console.log(me.getSelectionModel()); - if (!pickerField.multiSelect && selected.length) { - selected = selected[0]; - // Not all pickerField's have a collapse API, i.e. Ext.ux.form.MultiSelect. - console.log(selected); - console.log(record.get(valueField)); - console.log(selected.get(valueField)); - - if (selected && pickerField.isEqual(record.get(valueField), selected.get(valueField)) && pickerField.collapse) { - console.log("FOO123"); - pickerField.collapse(); - } - } }, }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/OperatorStore.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/OperatorStore.js @@ -10,80 +10,142 @@ Ext.define("PartKeepr.Data.store.OperatorStore", { storeId: 'OperatorStore', fields: [ + /** + * The operator to use for Ext.util.Filter + */ { + name: 'operator', type: 'string' - }, { + }, + /** + * The symbol to display to the user + */ + { + name: 'symbol', + type: 'string' + }, + /** + * The description, so that the user knows what the operator does + */ + { name: 'description', type: 'string' }, + /** + * The operator type. May be "scalar" or "list" + */ { name: 'type', type: 'string' }, + /** + * Defines if the operator can be used for scalar comparisons + */ { name: 'scalar', type: 'boolean' }, + /** + * Defines if the operator can be used for entity comparisons + */ { name: 'entity', type: 'boolean' + }, + /** + * Defines if the operator can be used for string values + */ + { + name: 'string', + type: 'boolean' + }, + /** + * Defines if the operator can be used for numeric values + */ + { + name: 'numeric', + type: 'boolean' } ], data: [ { operator: "<", + symbol: "<", description: i18n("Less than"), type: 'scalar', scalar: true, + string: false, + numeric: true, entity: false }, { operator: ">", + symbol: ">", description: i18n("Greater than"), type: 'scalar', scalar: true, + string: false, + numeric: true, entity: false }, { operator: "=", + symbol: "=", description: i18n("Equals"), type: 'scalar', scalar: true, + string: true, + numeric: true, entity: true }, { operator: ">=", + symbol: "≥", description: i18n("Greater than or equals"), type: 'scalar', scalar: true, + string: false, + numeric: true, entity: false }, { operator: "<=", + symbol: "≤", description: i18n("Less than or equals"), type: 'scalar', scalar: true, + string: false, + numeric: true, entity: false }, { operator: "!=", + symbol: "≠", description: i18n("Not equals"), type: 'scalar', scalar: true, + string: true, + numeric: true, entity: true }, { operator: "in", + symbol: "IN", description: i18n("Matches a list"), type: 'list', scalar: true, + string: true, + numeric: true, entity: true }, { operator: "like", + symbol: "%%", description: i18n("Matches a subtext with wildcards (%)"), type: 'scalar', scalar: true, + string: true, + numeric: false, entity: false } ] diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Models/ProjectReport.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Models/ProjectReport.js @@ -11,12 +11,22 @@ Ext.define("PartKeepr.ProjectBundle.Entity.ProjectReport", { {name: 'distributor_order_number', type: 'string'}, {name: 'sum_order', type: 'float'}, {name: 'sum', type: 'float'}, + {name: 'projectNames', type: 'string'}, {name: 'projects', type: 'string'}, {name: 'remarks', type: 'string'}, + {name: 'productionRemarks', type: 'string'}, {name: 'part', reference: 'PartKeepr.PartBundle.Entity.Part'}, {name: 'distributor', reference: 'PartKeepr.DistributorBundle.Entity.Distributor'} ], + hasMany: [ + { + name: 'subParts', + associationKey: 'subParts', + model: 'PartKeepr.PartBundle.Entity.Part' + } + ], + proxy: { type: "Hydra", url: '/api/project_reports' diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Models/ProjectReportPart.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Models/ProjectReportPart.js @@ -0,0 +1,8 @@ +Ext.define("PartKeepr.ProjectBundle.Entity.ProjectReportPart", { + extend: "PartKeepr.PartBundle.Entity.Part", + + constructor: function () { + this.fields.push({name: 'stockToUse', type: 'int'}); + this.callParent(arguments); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/PartKeepr.js b/src/PartKeepr/FrontendBundle/Resources/public/js/PartKeepr.js @@ -660,7 +660,7 @@ Ext.application({ format.currencySign = PartKeepr.getApplication().getUserPreference("partkeepr.formatting.currency.symbol", "€"); if (code !== null) { - var currency = this.getCurrencyStore().findRecord("code", code, 0, false, false, true); + var currency = PartKeepr.getApplication().getCurrencyStore().findRecord("code", code, 0, false, false, true); if (currency !== null) { format.currencySign = currency.get("symbol"); diff --git a/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig b/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig @@ -155,6 +155,7 @@ '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.tree.View-missingMethods.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.form.Basic-AssociationSupport.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.ux.TreePicker-setValueWithObject.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Widgets/OperatorComboBox.js' '@PartKeeprFrontendBundle/Resources/public/js/Actions/BaseAction.js' '@PartKeeprFrontendBundle/Resources/public/js/Actions/LogoutAction.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Statusbar.js' @@ -165,6 +166,9 @@ '@PartKeeprFrontendBundle/Resources/public/js/Components/Part/PartDisplay.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Part/PartStockWindow.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Part/PartFilterPanel.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Part/Editor/MetaPartEditorWindow.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Widgets/PartParameterSearch.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Widgets/PartParameterSearchWindow.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/MenuBar.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Grid/BaseGrid.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Part/Editor/PartParameterGrid.js' @@ -233,6 +237,7 @@ '@PartKeeprFrontendBundle/Resources/public/js/Components/User/UserEditor.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/SystemNotice/SystemNoticeEditor.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationEditor.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Part/Editor/MetaPartEditor.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Project/ProjectEditor.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Editor/EditorComponent.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Distributor/DistributorEditorComponent.js' @@ -279,6 +284,9 @@ '@PartKeeprFrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/RequiredPartDistributorFields.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/BarcodeScannerConfiguration.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/ActionsConfiguration.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/ProjectRun/ProjectRunEditor.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/ProjectRun/ProjectRunGrid.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/ProjectRun/ProjectRunEditorComponent.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/BarcodeScanner/Manager.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/BarcodeScanner/Action.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/BarcodeScanner/ActionsComboBox.js' diff --git a/src/PartKeepr/PartBundle/Action/GetPartsAction.php b/src/PartKeepr/PartBundle/Action/GetPartsAction.php @@ -0,0 +1,80 @@ +<?php +namespace PartKeepr\PartBundle\Action; + +use Doctrine\ORM\EntityManager; +use Dunglas\ApiBundle\Action\ActionUtilTrait; +use Dunglas\ApiBundle\Exception\RuntimeException; +use Dunglas\ApiBundle\Model\DataProviderInterface; +use PartKeepr\PartBundle\Entity\Part; +use PartKeepr\PartBundle\Services\PartService; +use Symfony\Component\HttpFoundation\Request; + +/** + * Default API action retrieving a collection of resources. + * + * @author Kévin Dunglas <dunglas@gmail.com> + */ +class GetPartsAction +{ + use ActionUtilTrait; + + /** + * @var DataProviderInterface + */ + private $dataProvider; + + /** + * @var EntityManager + */ + private $em; + + /** + * @var PartService + */ + private $partService; + + public function __construct(DataProviderInterface $dataProvider, EntityManager $em, PartService $partService) + { + $this->dataProvider = $dataProvider; + $this->em = $em; + $this->partService = $partService; + } + + /** + * Retrieves a collection of resources. + * + * @param Request $request + * + * @return array|\Dunglas\ApiBundle\Model\PaginatorInterface|\Traversable + * + * @throws RuntimeException + */ + public function __invoke(Request $request) + { + list($resourceType) = $this->extractAttributes($request); + + $items = $this->dataProvider->getCollection($resourceType); + + /** + * @var $part Part + */ + foreach ($items as $part) { + if ($part->isMetaPart()) { + $sum = 0; + + $parts = $this->partService->getMatchingMetaParts($part); + + foreach ($parts as $matchingPart) { + /** + * @var $matchingPart Part + */ + $sum += $matchingPart->getStockLevel(); + } + + $part->setStockLevel($sum); + + } + } + return $items; + } +} diff --git a/src/PartKeepr/PartBundle/Controller/PartController.php b/src/PartKeepr/PartBundle/Controller/PartController.php @@ -6,6 +6,8 @@ use Dunglas\ApiBundle\Api\IriConverter; use FOS\RestBundle\Controller\Annotations\View; use FOS\RestBundle\Controller\FOSRestController; use PartKeepr\PartBundle\Entity\Part; +use PartKeepr\ProjectBundle\Entity\ProjectRun; +use PartKeepr\ProjectBundle\Entity\ProjectRunPart; use PartKeepr\StockBundle\Entity\StockEntry; use Sensio\Bundle\FrameworkExtraBundle\Configuration as Routing; use Symfony\Component\HttpFoundation\Request; @@ -22,16 +24,39 @@ class PartController extends FOSRestController */ public function massRemoveStockAction(Request $request) { + /** + * @var IriConverter + */ + $iriConverter = $this->get('api.iri_converter'); + $removals = json_decode($request->get('removals')); if (!is_array($removals)) { throw new \Exception('removals parameter must be an array'); } + $projects = json_decode($request->get('projects')); + + if (!is_array($projects)) { + throw new \Exception('projects parameter must be an array'); + } + /** - * @var IriConverter + * @var $projectRuns ProjectRun[] */ - $iriConverter = $this->get('api.iri_converter'); + $projectRuns = []; + + foreach ($projects as $projectInfo) { + $project = $iriConverter->getItemFromIri($projectInfo->project); + + $projectRun = new ProjectRun(); + $projectRun->setQuantity($projectInfo->quantity); + $projectRun->setRunDateTime(new \DateTime()); + $projectRun->setProject($project); + + $projectRuns[$projectInfo->project] = $projectRun; + } + $user = $this->get('partkeepr.userservice')->getUser(); @@ -58,6 +83,20 @@ class PartController extends FOSRestController } $part->addStockLevel($stock); + + $projectRunPart = new ProjectRunPart(); + $projectRunPart->setPart($part); + $projectRunPart->setQuantity($removal->amount); + + foreach ($projectRuns as $projectRun) { + $projectRun->addPart($projectRunPart); + } + } + + + foreach ($projectRuns as $projectRun) { + var_dump($projectRun); + $this->get('doctrine.orm.entity_manager')->persist($projectRun); } $this->get('doctrine.orm.entity_manager')->flush(); @@ -71,10 +110,41 @@ class PartController extends FOSRestController */ public function getParameterNamesAction() { - $dql = "SELECT p.name, p.description FROM PartKeepr\PartBundle\Entity\PartParameter p GROUP BY p.name, p.description"; + $dql = "SELECT p.name, p.description, p.valueType, u.name AS unitName, u.symbol AS unitSymbol FROM PartKeepr\PartBundle\Entity\PartParameter p LEFT JOIN p.unit u GROUP BY p.name, p.description, p.valueType, u.name"; $query = $this->get("doctrine.orm.default_entity_manager")->createQuery($dql); return $query->getArrayResult(); } + + /** + * @Routing\Route("/api/parts/getPartParameterValues", defaults={"method" = "get","_format" = "json"}) + * @View() + * + * @return array An array with name and description properties + */ + public function getParameterValuesAction (Request $request) { + if (!$request->query->has("name")) { + throw new \InvalidArgumentException("The parameter 'name' must be given"); + } + + if (!$request->query->has("valueType")) { + throw new \InvalidArgumentException("The parameter 'valueType' must be given"); + } + + if ($request->query->get("valueType") == "string") { + $dql = "SELECT p.stringValue AS value FROM PartKeepr\PartBundle\Entity\PartParameter p WHERE p.name = :name AND p.valueType = :valueType GROUP BY p.stringValue"; + $query = $this->get("doctrine.orm.default_entity_manager")->createQuery($dql); + $query->setParameter("name", $request->query->get("name")); + $query->setParameter("valueType", $request->query->get("valueType")); + return $query->getArrayResult(); + } else { + $dql = "SELECT p.value FROM PartKeepr\PartBundle\Entity\PartParameter p WHERE p.name = :name AND p.valueType = :valueType GROUP BY p.value"; + + $query = $this->get("doctrine.orm.default_entity_manager")->createQuery($dql); + $query->setParameter("name", $request->query->get("name")); + $query->setParameter("valueType", $request->query->get("valueType")); + return $query->getArrayResult(); + } + } } diff --git a/src/PartKeepr/PartBundle/Entity/MetaPartParameterCriteria.php b/src/PartKeepr/PartBundle/Entity/MetaPartParameterCriteria.php @@ -0,0 +1,257 @@ +<?php +namespace PartKeepr\PartBundle\Entity; + + +use Doctrine\ORM\Mapping as ORM; +use PartKeepr\CoreBundle\Entity\BaseEntity; +use PartKeepr\SiPrefixBundle\Entity\SiPrefix; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * @ORM\Entity() + */ +class MetaPartParameterCriteria extends BaseEntity +{ + /** + * @ORM\ManyToOne(targetEntity="PartKeepr\PartBundle\Entity\Part", inversedBy="metaPartParameterCriterias") + * + * @var Part + */ + private $part; + + /** + * @ORM\Column(type="string") + * @Groups({"default"}) + * + * @var string + */ + private $partParameterName; + + /** + * @ORM\Column(type="string") + * @Groups({"default"}) + * + * @var string + */ + private $operator; + + /** + * @ORM\Column(type="float",nullable=true) + * @Groups({"default"}) + * + * @var float + */ + private $value; + + /** + * @ORM\Column(type="float",nullable=true) + * @var float + */ + private $normalizedValue; + + /** + * The SiPrefix of the unit. + * + * @ORM\ManyToOne(targetEntity="PartKeepr\SiPrefixBundle\Entity\SiPrefix") + * @Groups({"default"}) + * + * @var SiPrefix + */ + private $siPrefix; + + /** + * @ORM\Column(type="string") + * @Groups({"default"}) + * + * @var string + */ + private $stringValue; + + /** + * The type of the value. + * + * @ORM\Column(type="string") + * @Groups({"default"}) + * + * @var string + */ + private $valueType; + + /** + * The unit for this type. May be null. + * + * @ORM\ManyToOne(targetEntity="PartKeepr\UnitBundle\Entity\Unit") + * @Groups({"default"}) + * + * @var \PartKeepr\UnitBundle\Entity\Unit + */ + private $unit; + + public function __construct() + { + $this->setValueType(PartParameter::VALUE_TYPE_STRING); + } + + /** + * @return float + */ + public function getNormalizedValue() + { + return $this->normalizedValue; + } + + /** + * @param float $normalizedValue + */ + public function setNormalizedValue($normalizedValue) + { + $this->normalizedValue = $normalizedValue; + } + + protected function recalculateNormalizedValue () { + if ($this->getSiPrefix() === null) { + $this->setNormalizedValue($this->getValue()); + } else { + $this->setNormalizedValue($this->getSiPrefix()->calculateProduct($this->getValue())); + } + } + + /** + * @return \PartKeepr\UnitBundle\Entity\Unit + */ + public function getUnit() + { + return $this->unit; + } + + /** + * @param \PartKeepr\UnitBundle\Entity\Unit $unit + */ + public function setUnit($unit = null) + { + $this->unit = $unit; + } + + /** + * @return string + */ + public function getValueType() + { + // Fallback to numeric if legacy parameter + if (!in_array($this->valueType, PartParameter::VALUE_TYPES)) { + return PartParameter::VALUE_TYPE_NUMERIC; + } + + return $this->valueType; + } + + /** + * @param string $valueType + */ + public function setValueType($valueType) + { + if (!in_array($valueType, PartParameter::VALUE_TYPES)) { + throw new \Exception("Invalid value type given:".$valueType); + } + + $this->valueType = $valueType; + } + + /** + * @return SiPrefix + */ + public function getSiPrefix() + { + return $this->siPrefix; + } + + /** + * @param SiPrefix $siPrefix + */ + public function setSiPrefix($siPrefix) + { + $this->siPrefix = $siPrefix; + $this->recalculateNormalizedValue(); + } + + /** + * @return Part + */ + public function getPart() + { + return $this->part; + } + + /** + * @param Part $part + */ + public function setPart($part = null) + { + $this->part = $part; + } + + /** + * @return string + */ + public function getPartParameterName() + { + return $this->partParameterName; + } + + /** + * @param string $partParameterName + */ + public function setPartParameterName($partParameterName) + { + $this->partParameterName = $partParameterName; + } + + /** + * @return string + */ + public function getOperator() + { + return $this->operator; + } + + /** + * @param string $operator + */ + public function setOperator($operator) + { + $this->operator = $operator; + } + + /** + * @return float + */ + public function getValue() + { + return $this->value; + } + + /** + * @param float $value + */ + public function setValue($value) + { + $this->value = $value; + $this->recalculateNormalizedValue(); + } + + /** + * @return string + */ + public function getStringValue() + { + return $this->stringValue; + } + + /** + * @param string $stringValue + */ + public function setStringValue($stringValue) + { + $this->stringValue = $stringValue; + } +} diff --git a/src/PartKeepr/PartBundle/Entity/Part.php b/src/PartKeepr/PartBundle/Entity/Part.php @@ -74,7 +74,6 @@ class Part extends BaseEntity * in "pieces", "meters" or "grams". * * @ORM\ManyToOne(targetEntity="PartKeepr\PartBundle\Entity\PartMeasurementUnit", inversedBy="parts") - * @Assert\NotNull() * @Groups({"default"}) * * @var PartMeasurementUnit @@ -85,7 +84,6 @@ class Part extends BaseEntity * Defines the storage location of this part. * * @ORM\ManyToOne(targetEntity="PartKeepr\StorageLocationBundle\Entity\StorageLocation") - * @Assert\NotNull() * @Groups({"default"}) * * @var StorageLocation @@ -95,7 +93,8 @@ class Part extends BaseEntity /** * Holds the manufacturers which can manufacture this part. * - * @ORM\OneToMany(targetEntity="PartKeepr\PartBundle\Entity\PartManufacturer",mappedBy="part",cascade={"persist", "remove"}, orphanRemoval=true) + * @ORM\OneToMany(targetEntity="PartKeepr\PartBundle\Entity\PartManufacturer",mappedBy="part",cascade={"persist", "remove"}, + * orphanRemoval=true) * @Groups({"default"}) * * @var ArrayCollection @@ -105,7 +104,8 @@ class Part extends BaseEntity /** * Holds the distributors from where we can buy the part. * - * @ORM\OneToMany(targetEntity="PartKeepr\PartBundle\Entity\PartDistributor",mappedBy="part",cascade={"persist", "remove"}, orphanRemoval=true) + * @ORM\OneToMany(targetEntity="PartKeepr\PartBundle\Entity\PartDistributor",mappedBy="part",cascade={"persist", "remove"}, + * orphanRemoval=true) * @Groups({"default"}) * * @var ArrayCollection @@ -186,6 +186,17 @@ class Part extends BaseEntity private $parameters; /** + * The meta part parameter criterias for this part. + * + * @ORM\OneToMany(targetEntity="PartKeepr\PartBundle\Entity\MetaPartParameterCriteria", + * mappedBy="part",cascade={"persist", "remove"}, orphanRemoval=true) + * @Groups({"default"}) + * + * @var ArrayCollection + */ + private $metaPartParameterCriterias; + + /** * The part status for this part. * * @ORM\Column(type="string",nullable=true) @@ -216,6 +227,16 @@ class Part extends BaseEntity private $partCondition; /** + * Defines the production remarks for a part + * + * @ORM\Column(type="string",nullable=true) + * @Groups({"default"}) + * + * @var string + */ + private $productionRemarks; + + /** * The create date+time for this part. * * @ORM\Column(type="datetime",nullable=true) @@ -258,6 +279,24 @@ class Part extends BaseEntity */ private $lowStock = false; + /** + * Defines if the part is a meta-part + * + * @ORM\Column(type="boolean") + * @Groups({"default"}) + * + * @var boolean + */ + private $metaPart; + + /** + * An array of all matching meta parts + * @Groups({"default"}) + * + * @var array + */ + private $metaPartMatches; + public function __construct() { $this->distributors = new ArrayCollection(); @@ -266,8 +305,10 @@ class Part extends BaseEntity $this->attachments = new ArrayCollection(); $this->stockLevels = new ArrayCollection(); $this->projectParts = new ArrayCollection(); + $this->metaPartParameterCriterias = new ArrayCollection(); $this->setCreateDate(new \DateTime()); $this->setNeedsReview(false); + $this->setMetaPart(false); } /** @@ -281,6 +322,38 @@ class Part extends BaseEntity } /** + * @return string + */ + public function getProductionRemarks() + { + return $this->productionRemarks; + } + + /** + * @param string $productionRemarks + */ + public function setProductionRemarks($productionRemarks) + { + $this->productionRemarks = $productionRemarks; + } + + /** + * @return array + */ + public function getMetaPartMatches() + { + return $this->metaPartMatches; + } + + /** + * @param array $metaPartMatches + */ + public function setMetaPartMatches($metaPartMatches) + { + $this->metaPartMatches = $metaPartMatches; + } + + /** * @return bool */ public function isLowStock() @@ -381,7 +454,7 @@ class Part extends BaseEntity * * @param PartMeasurementUnit $partUnit The part unit object to set */ - public function setPartUnit(PartMeasurementUnit $partUnit) + public function setPartUnit(PartMeasurementUnit $partUnit = null) { $this->partUnit = $partUnit; } @@ -521,6 +594,16 @@ class Part extends BaseEntity } /** + * Returns the meta part parameter criterias assigned to this part. + * + * @return MetaPartParameterCriteria[] An array of MetaPartParameterCriteria objects + */ + public function getMetaPartParameterCriterias() + { + return $this->metaPartParameterCriterias->getValues(); + } + + /** * Returns the create date. * * @return \DateTime The create date+time @@ -609,7 +692,7 @@ class Part extends BaseEntity */ private function checkStorageLocationConsistency() { - if ($this->getStorageLocation() === null) { + if ($this->getStorageLocation() === null && !$this->isMetaPart()) { throw new StorageLocationNotAssignedException(); } } @@ -625,67 +708,37 @@ class Part extends BaseEntity } /** - * @param mixed $removals - */ - public function setRemovals($removals = false) - { - $this->removals = $removals; - } - - /** - * Returns all stock entries. + * Sets the storage location for this part. * - * @return ArrayCollection + * @param \PartKeepr\StorageLocationBundle\Entity\StorageLocation $storageLocation The storage location */ - public function getStockLevels() + public function setStorageLocation(StorageLocation $storageLocation = null) { - return $this->stockLevels->getValues(); + $this->storageLocation = $storageLocation; } /** - * Returns the minimum stock level. - * - * @return int + * @return bool */ - public function getMinStockLevel() + public function isMetaPart() { - return $this->minStockLevel; + return $this->metaPart; } /** - * Set the minimum stock level for this part. - * - * Only positive values are allowed. - * - * @param int $minStockLevel A minimum stock level, only values >= 0 are allowed. - * - * @throws MinStockLevelOutOfRangeException If the passed stock level is not in range (>=0) + * @param bool $metaPart */ - public function setMinStockLevel($minStockLevel) + public function setMetaPart($metaPart) { - $minStockLevel = intval($minStockLevel); - - if ($minStockLevel < 0) { - throw new MinStockLevelOutOfRangeException(); - } - - $this->minStockLevel = $minStockLevel; - - if ($this->getStockLevel() < $this->getMinStockLevel()) { - $this->setLowStock(true); - } else { - $this->setLowStock(false); - } + $this->metaPart = $metaPart; } /** - * Sets the average price for this part. - * - * @param float $price The price to set + * @param mixed $removals */ - public function setAveragePrice($price) + public function setRemovals($removals = false) { - $this->averagePrice = $price; + $this->removals = $removals; } /** @@ -699,13 +752,13 @@ class Part extends BaseEntity } /** - * Sets the storage location for this part. + * Sets the average price for this part. * - * @param \PartKeepr\StorageLocationBundle\Entity\StorageLocation $storageLocation The storage location + * @param float $price The price to set */ - public function setStorageLocation(StorageLocation $storageLocation) + public function setAveragePrice($price) { - $this->storageLocation = $storageLocation; + $this->averagePrice = $price; } /** @@ -723,26 +776,6 @@ class Part extends BaseEntity } /** - * Returns the stock level. - * - * @return int The stock level - */ - public function getStockLevel() - { - return $this->stockLevel; - } - - /** - * Sets the stock level. - * - * @param $stockLevel int The stock level to set - */ - public function setStockLevel($stockLevel) - { - $this->stockLevel = $stockLevel; - } - - /** * Adds a new stock entry to this part. * * @param StockEntry $stockEntry @@ -789,6 +822,30 @@ class Part extends BaseEntity } /** + * Adds a Meta Part Parameter Criteria + * + * @param MetaPartParameterCriteria $metaPartParameterCriteria A meta part parameter criteria to + */ + public function addMetaPartParameterCriteria($metaPartParameterCriteria) + { + if ($metaPartParameterCriteria instanceof MetaPartParameterCriteria) { + $metaPartParameterCriteria->setPart($this); + } + $this->metaPartParameterCriterias->add($metaPartParameterCriteria); + } + + /** + * Removes a Part Parameter. + * + * @param MetaPartParameterCriteria $metaPartParameterCriteria A meta part parameter criteria to remove + */ + public function removeMetaPartParameterCriteria($metaPartParameterCriteria) + { + $metaPartParameterCriteria->setPart(null); + $this->metaPartParameterCriterias->removeElement($metaPartParameterCriteria); + } + + /** * Adds a Part Attachment. * * @param PartAttachment $partAttachment An attachment to add @@ -933,4 +990,70 @@ class Part extends BaseEntity $this->setLowStock(false); } } + + /** + * Returns all stock entries. + * + * @return ArrayCollection + */ + public function getStockLevels() + { + return $this->stockLevels->getValues(); + } + + /** + * Returns the minimum stock level. + * + * @return int + */ + public function getMinStockLevel() + { + return $this->minStockLevel; + } + + /** + * Set the minimum stock level for this part. + * + * Only positive values are allowed. + * + * @param int $minStockLevel A minimum stock level, only values >= 0 are allowed. + * + * @throws MinStockLevelOutOfRangeException If the passed stock level is not in range (>=0) + */ + public function setMinStockLevel($minStockLevel) + { + $minStockLevel = intval($minStockLevel); + + if ($minStockLevel < 0) { + throw new MinStockLevelOutOfRangeException(); + } + + $this->minStockLevel = $minStockLevel; + + if ($this->getStockLevel() < $this->getMinStockLevel()) { + $this->setLowStock(true); + } else { + $this->setLowStock(false); + } + } + + /** + * Returns the stock level. + * + * @return int The stock level + */ + public function getStockLevel() + { + return $this->stockLevel; + } + + /** + * Sets the stock level. + * + * @param $stockLevel int The stock level to set + */ + public function setStockLevel($stockLevel) + { + $this->stockLevel = $stockLevel; + } } diff --git a/src/PartKeepr/PartBundle/Entity/PartParameter.php b/src/PartKeepr/PartBundle/Entity/PartParameter.php @@ -74,6 +74,14 @@ class PartParameter extends BaseEntity private $value; /** + * The normalized value (the product of si prefix + value) + * + * @ORM\Column(type="float",nullable=true) + * @var + */ + private $normalizedValue; + + /** * The maximum value of the parameter. * * @ORM\Column(type="float",name="maximumValue",nullable=true) @@ -84,6 +92,14 @@ class PartParameter extends BaseEntity private $maxValue; /** + * The normalized maximum value (the product of si prefix + value) + * + * @ORM\Column(type="float",nullable=true) + * @var + */ + private $normalizedMaxValue; + + /** * The minimum value of the parameter. * * @ORM\Column(type="float",name="minimumValue",nullable=true) @@ -94,6 +110,14 @@ class PartParameter extends BaseEntity private $minValue; /** + * The normalized minimum value (the product of si prefix + value) + * + * @ORM\Column(type="float",nullable=true) + * @var + */ + private $normalizedMinValue; + + /** * The string value if the parameter is a string. * * @ORM\Column(type="string") @@ -149,67 +173,51 @@ class PartParameter extends BaseEntity } /** - * @return SiPrefix + * @return mixed */ - public function getMinSiPrefix() + public function getNormalizedValue() { - return $this->minSiPrefix; + return $this->normalizedValue; } /** - * @param SiPrefix $minSiPrefix + * @param mixed $normalizedValue */ - public function setMinSiPrefix($minSiPrefix) + public function setNormalizedValue($normalizedValue) { - $this->minSiPrefix = $minSiPrefix; + $this->normalizedValue = $normalizedValue; } /** - * @return SiPrefix + * @return mixed */ - public function getMaxSiPrefix() + public function getNormalizedMaxValue() { - return $this->maxSiPrefix; + return $this->normalizedMaxValue; } /** - * @param SiPrefix $maxSiPrefix + * @param mixed $normalizedMaxValue */ - public function setMaxSiPrefix($maxSiPrefix) + public function setNormalizedMaxValue($normalizedMaxValue) { - $this->maxSiPrefix = $maxSiPrefix; + $this->normalizedMaxValue = $normalizedMaxValue; } /** - * @return float + * @return mixed */ - public function getMaxValue() + public function getNormalizedMinValue() { - return $this->maxValue; + return $this->normalizedMinValue; } /** - * @param float $maxValue + * @param mixed $normalizedMinValue */ - public function setMaxValue($maxValue) + public function setNormalizedMinValue($normalizedMinValue) { - $this->maxValue = $maxValue; - } - - /** - * @return float - */ - public function getMinValue() - { - return $this->minValue; - } - - /** - * @param float $minValue - */ - public function setMinValue($minValue) - { - $this->minValue = $minValue; + $this->normalizedMinValue = $normalizedMinValue; } /** @@ -233,6 +241,11 @@ class PartParameter extends BaseEntity */ public function getValueType() { + // Fallback to numeric if legacy parameter + if (!in_array($this->valueType, self::VALUE_TYPES)) { + return self::VALUE_TYPE_NUMERIC; + } + return $this->valueType; } @@ -244,7 +257,7 @@ class PartParameter extends BaseEntity public function setValueType($valueType) { if (!in_array($valueType, self::VALUE_TYPES)) { - throw new \Exception("Invalid value type given"); + throw new \Exception("Invalid value type given:".$valueType); } $this->valueType = $valueType; @@ -330,6 +343,48 @@ class PartParameter extends BaseEntity $this->part = $part; } + protected function recalculateNormalizedValues() + { + if ($this->getSiPrefix() === null) { + $this->setNormalizedValue($this->getValue()); + } else { + $this->setNormalizedValue($this->getSiPrefix()->calculateProduct($this->getValue())); + } + + if ($this->getMinSiPrefix() === null) { + $this->setNormalizedMinValue($this->getMinValue()); + } else { + $this->setNormalizedMinValue($this->getMinSiPrefix()->calculateProduct($this->getMinValue())); + } + + if ($this->getMaxSiPrefix() === null) { + $this->setNormalizedMaxValue($this->getMaxValue()); + } else { + $this->setNormalizedMaxValue($this->getMaxSiPrefix()->calculateProduct($this->getMaxValue())); + } + } + + /** + * Returns the si prefix for this parameter. + * + * @return \PartKeepr\SiPrefixBundle\Entity\SiPrefix the si prefix or null + */ + public function getSiPrefix() + { + return $this->siPrefix; + } + + /** + * Sets the si prefix for this parameter. + * + * @param \PartKeepr\SiPrefixBundle\Entity\SiPrefix $prefix The prefix to set, or null + */ + public function setSiPrefix(SiPrefix $prefix = null) + { + $this->siPrefix = $prefix; + $this->recalculateNormalizedValues(); + } + /** * Returns the value. * @@ -348,25 +403,74 @@ class PartParameter extends BaseEntity public function setValue($value) { $this->value = $value; + $this->recalculateNormalizedValues(); } /** - * Returns the si prefix for this parameter. - * - * @return \PartKeepr\SiPrefixBundle\Entity\SiPrefix the si prefix or null + * @return SiPrefix */ - public function getSiPrefix() + public function getMinSiPrefix() { - return $this->siPrefix; + return $this->minSiPrefix; } /** - * Sets the si prefix for this parameter. - * - * @param \PartKeepr\SiPrefixBundle\Entity\SiPrefix $prefix The prefix to set, or null + * @param SiPrefix $minSiPrefix */ - public function setSiPrefix(SiPrefix $prefix = null) + public function setMinSiPrefix($minSiPrefix) { - $this->siPrefix = $prefix; + $this->minSiPrefix = $minSiPrefix; + $this->recalculateNormalizedValues(); + } + + /** + * @return float + */ + public function getMinValue() + { + return $this->minValue; + } + + /** + * @param float $minValue + */ + public function setMinValue($minValue) + { + $this->minValue = $minValue; + $this->recalculateNormalizedValues(); + } + + /** + * @return SiPrefix + */ + public function getMaxSiPrefix() + { + return $this->maxSiPrefix; + } + + /** + * @param SiPrefix $maxSiPrefix + */ + public function setMaxSiPrefix($maxSiPrefix) + { + $this->maxSiPrefix = $maxSiPrefix; + $this->recalculateNormalizedValues(); + } + + /** + * @return float + */ + public function getMaxValue() + { + return $this->maxValue; + } + + /** + * @param float $maxValue + */ + public function setMaxValue($maxValue) + { + $this->maxValue = $maxValue; + $this->recalculateNormalizedValues(); } } diff --git a/src/PartKeepr/PartBundle/Exceptions/NotAMetaPartException.php b/src/PartKeepr/PartBundle/Exceptions/NotAMetaPartException.php @@ -0,0 +1,12 @@ +<?php + + +namespace PartKeepr\PartBundle\Exceptions; + + +class NotAMetaPartException extends \RuntimeException +{ + public function __construct () { + parent::__construct("Attempted to retrieve parts for a meta part, but the given part is not a meta part!"); + } +} diff --git a/src/PartKeepr/PartBundle/Resources/config/actions.xml b/src/PartKeepr/PartBundle/Resources/config/actions.xml @@ -34,5 +34,11 @@ <argument type="service" id="api.data_provider"/> <argument type="service" id="partkeepr.part_measurement_unit_service"/> </service> + <service id="partkeepr.parts.collection_get" + class="PartKeepr\PartBundle\Action\GetPartsAction"> + <argument type="service" id="api.data_provider"/> + <argument type="service" id="doctrine.orm.default_entity_manager"/> + <argument type="service" id="partkeepr.part_service"/> + </service> </services> </container> diff --git a/src/PartKeepr/PartBundle/Resources/config/services.xml b/src/PartKeepr/PartBundle/Resources/config/services.xml @@ -5,16 +5,18 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> - <service id="partkeepr.part_measurement_unit_service" class="PartKeepr\PartBundle\Services\PartMeasurementUnitService"> + <service id="partkeepr.part_measurement_unit_service" + class="PartKeepr\PartBundle\Services\PartMeasurementUnitService"> <argument type="service" id="doctrine.orm.default_entity_manager"/> </service> <service id="partkeepr.part.category_service" class="PartKeepr\CategoryBundle\Services\CategoryService"> - <argument type="service" id="doctrine.orm.default_entity_manager" /> + <argument type="service" id="doctrine.orm.default_entity_manager"/> <argument>PartKeepr\PartBundle\Entity\PartCategory</argument> </service> <service id="partkeepr.part_service" class="PartKeepr\PartBundle\Services\PartService"> - <argument type="service" id="doctrine.orm.default_entity_manager" /> + <argument type="service" id="doctrine.orm.default_entity_manager"/> + <argument type="service" id="doctrine_filter_service"/> <argument type="string">%partkeepr.parts.limit%</argument> <argument type="string">%partkeepr.parts.internalpartnumberunique%</argument> </service> diff --git a/src/PartKeepr/PartBundle/Services/PartService.php b/src/PartKeepr/PartBundle/Services/PartService.php @@ -3,7 +3,11 @@ namespace PartKeepr\PartBundle\Services; use Doctrine\ORM\EntityManager; +use PartKeepr\DoctrineReflectionBundle\Filter\Filter; +use PartKeepr\DoctrineReflectionBundle\Services\FilterService; use PartKeepr\PartBundle\Entity\Part; +use PartKeepr\PartBundle\Entity\PartParameter; +use PartKeepr\PartBundle\Exceptions\NotAMetaPartException; class PartService { @@ -26,12 +30,19 @@ class PartService */ private $entityManager; + /** + * @var FilterService + */ + private $filterService; + public function __construct( EntityManager $entityManager, + FilterService $filterService, $partLimit = false, $checkInternalPartNumberUniqueness = false ) { $this->entityManager = $entityManager; + $this->filterService = $filterService; $this->partLimit = $partLimit; $this->checkInternalPartNumberUniqueness = $checkInternalPartNumberUniqueness; } @@ -101,4 +112,73 @@ class PartService return false; } + + public function getMatchingMetaParts(Part $metaPart) + { + $paramCount = 0; + $paramPrefix = ":param"; + $results = []; + + if (!$metaPart->isMetaPart()) { + throw new NotAMetaPartException(); + } + + + foreach ($metaPart->getMetaPartParameterCriterias() as $metaPartParameterCriteria) { + $qb = $this->entityManager->createQueryBuilder(); + $qb->select("p.id AS id") + ->from("PartKeeprPartBundle:PartParameter", "pp") + ->join("pp.part", "p") + ->where("1=1"); + + $filter = new Filter(); + $filter->setOperator($metaPartParameterCriteria->getOperator()); + $filter->setProperty("name"); + + switch ($metaPartParameterCriteria->getValueType()) { + case PartParameter::VALUE_TYPE_NUMERIC: + $expr = $this->filterService->getExpressionForFilter($filter, "pp.normalizedValue", + $paramPrefix.$paramCount); + + $qb->setParameter($paramPrefix.$paramCount, $metaPartParameterCriteria->getNormalizedValue()); + $paramCount++; + break; + case PartParameter::VALUE_TYPE_STRING: + $expr = $this->filterService->getExpressionForFilter($filter, "pp.stringValue", + $paramPrefix.$paramCount); + $qb->setParameter($paramPrefix.$paramCount, $metaPartParameterCriteria->getStringValue()); + $paramCount++; + break; + default: + throw new \InvalidArgumentException("Unknown value type"); + } + + $expr2 = $qb->expr()->eq("pp.name", $paramPrefix.$paramCount); + $qb->setParameter($paramPrefix.$paramCount, $metaPartParameterCriteria->getPartParameterName()); + + $qb->andWhere( + $qb->expr()->andX($expr, $expr2)); + + $result = []; + foreach ($qb->getQuery()->getScalarResult() as $partId) { + $result[] = $partId["id"]; + } + + $results[] = $result; + } + + if (count($results) > 1) { + $result = call_user_func_array("array_intersect", $results); + } else { + $result = $results; + } + + $qb = $this->entityManager->createQueryBuilder(); + $qb->select("p")->from("PartKeeprPartBundle:Part", "p") + ->where( + $qb->expr()->in("p.id", ":result")); + + $qb->setParameter(":result", $result); + return $qb->getQuery()->getResult(); + } } diff --git a/src/PartKeepr/ProjectBundle/Controller/ProjectReportController.php b/src/PartKeepr/ProjectBundle/Controller/ProjectReportController.php @@ -5,6 +5,8 @@ namespace PartKeepr\ProjectBundle\Controller; use Dunglas\ApiBundle\Api\IriConverter; use FOS\RestBundle\Controller\Annotations\View; use FOS\RestBundle\Controller\FOSRestController; +use PartKeepr\PartBundle\Entity\Part; +use PartKeepr\ProjectBundle\Entity\ProjectPart; use Sensio\Bundle\FrameworkExtraBundle\Configuration as Routing; use Symfony\Component\HttpFoundation\Request; @@ -57,20 +59,34 @@ class ProjectReportController extends FOSRestController $aPartResults = []; foreach ($projects as $report) { - $dql = 'SELECT pp.quantity, pro.name AS projectname, pp.remarks, p.id FROM '; + $dql = 'SELECT pp.quantity, pro.name AS projectname, pp.overage, pp.overageType, pp.remarks, p.id FROM '; $dql .= 'PartKeepr\\ProjectBundle\\Entity\\ProjectPart pp JOIN pp.part p '; $dql .= 'JOIN pp.project pro WHERE pp.project = :project'; $query = $this->get('doctrine.orm.entity_manager')->createQuery($dql); $query->setParameter('project', $report['project']); + $projectIRI = $iriConverter->getIriFromItem($report['project']); + foreach ($query->getArrayResult() as $result) { $part = $partRepository->find($result['id']); + /** + * @var $part Part + */ + + if ($result["overageType"] === ProjectPart::OVERAGE_TYPE_PERCENT) { + $overage = $result['quantity'] * $report['quantity'] * ($result["overage"] / 100); + } else { + $overage = $result["overage"]; + } if (array_key_exists($result['id'], $aPartResults)) { // Only update the quantity of the part - $aPartResults[$result['id']]['quantity'] += $result['quantity'] * $report['quantity']; - $aPartResults[$result['id']]['projects'][] = $result['projectname']; + + $aPartResults[$result['id']]['quantity'] += ($result['quantity'] * $report['quantity']) + $overage; + $aPartResults[$result['id']]['projectNames'][] = $result['projectname']; + $aPartResults[$result['id']]['projects'][] = $projectIRI; + if ($result['remarks'] != '') { $aPartResults[$result['id']]['remarks'][] = $result['projectname'].': '.$result['remarks']; @@ -80,14 +96,39 @@ class ProjectReportController extends FOSRestController $part, 'jsonld' ); + + $storageLocationName = ""; + + if ($part->getStorageLocation() !== null) { + $storageLocationName = $part->getStorageLocation()->getName(); + } + + $subParts = []; + + if ($part->isMetaPart()) { + + $matchingParts = $this->container->get("partkeepr.part_service")->getMatchingMetaParts($part); + foreach ($matchingParts as $matchingPart) { + $subParts[] = $this->get('serializer')->normalize( + $matchingPart, + 'jsonld' + ); + } + } + + // Create a full resultset $aPartResults[$result['id']] = [ - 'quantity' => $result['quantity'] * $report['quantity'], + 'quantity' => ($result['quantity'] * $report['quantity']) + $overage, 'part' => $serializedData, - 'storageLocation_name' => $part->getStorageLocation()->getName(), + 'storageLocation_name' => $storageLocationName, 'available' => $part->getStockLevel(), 'sum_order' => 0, - 'projects' => [$result['projectname']], + 'projectNames' => [$result['projectname']], + 'projects' => [$projectIRI], + 'subParts' => $subParts, + 'metaPart' => $part->isMetaPart(), + 'productionRemarks' => $part->getProductionRemarks(), 'remarks' => [], ]; @@ -110,7 +151,8 @@ class ProjectReportController extends FOSRestController $partResult['missing'] = $missing; $partResult['remarks'] = implode(', ', $partResult['remarks']); - $partResult['projects'] = implode(', ', $partResult['projects']); + $partResult['projectNames'] = implode(', ', $partResult['projectNames']); + $partResult['projects'] = json_encode($partResult['projects']); $aFinalResult[] = $partResult; } diff --git a/src/PartKeepr/ProjectBundle/DataFixtures/ProjectFixtureLoader.php b/src/PartKeepr/ProjectBundle/DataFixtures/ProjectFixtureLoader.php @@ -14,10 +14,14 @@ class ProjectFixtureLoader extends AbstractFixture $projectPart1 = new ProjectPart(); $projectPart1->setPart($this->getReference('part.1')); $projectPart1->setQuantity(1); + $projectPart1->setOverageType(ProjectPart::OVERAGE_TYPE_ABSOLUTE); + $projectPart1->setOverage(0); $projectPart2 = new ProjectPart(); $projectPart2->setPart($this->getReference('part.2')); $projectPart2->setQuantity(1); + $projectPart2->setOverageType(ProjectPart::OVERAGE_TYPE_ABSOLUTE); + $projectPart2->setOverage(0); $project = new Project(); $project->setName('FOOBAR'); diff --git a/src/PartKeepr/ProjectBundle/Entity/ProjectPart.php b/src/PartKeepr/ProjectBundle/Entity/ProjectPart.php @@ -18,6 +18,12 @@ use Symfony\Component\Validator\Constraints as Assert; */ class ProjectPart extends BaseEntity { + const OVERAGE_TYPE_ABSOLUTE = "absolute"; + + const OVERAGE_TYPE_PERCENT = "percent"; + + const OVERAGE_TYPES = [self::OVERAGE_TYPE_ABSOLUTE, self::OVERAGE_TYPE_PERCENT]; + /** * The part this project part refers to. * @@ -45,7 +51,6 @@ class ProjectPart extends BaseEntity * Specifies the project which belongs to this project part. * * @ORM\ManyToOne(targetEntity="PartKeepr\ProjectBundle\Entity\Project", inversedBy="parts") - * @Groups({"default"}) * @Assert\NotNull() * * @var Project @@ -63,6 +68,66 @@ class ProjectPart extends BaseEntity private $remarks; /** + * The overage type + * + * @ORM\Column(type="string") + * @Groups({"default"}) + * + * @var string + */ + private $overageType; + + /** + * Specifies the overage, which can either be percent or an absolute value depending on overageType + * + * @ORM\Column(type="integer") + * @Groups({"default"}) + * + * @var integer + */ + private $overage; + + /** + * @return string + */ + public function getOverageType() + { + if (!in_array($this->overageType, self::OVERAGE_TYPES)) { + return self::OVERAGE_TYPE_ABSOLUTE; + } + + return $this->overageType; + } + + /** + * @param string $overageType + */ + public function setOverageType($overageType) + { + if (!in_array($overageType, self::OVERAGE_TYPES)) { + $overageType = self::OVERAGE_TYPE_ABSOLUTE; + } + + $this->overageType = $overageType; + } + + /** + * @return int + */ + public function getOverage() + { + return $this->overage; + } + + /** + * @param int $overage + */ + public function setOverage($overage) + { + $this->overage = $overage; + } + + /** * Returns the part which belongs to this entry. * * @return Part diff --git a/src/PartKeepr/ProjectBundle/Entity/ProjectRun.php b/src/PartKeepr/ProjectBundle/Entity/ProjectRun.php @@ -0,0 +1,148 @@ +<?php +namespace PartKeepr\ProjectBundle\Entity; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\Mapping as ORM; +use PartKeepr\CoreBundle\Entity\BaseEntity; +use PartKeepr\DoctrineReflectionBundle\Annotation\TargetService; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * Represents a project. + * + * @ORM\Entity + * @TargetService("/api/project_runs") + */ +class ProjectRun extends BaseEntity +{ + /** + * Stores the date and time of a project run + * + * @ORM\Column(type="datetime") + * @Groups({"default"}) + * + * @var \DateTime + */ + private $runDateTime; + + /** + * Stores the project used in a production run + * + * @ORM\ManyToOne(targetEntity="PartKeepr\ProjectBundle\Entity\Project") + * @Groups({"default"}) + * + * @var Project + */ + private $project; + + /** + * Stores the quantity this project has been build + * + * @ORM\Column(type="integer") + * @Groups({"default"}) + * + * @var integer + */ + private $quantity; + + /** + * Stores the parts + * @ORM\OneToMany( + * targetEntity="PartKeepr\ProjectBundle\Entity\ProjectRunPart", + * mappedBy="projectRun", + * cascade={"persist", "remove"}, + * orphanRemoval=true) + * + * @Groups({"default"}) + * + * @var ArrayCollection + */ + private $parts; + + public function __construct () { + $this->parts = new ArrayCollection(); + } + + /** + * @return int + */ + public function getQuantity() + { + return $this->quantity; + } + + /** + * @param int $quantity + */ + public function setQuantity($quantity) + { + $this->quantity = $quantity; + } + + /** + * @return \DateTime + */ + public function getRunDateTime() + { + return $this->runDateTime; + } + + /** + * @param \DateTime $runDateTime + */ + public function setRunDateTime($runDateTime) + { + $this->runDateTime = $runDateTime; + } + + /** + * @return Project + */ + public function getProject() + { + return $this->project; + } + + /** + * @param Project $project + */ + public function setProject($project) + { + $this->project = $project; + } + + /** + * @return ArrayCollection + */ + public function getParts() + { + return $this->parts->getValues(); + } + + /** + * Adds a project run part + * + * @param ProjectRunPart + */ + public function addPart($part) + { + if ($part instanceof ProjectRunPart) { + $part->setProjectRun($this); + } + $this->parts->add($part); + } + + /** + * Removes a project run part + * + * @param ProjectRunPart + */ + public function removePart ($part) + { + if ($part instanceof ProjectRunPart) { + $part->setProjectRun(null); + } + $this->parts->removeElement($part); + } + +} diff --git a/src/PartKeepr/ProjectBundle/Entity/ProjectRunPart.php b/src/PartKeepr/ProjectBundle/Entity/ProjectRunPart.php @@ -0,0 +1,94 @@ +<?php +namespace PartKeepr\ProjectBundle\Entity; + +use PartKeepr\PartBundle\Entity\Part; +use Doctrine\ORM\Mapping as ORM; +use PartKeepr\CoreBundle\Entity\BaseEntity; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * Represents a project run part. + * + * @ORM\Entity + */ +class ProjectRunPart extends BaseEntity +{ + + /** + * Stores the project run + * + * @ORM\ManyToOne(targetEntity="PartKeepr\ProjectBundle\Entity\ProjectRun") + * @Groups({"default"}) + * + * @var ProjectRun + */ + private $projectRun; + + /** + * Stores the part used in a production run + * + * @ORM\ManyToOne(targetEntity="PartKeepr\PartBundle\Entity\Part") + * @Groups({"default"}) + * + * @var Part + */ + private $part; + + /** + * Stores the quantity of a production run + * + * @ORM\Column(type="integer") + * @Groups({"default"}) + * + * @var integer + */ + private $quantity; + + /** + * @return ProjectRun + */ + public function getProjectRun() + { + return $this->projectRun; + } + + /** + * @param ProjectRun $projectRun + */ + public function setProjectRun($projectRun) + { + $this->projectRun = $projectRun; + } + + /** + * @return Part + */ + public function getPart() + { + return $this->part; + } + + /** + * @param Part $part + */ + public function setPart($part) + { + $this->part = $part; + } + + /** + * @return int + */ + public function getQuantity() + { + return $this->quantity; + } + + /** + * @param int $quantity + */ + public function setQuantity($quantity) + { + $this->quantity = $quantity; + } +} diff --git a/src/PartKeepr/ProjectBundle/Tests/ProjectTest.php b/src/PartKeepr/ProjectBundle/Tests/ProjectTest.php @@ -7,6 +7,7 @@ use PartKeepr\CoreBundle\Tests\WebTestCase; use PartKeepr\PartBundle\Entity\Part; use PartKeepr\ProjectBundle\Entity\Project; use PartKeepr\ProjectBundle\Entity\ProjectAttachment; +use PartKeepr\ProjectBundle\Entity\ProjectPart; use Symfony\Component\HttpFoundation\File\UploadedFile; class ProjectTest extends WebTestCase @@ -80,11 +81,15 @@ class ProjectTest extends WebTestCase 'quantity' => 1, 'part' => $serializedPart1, 'remarks' => 'testremark', + 'overageType' => ProjectPart::OVERAGE_TYPE_ABSOLUTE, + 'overage' => 0 ], [ 'quantity' => 2, 'part' => $serializedPart2, 'remarks' => 'testremark2', + 'overageType' => ProjectPart::OVERAGE_TYPE_ABSOLUTE, + 'overage' => 0 ], ], ]; @@ -204,4 +209,37 @@ class ProjectTest extends WebTestCase $this->assertEquals(0, count($response->attachments)); } + + /** + * Tests that the project part does not contain a reference to the project. This is because we serialize the + * project reference as IRI and not as object, which causes problems when reading in the project part in the + * frontend and serializing it back. + * + */ + public function testAbsentProjectReference () { + $client = static::makeClient(true); + + $project = $this->fixtures->getReference('project'); + + $iriConverter = $this->getContainer()->get('api.iri_converter'); + $iri = $iriConverter->getIriFromItem($project); + + $client->request( + 'GET', + $iri, + [], + [], + [] + ); + + $project = json_decode($client->getResponse()->getContent()); + + $this->assertObjectHasAttribute("parts", $project); + $this->assertInternalType("array", $project->parts); + + foreach ($project->parts as $part) { + $this->assertObjectNotHasAttribute("project", $part); + } + + } } diff --git a/src/PartKeepr/SetupBundle/Services/ConfigSetupService.php b/src/PartKeepr/SetupBundle/Services/ConfigSetupService.php @@ -84,6 +84,7 @@ class ConfigSetupService 'partkeepr.parts.limit' => false, 'partkeepr.users.limit' => false, 'partkeepr.parts.internalpartnumberunique' => false, + 'partkeepr.octopart.apikey' => "", ]; if (function_exists('apc_fetch')) { diff --git a/src/PartKeepr/SetupBundle/Services/FootprintSetupService.php b/src/PartKeepr/SetupBundle/Services/FootprintSetupService.php @@ -139,7 +139,7 @@ class FootprintSetupService $category = new FootprintCategory(); $category->setParent($parentNode); $category->setName($name); - $parentNode->getChildren()->add($category); + $parentNode->getChildren()[] = $category; $this->entityManager->persist($category); } diff --git a/src/PartKeepr/SetupBundle/Services/UnitSetupService.php b/src/PartKeepr/SetupBundle/Services/UnitSetupService.php @@ -71,7 +71,7 @@ class UnitSetupService throw new \Exception('Unable to find SI Prefix '.$name); } - $unit->getPrefixes()->add($prefix); + $unit->getPrefixes()[] = $prefix; } } $this->entityManager->persist($unit); diff --git a/src/PartKeepr/StatisticBundle/Services/StatisticService.php b/src/PartKeepr/StatisticBundle/Services/StatisticService.php @@ -198,7 +198,7 @@ class StatisticService $snapshotUnit->setStockLevel(0); } - $snapshot->getUnits()->add($snapshotUnit); + $snapshot->getUnits()[] = $snapshotUnit; } $this->entityManager->persist($snapshot);