Compare commits

..

77 Commits

Author SHA1 Message Date
Jan Böhmer
9313f870bc Bumped version to 1.2.0 2023-03-18 22:29:59 +01:00
dependabot[bot]
9e72e88930 Bump symfonycorp/security-checker-action from 4 to 5 (#246)
Bumps [symfonycorp/security-checker-action](https://github.com/symfonycorp/security-checker-action) from 4 to 5.
- [Release notes](https://github.com/symfonycorp/security-checker-action/releases)
- [Commits](https://github.com/symfonycorp/security-checker-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: symfonycorp/security-checker-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-18 22:27:01 +01:00
Jan Böhmer
dcb64bf0a6 Merge remote-tracking branch 'origin/master' 2023-03-18 22:26:40 +01:00
Jan Böhmer
5d07070558 Do not build docker images for pull requests 2023-03-18 22:26:36 +01:00
dependabot[bot]
8c6ba9175b Bump actions/checkout from 2 to 3 (#247)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-18 22:25:31 +01:00
dependabot[bot]
ccaa2c48e2 Bump github/codeql-action from 1 to 2 (#248)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-18 22:25:07 +01:00
Jan Böhmer
5d38bf2e66 Use Github dependabot to check for outdated github actions 2023-03-18 22:20:04 +01:00
Jan Böhmer
15331da389 Removed actions updater workflow, as it is not compatiblee with our auto generated jekyll page build action 2023-03-18 22:17:28 +01:00
Jan Böhmer
477171abac Fixed actions updater workflow 2023-03-18 22:11:50 +01:00
Jan Böhmer
dc85e4f4a4 Run actions updater on every push 2023-03-18 22:05:30 +01:00
Jan Böhmer
ac402a6697 Updated some github actions workflows and added an workflow to automatically update actions 2023-03-18 22:03:34 +01:00
Jan Böhmer
f86d35f8d1 Dont disable the table multi action submit button, when user can not change parts as we use it for exporting and label generation too 2023-03-18 21:52:29 +01:00
Jan Böhmer
7d6c04e3cf Improved documentation 2023-03-18 21:41:00 +01:00
Jan Böhmer
5c059ce9fe Merge remote-tracking branch 'origin/l10n_master' 2023-03-18 20:36:09 +01:00
Jan Böhmer
575bffe0bf New translations messages.en.xlf (German) 2023-03-18 20:27:32 +01:00
Jan Böhmer
d0b70253fa New translations messages.en.xlf (German) 2023-03-18 20:06:47 +01:00
Jan Böhmer
5f04b2649f Updated dependencies. 2023-03-18 19:54:27 +01:00
Jan Böhmer
f0099859bb New translations messages.en.xlf (English) 2023-03-17 00:46:48 +01:00
Jan Böhmer
906b654afa Bumped version to 1.2.0-dev 2023-03-17 00:11:53 +01:00
Jan Böhmer
14740fad58 Merge branch 'part_import' 2023-03-17 00:11:16 +01:00
Jan Böhmer
e97a149474 Fixed static analysis issues 2023-03-17 00:11:01 +01:00
Jan Böhmer
c1d1270d59 Added documentation for BOM import 2023-03-17 00:08:49 +01:00
Jan Böhmer
e550918d7c Added links to bom import to project edit and info page 2023-03-16 23:56:46 +01:00
Jan Böhmer
f3449babc1 Added bom import to ApplicationAvailabilityFunctionalTest 2023-03-16 23:39:28 +01:00
Jan Böhmer
e444388517 Added tests for PCBnew BOM type 2023-03-16 23:32:12 +01:00
Jan Böhmer
bd2559c37b Added the basic possibility to import KiCAD BOMs into projects 2023-03-16 00:05:46 +01:00
Jan Böhmer
7abf44e893 Merge branch 'master' into part_import 2023-03-15 23:01:04 +01:00
Jan Böhmer
0b94a31d15 New translations messages.en.xlf (English) 2023-03-15 22:38:00 +01:00
Jan Böhmer
989e09b610 New translations messages.en.xlf (Russian) 2023-03-15 22:37:57 +01:00
Jan Böhmer
7e69e80290 New translations messages.en.xlf (Japanese) 2023-03-15 22:37:54 +01:00
Jan Böhmer
a3177dcfaf New translations messages.en.xlf (German) 2023-03-15 22:37:50 +01:00
Jan Böhmer
10e54d7a2d New translations messages.en.xlf (French) 2023-03-15 22:37:47 +01:00
Jan Böhmer
ed514a01bb Fixed exception when attachment file is not openable 2023-03-15 22:15:30 +01:00
Jan Böhmer
47fce4e914 Updated composer dependencies 2023-03-15 21:59:33 +01:00
Jan Böhmer
54276e19e9 Merge branch 'part_import' 2023-03-15 21:52:08 +01:00
Jan Böhmer
193650efd4 Added option to mark all imported parts as "needs review" 2023-03-15 21:46:14 +01:00
Jan Böhmer
b7aae7d87b Improved documentation and added example CSV file 2023-03-15 21:33:18 +01:00
Jan Böhmer
2c799d894b Fixed static analysis issues 2023-03-15 21:05:30 +01:00
Jan Böhmer
5745fc1046 Make import/export documentation a child of usage section 2023-03-14 00:20:44 +01:00
Jan Böhmer
80085abe16 Show better error messages for entity import at admin pages 2023-03-14 00:19:10 +01:00
Jan Böhmer
fe5dd065ed Added tests for EntityImporter service 2023-03-14 00:17:13 +01:00
Jan Böhmer
945fd988b3 Added tests for serializer normalizers 2023-03-14 00:02:40 +01:00
Jan Böhmer
3bbff0aecf Fixed errors that prevented import of users 2023-03-13 22:43:26 +01:00
Jan Böhmer
9188331c1e Fixed error popup behavior, when turbo could not find a matching turbo-fram in the response. 2023-03-13 22:39:07 +01:00
Jan Böhmer
be5663c468 Allow import/export of users 2023-03-13 22:16:02 +01:00
Jan Böhmer
9ac8098f15 Deny access to part import tool without permission and added to tools menu 2023-03-13 22:02:55 +01:00
Jan Böhmer
bd5ee837f4 Added permissions for importing data 2023-03-13 21:51:56 +01:00
Jan Böhmer
4be6cb2459 Added documentation on import/export function 2023-03-13 17:42:48 +01:00
Jan Böhmer
c466cb68b9 Allow to import supplier, supplier part number and price via CSV 2023-03-13 01:04:49 +01:00
Jan Böhmer
820be46ed3 Make more fiields importable 2023-03-13 00:52:22 +01:00
Jan Böhmer
4437f206af Allow alternative names for import for parts 2023-03-13 00:44:05 +01:00
Jan Böhmer
a1f4b35749 Explicitly mark our normalizers as cachabel or not 2023-03-13 00:35:31 +01:00
Jan Böhmer
b38f49a90e Added possibility to import storelocation and instock amount 2023-03-13 00:22:46 +01:00
Jan Böhmer
5d318b2693 Removed left over dump tag 2023-03-12 22:10:55 +01:00
Jan Böhmer
c7b9f9e50a Fixed PHPunit tests 2023-03-12 22:07:48 +01:00
Jan Böhmer
256d628543 Allow to control the path delimiter and create unknown datastructures
Also the labeling of form fields was improved
2023-03-12 22:03:02 +01:00
Jan Böhmer
508641d1e8 Added possibility to autoselect the import format 2023-03-12 21:43:40 +01:00
Jan Böhmer
61e2dde400 Allow to import category, footprint and manufacturer by giving a string in the CSV file 2023-03-12 21:10:48 +01:00
Jan Böhmer
85ae862381 Allow to set basic data via import 2023-03-12 20:01:29 +01:00
Jan Böhmer
7a9b7c87a4 Added a very basic import dialog for Parts 2023-03-12 19:53:55 +01:00
Jan Böhmer
8f033910ce Refactored EntityImporter service 2023-03-12 19:16:49 +01:00
Jan Böhmer
38b5e95842 Improved serialization result for parts 2023-03-12 01:41:44 +01:00
Jan Böhmer
2c67586873 Improved serialized fields 2023-03-12 01:12:35 +01:00
Jan Böhmer
b99e6c9a21 Updated serializer discriminator map 2023-03-12 00:35:48 +01:00
Jan Böhmer
49944cda87 Added possibility to export Parts from part tables 2023-03-12 00:27:04 +01:00
Jan Böhmer
3b36b2a4dc Improved exporter service 2023-03-11 22:40:53 +01:00
Jan Böhmer
1dfcffe70d We are in development of 1.1.2 now 2023-03-11 19:50:05 +01:00
Jan Böhmer
a9b3dcd2c2 Do the color inversion for the IC logos when darkmode is enabled, the logos are then shown as white on black background.
This fixes issue #242
2023-03-11 19:48:42 +01:00
Jan Böhmer
31f9145d3f Fixed jump to letter buttons on IC logos page 2023-03-11 19:43:43 +01:00
Jan Böhmer
ba04b94964 Bumped to version 1.1.0 2023-03-10 11:01:52 +01:00
Jan Böhmer
4ecf99c17e Don't fail when datatables state was not saved before
This should fix issue #241
2023-03-10 01:40:54 +01:00
Jan Böhmer
9e80b23726 New translations security.en.xlf (English) 2023-03-06 01:31:11 +01:00
Jan Böhmer
494a1c49f9 New translations security.en.xlf (German) 2023-03-06 01:31:08 +01:00
Jan Böhmer
4a77064826 New translations validators.en.xlf (English) 2023-03-06 01:31:07 +01:00
Jan Böhmer
ce90f10243 New translations validators.en.xlf (German) 2023-03-06 01:31:04 +01:00
Jan Böhmer
426aa4e41d New translations messages.en.xlf (English) 2023-03-06 01:31:02 +01:00
Jan Böhmer
bdc953cab0 New translations messages.en.xlf (German) 2023-03-06 01:30:58 +01:00
88 changed files with 5618 additions and 2946 deletions

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "github-actions" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -29,7 +29,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
@@ -37,7 +37,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -51,4 +51,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

View File

@@ -10,9 +10,6 @@ on:
tags:
- 'v*.*.*'
- 'v*.*.*-**'
pull_request:
branches:
- 'master'
jobs:
docker:

View File

@@ -43,7 +43,7 @@ jobs:
run: ./bin/console lint:xliff translations
- name: Check dependencies for security
uses: symfonycorp/security-checker-action@v3
uses: symfonycorp/security-checker-action@v5
- name: Check doctrine mapping
run: ./bin/console doctrine:schema:validate --skip-sync -vvv --no-interaction

View File

@@ -63,7 +63,7 @@ jobs:
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v1
- uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}

View File

@@ -42,7 +42,7 @@ and multiple store locations and price information. Parts can be grouped using t
* User system with groups and detailed (fine granular) permissions.
Two-factor authentication is supported (Google Authenticator and Webauthn/U2F keys) and can be enforced for groups. Password reset via email can be setuped.
* Optional support for single sign-on (SSO) via SAML (using an intermediate service like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server)
* Import/Export system (partial working)
* Import/Export system for parts and datastructure. BOM import for projects from KiCAD is supported.
* Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build this project and directly withdraw all components needed from DB
* Event log: Track what changes happens to your inventory, track which user does what. Revert your parts to older versions.
* Responsive design: You can use Part-DB on your PC, your tablet and your smartphone using the same interface.

View File

@@ -1 +1 @@
1.1.0
1.2.0

View File

@@ -68,8 +68,10 @@ export default class extends Controller {
stateLoadCallback(settings) {
const data = JSON.parse( localStorage.getItem(this.getStateSaveKey()) );
//Do not save the start value (current page), as we want to always start at the first page on a page reload
data.start = 0;
if (data) {
//Do not save the start value (current page), as we want to always start at the first page on a page reload
data.start = 0;
}
return data;
}
@@ -201,4 +203,4 @@ export default class extends Controller {
return this.element.dataset.select ?? false;
}
}
}

View File

@@ -107,6 +107,13 @@ export default class extends DatatablesController {
//Hide the select element (the tomselect button is the sibling of the select element)
select_target.nextElementSibling.classList.add('d-none');
}
//If the selected option has a data-turbo attribute, set it to the form
if (selected_option.dataset.turbo) {
this.element.dataset.turbo = selected_option.dataset.turbo;
} else {
this.element.dataset.turbo = true;
}
}
confirmDeletionAtSubmit(event) {

View File

@@ -31,3 +31,7 @@
.darkmode--activated .hoverpic:hover {
background: black;
}
.tools-ic-logos img {
mix-blend-mode: normal;
}

View File

@@ -28,7 +28,9 @@ class ErrorHandlerHelper {
console.log('Error Handler registered');
const content = document.getElementById('content');
content.addEventListener('turbo:before-fetch-response', (event) => this.handleError(event));
//content.addEventListener('turbo:before-fetch-response', (event) => this.handleError(event));
content.addEventListener('turbo:fetch-request-error', (event) => this.handleError(event));
content.addEventListener('turbo:frame-missing', (event) => this.handleError(event));
$(document).ajaxError(this.handleJqueryErrror.bind(this));
}
@@ -87,8 +89,10 @@ class ErrorHandlerHelper {
}
handleError(event) {
const fetchResponse = event.detail.fetchResponse;
const response = fetchResponse.response;
//Prevent default error handling
event.preventDefault();
const response = event.detail.response;
//Ignore aborted requests.
if (response.statusText === 'abort' || response.status == 0) {
@@ -100,11 +104,11 @@ class ErrorHandlerHelper {
return;
}
if(fetchResponse.failed) {
if(!response.ok) {
response.text().then(responseHTML => {
this._showAlert(response.statusText, response.status, fetchResponse.location.toString(), responseHTML);
this._showAlert(response.statusText, response.status, response.url, responseHTML);
}).catch(err => {
this._showAlert(response.statusText, response.status, fetchResponse.location.toString(), '<pre>' + err + '</pre>');
this._showAlert(response.statusText, response.status, response.url, '<pre>' + err + '</pre>');
});
}
}

View File

@@ -24,6 +24,7 @@
"gregwar/captcha-bundle": "^2.1.0",
"hslavich/oneloginsaml-bundle": "^2.10",
"jbtronics/2fa-webauthn": "^1.0.0",
"league/csv": "^9.8.0",
"league/html-to-markdown": "^5.0.1",
"liip/imagine-bundle": "^2.2",
"nelexa/zip": "^4.0",

173
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "73b35aff40231c2fe1ebf72c1d098689",
"content-hash": "43882c51b2efa31f08c345a94661916c",
"packages": [
{
"name": "beberlei/assert",
@@ -2746,6 +2746,90 @@
],
"time": "2023-01-02T13:28:00+00:00"
},
{
"name": "league/csv",
"version": "9.8.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/csv.git",
"reference": "9d2e0265c5d90f5dd601bc65ff717e05cec19b47"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/csv/zipball/9d2e0265c5d90f5dd601bc65ff717e05cec19b47",
"reference": "9d2e0265c5d90f5dd601bc65ff717e05cec19b47",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"ext-curl": "*",
"ext-dom": "*",
"friendsofphp/php-cs-fixer": "^v3.4.0",
"phpstan/phpstan": "^1.3.0",
"phpstan/phpstan-phpunit": "^1.0.0",
"phpstan/phpstan-strict-rules": "^1.1.0",
"phpunit/phpunit": "^9.5.11"
},
"suggest": {
"ext-dom": "Required to use the XMLConverter and or the HTMLConverter classes",
"ext-iconv": "Needed to ease transcoding CSV using iconv stream filters"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"League\\Csv\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ignace Nyamagana Butera",
"email": "nyamsprod@gmail.com",
"homepage": "https://github.com/nyamsprod/",
"role": "Developer"
}
],
"description": "CSV data manipulation made easy in PHP",
"homepage": "https://csv.thephpleague.com",
"keywords": [
"convert",
"csv",
"export",
"filter",
"import",
"read",
"transform",
"write"
],
"support": {
"docs": "https://csv.thephpleague.com",
"issues": "https://github.com/thephpleague/csv/issues",
"rss": "https://github.com/thephpleague/csv/releases.atom",
"source": "https://github.com/thephpleague/csv"
},
"funding": [
{
"url": "https://github.com/sponsors/nyamsprod",
"type": "github"
}
],
"time": "2022-01-04T00:13:07+00:00"
},
{
"name": "league/html-to-markdown",
"version": "5.1.0",
@@ -14468,16 +14552,16 @@
},
{
"name": "phpstan/phpstan",
"version": "1.10.3",
"version": "1.10.7",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "5419375b5891add97dc74be71e6c1c34baaddf64"
"reference": "b10ceb526d9607903c5b2673f1fc8775dbe48975"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/5419375b5891add97dc74be71e6c1c34baaddf64",
"reference": "5419375b5891add97dc74be71e6c1c34baaddf64",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/b10ceb526d9607903c5b2673f1fc8775dbe48975",
"reference": "b10ceb526d9607903c5b2673f1fc8775dbe48975",
"shasum": ""
},
"require": {
@@ -14506,8 +14590,11 @@
"static analysis"
],
"support": {
"docs": "https://phpstan.org/user-guide/getting-started",
"forum": "https://github.com/phpstan/phpstan/discussions",
"issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.10.3"
"security": "https://github.com/phpstan/phpstan/security/policy",
"source": "https://github.com/phpstan/phpstan-src"
},
"funding": [
{
@@ -14523,20 +14610,20 @@
"type": "tidelift"
}
],
"time": "2023-02-25T14:47:13+00:00"
"time": "2023-03-16T15:24:20+00:00"
},
{
"name": "phpstan/phpstan-doctrine",
"version": "1.3.33",
"version": "1.3.37",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-doctrine.git",
"reference": "fcb67c2a747300ff8e6ae278191f6ff79fc90370"
"reference": "62bd362b432fe29e175168689510ddd927b698f8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/fcb67c2a747300ff8e6ae278191f6ff79fc90370",
"reference": "fcb67c2a747300ff8e6ae278191f6ff79fc90370",
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/62bd362b432fe29e175168689510ddd927b698f8",
"reference": "62bd362b432fe29e175168689510ddd927b698f8",
"shasum": ""
},
"require": {
@@ -14591,9 +14678,9 @@
"description": "Doctrine extensions for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-doctrine/issues",
"source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.33"
"source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.37"
},
"time": "2023-02-21T08:52:52+00:00"
"time": "2023-03-17T14:57:03+00:00"
},
{
"name": "phpstan/phpstan-symfony",
@@ -14737,12 +14824,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "91fab0df509bae51a5e0c0d224f3e088cea08ea2"
"reference": "ad434708152844968e15adf8eb3f4a2d7066242e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/91fab0df509bae51a5e0c0d224f3e088cea08ea2",
"reference": "91fab0df509bae51a5e0c0d224f3e088cea08ea2",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/ad434708152844968e15adf8eb3f4a2d7066242e",
"reference": "ad434708152844968e15adf8eb3f4a2d7066242e",
"shasum": ""
},
"conflict": {
@@ -14752,7 +14839,7 @@
"aheinze/cockpit": "<=2.2.1",
"akaunting/akaunting": "<2.1.13",
"akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53",
"alextselegidis/easyappointments": "<=1.4.3",
"alextselegidis/easyappointments": "<1.5",
"alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
"amazing/media2click": ">=1,<1.3.3",
"amphp/artax": "<1.0.6|>=2,<2.0.6",
@@ -14800,11 +14887,11 @@
"catfan/medoo": "<1.7.5",
"centreon/centreon": "<22.10-beta.1",
"cesnet/simplesamlphp-module-proxystatistics": "<3.1",
"cockpit-hq/cockpit": "<=2.3.9",
"cockpit-hq/cockpit": "<2.4.1",
"codeception/codeception": "<3.1.3|>=4,<4.1.22",
"codeigniter/framework": "<=3.0.6",
"codeigniter4/framework": "<4.2.11",
"codeigniter4/shield": "= 1.0.0-beta",
"codeigniter4/shield": "<1-beta.4|= 1.0.0-beta",
"codiad/codiad": "<=2.8.4",
"composer/composer": "<1.10.26|>=2-alpha.1,<2.2.12|>=2.3,<2.3.5",
"concrete5/concrete5": "<=9.1.3|>= 9.0.0RC1, < 9.1.3",
@@ -14815,7 +14902,7 @@
"contao/core-bundle": "<4.9.18|>=4.10,<4.11.7|>=4.13,<4.13.3|= 4.10.0",
"contao/listing-bundle": ">=4,<4.4.8",
"contao/managed-edition": "<=1.5",
"craftcms/cms": "<4.3.7|>= 4.0.0-RC1, < 4.2.1",
"craftcms/cms": "<3.7.64|>= 4.0.0-RC1, < 4.3.7|>= 4.0.0-RC1, < 4.2.1",
"croogo/croogo": "<3.0.7",
"cuyz/valinor": "<0.12",
"czproject/git-php": "<4.0.3",
@@ -14844,6 +14931,7 @@
"ectouch/ectouch": "<=2.7.2",
"elefant/cms": "<1.3.13",
"elgg/elgg": "<3.3.24|>=4,<4.0.5",
"encore/laravel-admin": "<=1.8.19",
"endroid/qr-code-bundle": "<3.4.2",
"enshrined/svg-sanitize": "<0.15",
"erusev/parsedown": "<1.7.2",
@@ -14858,11 +14946,11 @@
"ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6|>=1.5,<1.5.29|>=2.3,<2.3.26",
"ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1",
"ezsystems/ezplatform-graphql": ">=1-rc.1,<1.0.13|>=2-beta.1,<2.3.12",
"ezsystems/ezplatform-kernel": "<=1.2.5|>=1.3,<1.3.26",
"ezsystems/ezplatform-kernel": "<1.2.5.1|>=1.3,<1.3.26",
"ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8",
"ezsystems/ezplatform-richtext": ">=2.3,<=2.3.7",
"ezsystems/ezplatform-user": ">=1,<1.0.1",
"ezsystems/ezpublish-kernel": "<=6.13.8.1|>=7,<7.5.30",
"ezsystems/ezpublish-kernel": "<6.13.8.2|>=7,<7.5.30",
"ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.3.5.1",
"ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3",
"ezsystems/repository-forms": ">=2.3,<2.3.2.1|>=2.5,<2.5.15",
@@ -14876,7 +14964,7 @@
"firebase/php-jwt": "<6",
"fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2",
"fixpunkt/fp-newsletter": "<1.1.1|>=2,<2.1.2|>=2.2,<3.2.6",
"flarum/core": "<1.6.3",
"flarum/core": "<1.7",
"flarum/mentions": "<1.6.3",
"flarum/sticky": ">=0.1-beta.14,<=0.1-beta.15",
"flarum/tags": "<=0.1-beta.13",
@@ -14887,13 +14975,15 @@
"forkcms/forkcms": "<5.11.1",
"fossar/tcpdf-parser": "<6.2.22",
"francoisjacquet/rosariosis": "<10.8.2",
"frappant/frp-form-answers": "<3.1.2|>=4,<4.0.2",
"friendsofsymfony/oauth2-php": "<1.3",
"friendsofsymfony/rest-bundle": ">=1.2,<1.2.2",
"friendsofsymfony/user-bundle": ">=1.2,<1.3.5",
"friendsoftypo3/mediace": ">=7.6.2,<7.6.5",
"froala/wysiwyg-editor": "<3.2.7",
"froxlor/froxlor": "<2.0.11",
"froxlor/froxlor": "<2.0.13",
"fuel/core": "<1.8.1",
"funadmin/funadmin": "<=3.2",
"gaoming13/wechat-php-sdk": "<=1.10.2",
"genix/cms": "<=1.1.11",
"getgrav/grav": "<1.7.34",
@@ -14904,7 +14994,7 @@
"globalpayments/php-sdk": "<2",
"google/protobuf": "<3.15",
"gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3",
"gree/jose": "<=2.2",
"gree/jose": "<2.2.1",
"gregwar/rst": "<1.0.3",
"grumpydictator/firefly-iii": "<5.8",
"guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5",
@@ -14951,6 +15041,7 @@
"kimai/kimai": "<1.1",
"kitodo/presentation": "<3.1.2",
"klaviyo/magento2-extension": ">=1,<3",
"knplabs/knp-snappy": "<=1.4.1",
"krayin/laravel-crm": "<1.2.2",
"kreait/firebase-php": ">=3.2,<3.8.1",
"la-haute-societe/tcpdf": "<6.2.22",
@@ -14997,7 +15088,7 @@
"modx/revolution": "<= 2.8.3-pl|<2.8",
"mojo42/jirafeau": "<4.4",
"monolog/monolog": ">=1.8,<1.12",
"moodle/moodle": "<4.0.6|>=4.1-beta,<4.1.1",
"moodle/moodle": "<4.0.6|= 3.11|>=4.1-beta,<4.1.1",
"mustache/mustache": ">=2,<2.14.1",
"namshi/jose": "<2.2",
"neoan3-apps/template": "<1.1.1",
@@ -15050,14 +15141,14 @@
"phpmyfaq/phpmyfaq": "<=3.1.7",
"phpoffice/phpexcel": "<1.8",
"phpoffice/phpspreadsheet": "<1.16",
"phpseclib/phpseclib": "<=2.0.41|>=3,<3.0.7",
"phpseclib/phpseclib": "<2.0.31|>=3,<3.0.19",
"phpservermon/phpservermon": "<=3.5.2",
"phpunit/phpunit": ">=4.8.19,<4.8.28|>=5,<5.6.3",
"phpwhois/phpwhois": "<=4.2.5",
"phpxmlrpc/extras": "<0.6.1",
"phpxmlrpc/phpxmlrpc": "<4.9.2",
"pimcore/data-hub": "<1.2.4",
"pimcore/pimcore": "<10.5.18",
"pimcore/pimcore": "<11",
"pixelfed/pixelfed": "<=0.11.4",
"pocketmine/bedrock-protocol": "<8.0.2",
"pocketmine/pocketmine-mp": "<4.12.5|>= 4.0.0-BETA5, < 4.4.2",
@@ -15066,7 +15157,7 @@
"prestashop/blockwishlist": ">=2,<2.1.1",
"prestashop/contactform": ">=1.0.1,<4.3",
"prestashop/gamification": "<2.3.2",
"prestashop/prestashop": "<1.7.8.8",
"prestashop/prestashop": "<8.0.1",
"prestashop/productcomments": "<5.0.2",
"prestashop/ps_emailsubscription": "<2.6.1",
"prestashop/ps_facetedsearch": "<3.4.1",
@@ -15108,7 +15199,7 @@
"silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1",
"silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3",
"silverstripe/framework": "<4.11.14",
"silverstripe/graphql": "<3.5.2|>=4-alpha.1,<4-alpha.2|= 4.0.0-alpha1",
"silverstripe/graphql": "<3.5.2|>=4-alpha.1,<4-alpha.2|>=4.1.1,<4.1.2|>=4.2.2,<4.2.3|= 4.0.0-alpha1",
"silverstripe/hybridsessions": ">=1,<2.4.1|>=2.5,<2.5.1",
"silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1",
"silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4",
@@ -15217,6 +15308,7 @@
"unisharp/laravel-filemanager": "<=2.5.1",
"userfrosting/userfrosting": ">=0.3.1,<4.6.3",
"usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2",
"uvdesk/community-skeleton": "<1.1",
"vanilla/safecurl": "<0.9.2",
"verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4",
"vova07/yii2-fileapi-widget": "<0.1.9",
@@ -15301,6 +15393,9 @@
}
],
"description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it",
"keywords": [
"dev"
],
"support": {
"issues": "https://github.com/Roave/SecurityAdvisories/issues",
"source": "https://github.com/Roave/SecurityAdvisories/tree/latest"
@@ -15315,7 +15410,7 @@
"type": "tidelift"
}
],
"time": "2023-03-03T23:04:13+00:00"
"time": "2023-03-17T19:03:58+00:00"
},
{
"name": "sebastian/diff",
@@ -15980,22 +16075,22 @@
},
{
"name": "vimeo/psalm",
"version": "5.7.7",
"version": "5.8.0",
"source": {
"type": "git",
"url": "https://github.com/vimeo/psalm.git",
"reference": "e028ba46ba0d7f9a78bc3201c251e137383e145f"
"reference": "9cf4f60a333f779ad3bc704a555920e81d4fdcda"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vimeo/psalm/zipball/e028ba46ba0d7f9a78bc3201c251e137383e145f",
"reference": "e028ba46ba0d7f9a78bc3201c251e137383e145f",
"url": "https://api.github.com/repos/vimeo/psalm/zipball/9cf4f60a333f779ad3bc704a555920e81d4fdcda",
"reference": "9cf4f60a333f779ad3bc704a555920e81d4fdcda",
"shasum": ""
},
"require": {
"amphp/amp": "^2.4.2",
"amphp/byte-stream": "^1.5",
"composer/package-versions-deprecated": "^1.10.0",
"composer-runtime-api": "^2",
"composer/semver": "^1.4 || ^2.0 || ^3.0",
"composer/xdebug-handler": "^2.0 || ^3.0",
"dnoegel/php-xdg-base-dir": "^0.1.1",
@@ -16010,7 +16105,7 @@
"felixfbecker/language-server-protocol": "^1.5.2",
"fidry/cpu-core-counter": "^0.4.1 || ^0.5.1",
"netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
"nikic/php-parser": "^4.13",
"nikic/php-parser": "^4.14",
"php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0",
"sebastian/diff": "^4.0 || ^5.0",
"spatie/array-to-xml": "^2.17.0 || ^3.0",
@@ -16079,9 +16174,9 @@
],
"support": {
"issues": "https://github.com/vimeo/psalm/issues",
"source": "https://github.com/vimeo/psalm/tree/5.7.7"
"source": "https://github.com/vimeo/psalm/tree/5.8.0"
},
"time": "2023-02-25T01:05:07+00:00"
"time": "2023-03-09T04:14:35+00:00"
}
],
"aliases": [],

View File

@@ -43,6 +43,9 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
revert_element:
label: "perm.revert_elements"
alsoSet: ["read", "edit", "create", "delete", "show_history"]
import:
label: "perm.import"
alsoSet: ["read", "edit", "create"]
parts_stock:
group: "data"
@@ -76,6 +79,9 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
revert_element:
label: "perm.revert_elements"
alsoSet: ["read", "edit", "create", "delete", "show_history"]
import:
label: "perm.import"
alsoSet: [ "read", "edit", "create" ]
footprints:
<<: *PART_CONTAINING
@@ -156,6 +162,9 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
revert_element:
label: "perm.revert_elements"
alsoSet: ["read", "edit", "create", "delete", "edit_permissions", "show_history"]
import:
label: "perm.import"
alsoSet: [ "read", "edit", "create" ]
users:
label: "perm.users"
@@ -188,6 +197,9 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
revert_element:
label: "perm.revert_elements"
alsoSet: ["read", "create", "delete", "edit_permissions", "show_history", "edit_infos", "edit_username"]
import:
label: "perm.import"
alsoSet: [ "read", "create" ]
#database:
# label: "perm.database"

View File

@@ -0,0 +1,4 @@
name;description;category;notes;footprint;tags;quantity;storage_location;mass;ipn;mpn;manufacturing_status;manufacturer;supplier;spn;price;favorite;needs_review;minamount;partUnit;manufacturing_status
BC547;NPN transistor;Transistors -> NPN;very important notes;TO -> TO-92;NPN,Transistor;5;Room 1 -> Shelf 1 -> Box 2;10;;;Manufacturer;;You need to fill this line, to use spn and price;BC547C;2,3;0;;;;
BC557;PNP transistor;<b>HTML</b>;;TO -> TO-92;PNP,Transistor;10;Room 2-> Box 3;;Internal1234;;;;;;;;1;;;active
Copper Wire;;Wire;;;;;;;;;;;;;;;;;Meter;
1 name description category notes footprint tags quantity storage_location mass ipn mpn manufacturing_status manufacturer supplier spn price favorite needs_review minamount partUnit manufacturing_status
2 BC547 NPN transistor Transistors -> NPN very important notes TO -> TO-92 NPN,Transistor 5 Room 1 -> Shelf 1 -> Box 2 10 Manufacturer You need to fill this line, to use spn and price BC547C 2,3 0
3 BC557 PNP transistor <b>HTML</b> TO -> TO-92 PNP,Transistor 10 Room 2-> Box 3 Internal1234 1 active
4 Copper Wire Wire Meter

View File

@@ -23,13 +23,14 @@ Some things changed however to the old version and some features are still missi
* Configuration is now done via configuration files / environment variables instead of the WebUI (this maybe change in the future).
* Database updated are now done via console instead of the WebUI
* Permission system changed: **You will have to newly set the permissions of all users and groups!**
* Import / Export file format changed. Fields must be english now (unlike in legacy Part-DB versions, where german fields in CSV were possible)
and you maybe have to change the header line/field names of your CSV files.
## Missing features
* No possibility to mark parts for ordering (yet)
* No import / export possibility for parts (yet), however you can import/export other datastructures like Categories, Footprints, etc. (yet)
* No support for 3D models of footprints (yet)
* No possibility to disable footprints, manufacturers globally (or per category). This should not have a big impact, when you forbid users to edit/create them.
* No resitor calculator or SMD labels tools
* No resistor calculator or SMD labels tools
## Upgrade process

29
docs/usage/bom_import.md Normal file
View File

@@ -0,0 +1,29 @@
---
layout: default
title: Import Bill of Material (BOM) for Projects
nav_order: 5
parent: Usage
---
# Import Bill of Material (BOM) for Projects
Part-DB supports the import of Bill of Material (BOM) files for projects. This allows you to directly import a BOM file from your ECAD software into your Part-DB project.
The import process is currently semi-automatic. This means Part-DB will take the BOM file and create entries for all parts in the BOM file in your project and assign fields like
mountnames (e.g. 'C1, C2, C3'), quantity and more.
However, you still have to assign the parts from Part-DB database to the entries (if applicable) after the import by hand,
as Part-DB can not know which part you had in mind when you designed your schematic.
## Usage
In the project view or edit click on the "Import BOM" button, below the BOM table. This will open a dialog where you can
select the BOM file you want to import and some options for the import process:
* **Type**: The format/type of the BOM file. See below for explanations of the different types.
* **Clear existing BOM entries before import**: If this is checked, all existing BOM entries, which are currently associated with the project, will be deleted before the import.
### Supported BOM file formats
* **KiCAD Pcbnew BOM (CSV file)**: A CSV file of the Bill of Material (BOM) generated by [KiCAD Pcbnew](https://www.kicad.org/).
Please note that you have to export the BOM from the PCB editor, the BOM generated by the schematic editor (Eeschema) has a different format and does not work with this type.
You can generate this BOM file by going to "File" -> "Fabrication Outputs" -> "Bill of Materials" in Pcbnew and save the file to your desired location.

101
docs/usage/import_export.md Normal file
View File

@@ -0,0 +1,101 @@
---
layout: default
title: Import & Export data
nav_order: 4
parent: Usage
---
# Import & Export data
Part-DB offers the possibility to import existing data (parts, datastructures, etc.) from existing datasources into Part-DB. Data can also be exported from Part-DB into various formats.
## Import
{: .note }
> As data import is a very powerful feature and can easily fill up your database with lots of data, import is by default only available for
> administrators. If you want to allow other users to import data, or can not import data, check the permissions of the user. You can enable import for each data structure
> individually in the permissions settings.
### Import parts
Part-DB supports the import of parts from CSV files and other formats. This can be used to import existing parts from other databases or datasources into Part-DB. The import can be done via the "Tools -> Import parts" page, which you can find in the "Tools" sidebar panel.
{: .important }
> When importing data, the data is immediatley written to database during the import process, when the data is formally valid.
> You will not be able to check the data before it is written to the database, so you should review the data before using the import tool.
You can upload the file which should be imported here and choose various options on how the data should be treated:
* **Format**: By default "auto" is selected here and Part-DB will try to detect the format of the file automatically based on its file extension. If you want to force a specific format or Part-DB can not auto-detect the format, you can select it here.
* **CSV delimiter**: If you upload an CSV file, you can select the delimiter character which is used to separate the columns in the CSV file. Depending on the CSV file, this might be a comma (`,`), semicolon (`;`).
* **Category override**: You can select (or create) a category here, to which all imported parts should be assigned, no matter what was specified in the import file. This can be useful if you want to assign all imports to a certain category or if no category is specified in the data. If you leave this field empty, the category will be determined by the import file (or the export will error, if no category is specified).
* **Mark all imported parts as "Needs review"**: If this is selected, all imported parts will be marked as "Needs review" after the import. This can be useful if you want to review all imported parts before using them.
* **Create unknown datastructures**: If this is selected Part-DB will create new datastructures (like categories, manufacturers, etc.) if no datastructure(s) with the same name and path already exists. If this is not selected, only existing datastructures will be used and if no matching datastrucure is found, the imported parts field will be empty.
* **Path delimiter**: Part-DB allows you to create/select nested datastructures (like categories, manufacturers, etc.) by using a path (e.g. `Category 1->Category 1.1`, which will select/create the `Category 1.1` whose parent is `Category 1`). This path is separated by the path delimiter. If you want to use a different path delimiter than the default one (which is `>`), you can select it here.
* **Abort on validation error**: If this is selected, the import will be aborted if a validation error occurs (e.g. if a required field is empty) for any of the imported parts and validation errors will be shown on top of the page. If this is not selected, the import will continue for the other parts and only the invalid parts will be skipped.
After you have selected the options, you can start the import by clicking the "Import" button. When the import is finished, you will see the results of the import in the lower half of the page. You find a table with the imported parts (including links to them) there.
#### Fields description
For the importing of parts, you can use the following fields which will be imported into each part. Please note that the field names are case sensitive (so `name` is not the same as `Name`). All fields (besides name) are optional, so you can leave them empty or do not include the column in your file.
* **`name`** (required): The name of the part. This is the only required field, all other fields are optional.
* **`description`**: The description of the part, you can use markdown/HTML syntax here for rich text formatting.
* **`notes`** or **`comment`**: The notes of the part, you can use markdown/HTML syntax here for rich text formatting.
* **`category`**: The category of the part. This can be a path (e.g. `Category 1->Category 1.1`), which will select/create the `Category 1.1` whose parent is `Category 1`. If you want to use a different path delimiter than the default one (which is `->`), you can select it in the import options. If the category does not exist and the option "Create unknown datastructures" is selected, it will be created.
* **`footprint`**: The footprint of the part. Can be a path similar to the category field.
* **`favorite`**: If this is set to `1`, the part will be marked as favorite.
* **`manufacturer`**: The manufacturer of the part. Can be a path similar to the category field.
* **`manufacturer_product_number`** or **`mpn`**: The manufacturer product number of the part.
* **`manufacturer_product_url`: The URL to the product page of the manufacturer of the part.
* **`manufacturing_status`**: The manufacturing status of the part, must be one of the following values: `announced`, `active`, `nrfnd`, `eol`, `discontinued` or left empty.
* **`needs_review`** or **`needs_review`**: If this is set to `1`, the part will be marked as "needs review".
* **`tags`**: A comma separated list of tags for the part.
* **`mass`**: The mass of the part in grams.
* **`ipn`**: The IPN (Item Part Number) of the part.
* **`minamount`**: The minimum amount of the part which should be in stock.
* **`partUnit`**: The measurement unit of the part to use. Can be a path similar to the category field.
With the following fields you can specify storage locations and amount / quantiy in stock of the part. An PartLot will be created automatically from the data and assigned to the part. The following fields are helpers for an easy import for parts at one storage location. If you need to create a Part with multiple PartLots you have to use JSON format (or CSV) with nested objects:
**`storage_location`** or **`storelocation`**: The storage location of the part. Can be a path similar to the category field.
**`amount`**, **`quantity`** or **`instock`**: The amount of the part in stock. If this value is not set, the part lot will be marked with "unknown amount"
The following fields can be used to specify the supplier/distributor, supplier product number and the price of the part. This is only possible for a single supplier/distributor and price with this fields. If you need to specify multiple suppliers/distributors or prices, you have to use JSON format (or CSV) with nested objects.
**Please note that the supplier fields is required, if you want to import prices or supplier product numbers.**. If the supplier is not specified, the price and supplier product number fields will be ignored:
* **`supplier`**: The supplier of the part. Can be a path similar to the category field.
* **`supplier_product_number`** or **`supplier_part_number`** or * **`spn`**: The supplier product number of the part.
* **`price`**: The price of the part in the base currency of the database (by default euro).
#### Example data
Here you can find some example data for the import of parts, you can use it as a template for your own import (especially the CSV file).
* [Part import CSV example]({% link assets/usage/import_export/part_import_example.csv %}) with all possible fields
## Export
By default every user, who can read the datastructure, can also export the data of this datastructure, as this does not give the user any additional information.
### Exporting data structures (categories, manufacturers, etc.)
You can export data structures (like categories, manufacturers, etc.) in the respective edit page (e.g. Tools Panel -> Edit -> Category).
If you select a certain datastructure from your list, you can export it (and optionally all sub-datastructures) in the "Export" tab.
If you want to export all datastructures of a certain type (e.g. all categories in your database), you can select the "Export all" function in the "Import / Export" tab of the "new element" page.
You can select between the following export formats:
* **CSV** (Comma Separated Values): A semicolon separated list of values, where every line represents an element. This format can be imported into Excel or LibreOffice Calc and is easy to work with. However it does not support nested datastructures or sub data (like parameters, attachments, etc.), very well (many columns are generated, as every possible sub data is exported as a separate column).
* **JSON** (JavaScript Object Notation): A text-based format, which is easy to work with programming laguages. It supports nested datastructures and sub data (like parameters, attachments, etc.) very well. However it is not easy to work with in Excel or LibreOffice Calc and you maybe need to write some code to work with the exported data efficiently.
* **YAML** (Yet another Markup Language): Very similar to JSON
* **XML** (Extensible Markup Language): Good support with nested datastructures. Similar usecase as JSON and YAML.
Also you can select between the following export levels:
* **Simple**: This will only export very basic information about the name (like the name, or description for parts)
* **Extended**: This will export all commonly used information about this datastructure (like notes, options, etc)
* **Full**: This will export all available information about this datastructure (like all parameters, attachments)
Please note that the level will also be applied to all sub data or children elements. So if you select "Full" for a part, all the associated categories, manufacturers, footprints, etc. will also be exported with all available information, this can lead to very large export files.
### Exporting parts
You can export parts in all part tables. Select the parts you want via the checkbox in the table line and select the export format and level in the appearing menu.
See the section about exporting datastructures for more information about the export formats and levels.

View File

@@ -58,6 +58,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -338,20 +339,39 @@ abstract class BaseAdminController extends AbstractController
$file = $import_form['file']->getData();
$data = $import_form->getData();
if ($data['format'] === 'auto') {
$format = $importer->determineFormat($file->getClientOriginalExtension());
if (null === $format) {
$this->addFlash('error', 'parts.import.flash.error.unknown_format');
goto ret;
}
} else {
$format = $data['format'];
}
$options = [
'parent' => $data['parent'],
'preserve_children' => $data['preserve_children'],
'format' => $data['format'],
'csv_separator' => $data['csv_separator'],
'parent' => $data['parent'] ?? null,
'preserve_children' => $data['preserve_children'] ?? false,
'format' => $format,
'class' => $this->entity_class,
'csv_delimiter' => $data['csv_delimiter'],
'abort_on_validation_error' => $data['abort_on_validation_error'],
];
$this->commentHelper->setMessage('Import '.$file->getClientOriginalName());
$errors = $importer->fileToDBEntities($file, $this->entity_class, $options);
try {
$errors = $importer->importFileAndPersistToDB($file, $options);
foreach ($errors as $name => $error) {
/** @var ConstraintViolationList $error */
$this->addFlash('error', $name.':'.$error);
foreach ($errors as $name => $error) {
foreach ($error['violations'] as $violation) {
$this->addFlash('error', $name.': '.$violation->getMessage());
}
}
}
catch (UnexpectedValueException $e) {
$this->addFlash('error', 'parts.import.flash.error.invalid_file');
}
}
@@ -382,6 +402,7 @@ abstract class BaseAdminController extends AbstractController
$em->flush();
}
ret:
return $this->renderForm($this->twig_template, [
'entity' => $new_entity,
'form' => $form,

View File

@@ -0,0 +1,141 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Controller;
use App\Entity\Parts\Part;
use App\Form\AdminPages\ImportType;
use App\Services\ImportExportSystem\EntityExporter;
use App\Services\ImportExportSystem\EntityImporter;
use App\Services\LogSystem\EventCommentHelper;
use App\Services\Parts\PartsTableActionHandler;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use UnexpectedValueException;
class PartImportExportController extends AbstractController
{
private PartsTableActionHandler $partsTableActionHandler;
private EntityImporter $entityImporter;
private EventCommentHelper $commentHelper;
public function __construct(PartsTableActionHandler $partsTableActionHandler,
EntityImporter $entityImporter, EventCommentHelper $commentHelper)
{
$this->partsTableActionHandler = $partsTableActionHandler;
$this->entityImporter = $entityImporter;
$this->commentHelper = $commentHelper;
}
/**
* @Route("/parts/import", name="parts_import")
* @param Request $request
* @return Response
*/
public function importParts(Request $request): Response
{
$this->denyAccessUnlessGranted('@parts.import');
$import_form = $this->createForm(ImportType::class, ['entity_class' => Part::class]);
$import_form->handleRequest($request);
if ($import_form->isSubmitted() && $import_form->isValid()) {
/** @var UploadedFile $file */
$file = $import_form['file']->getData();
$data = $import_form->getData();
if ($data['format'] === 'auto') {
$format = $this->entityImporter->determineFormat($file->getClientOriginalExtension());
if (null === $format) {
$this->addFlash('error', 'parts.import.flash.error.unknown_format');
goto ret;
}
} else {
$format = $data['format'];
}
$options = [
'create_unknown_datastructures' => $data['create_unknown_datastructures'],
'path_delimiter' => $data['path_delimiter'],
'format' => $format,
'part_category' => $data['part_category'],
'class' => Part::class,
'csv_delimiter' => $data['csv_delimiter'],
'part_needs_review' => $data['part_needs_review'],
'abort_on_validation_error' => $data['abort_on_validation_error'],
];
$this->commentHelper->setMessage('Import '.$file->getClientOriginalName());
$entities = [];
try {
$errors = $this->entityImporter->importFileAndPersistToDB($file, $options, $entities);
} catch (UnexpectedValueException $e) {
$this->addFlash('error', 'parts.import.flash.error.invalid_file');
if ($e instanceof NotNormalizableValueException) {
$this->addFlash('error', $e->getMessage());
}
goto ret;
}
if (!isset($errors) || $errors) {
$this->addFlash('error', 'parts.import.flash.error');
} else {
$this->addFlash('success', 'parts.import.flash.success');
}
}
ret:
return $this->renderForm('parts/import/parts_import.html.twig', [
'import_form' => $import_form,
'imported_entities' => $entities ?? [],
'import_errors' => $errors ?? [],
]);
}
/**
* @Route("/parts/export", name="parts_export", methods={"GET"})
* @return Response
*/
public function exportParts(Request $request, EntityExporter $entityExporter): Response
{
$ids = $request->query->get('ids', '');
$parts = $this->partsTableActionHandler->idStringToArray($ids);
if (empty($parts)) {
throw new \RuntimeException('No parts found!');
}
//Ensure that we have access to the parts
foreach ($parts as $part) {
$this->denyAccessUnlessGranted('read', $part);
}
return $entityExporter->exportEntityFromRequest($parts, $request);
}
}

View File

@@ -28,17 +28,26 @@ use App\Form\ProjectSystem\ProjectBOMEntryCollectionType;
use App\Form\ProjectSystem\ProjectBuildType;
use App\Form\Type\StructuralEntityType;
use App\Helpers\Projects\ProjectBuildRequest;
use App\Services\ImportExportSystem\BOMImporter;
use App\Services\ProjectSystem\ProjectBuildHelper;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use League\Csv\Exception;
use League\Csv\SyntaxError;
use Omines\DataTablesBundle\DataTableFactory;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use function Symfony\Component\Translation\t;
/**
* @Route("/project")
@@ -119,6 +128,82 @@ class ProjectController extends AbstractController
]);
}
/**
* @Route("/{id}/import_bom", name="project_import_bom", requirements={"id"="\d+"})
*/
public function importBOM(Request $request, EntityManagerInterface $entityManager, Project $project,
BOMImporter $BOMImporter, ValidatorInterface $validator): Response
{
$this->denyAccessUnlessGranted('edit', $project);
$builder = $this->createFormBuilder();
$builder->add('file', FileType::class, [
'label' => 'import.file',
'required' => true,
'attr' => [
'accept' => '.csv'
]
]);
$builder->add('type', ChoiceType::class, [
'label' => 'project.bom_import.type',
'required' => true,
'choices' => [
'project.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew',
]
]);
$builder->add('clear_existing_bom', CheckboxType::class, [
'label' => 'project.bom_import.clear_existing_bom',
'required' => false,
'data' => false,
'help' => 'project.bom_import.clear_existing_bom.help',
]);
$builder->add('submit', SubmitType::class, [
'label' => 'import.btn',
]);
$form = $builder->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
//Clear existing BOM entries if requested
if ($form->get('clear_existing_bom')->getData()) {
$project->getBomEntries()->clear();
$entityManager->flush();
}
try {
$entries = $BOMImporter->importFileIntoProject($form->get('file')->getData(), $project, [
'type' => $form->get('type')->getData(),
]);
//Validate the project entries
$errors = $validator->validateProperty($project, 'bom_entries');
//If no validation errors occured, save the changes and redirect to edit page
if (count ($errors) === 0) {
$this->addFlash('success', t('project.bom_import.flash.success', ['%count%' => count($entries)]));
$entityManager->flush();
return $this->redirectToRoute('project_edit', ['id' => $project->getID()]);
}
if (count ($errors) > 0) {
$this->addFlash('error', t('project.bom_import.flash.invalid_entries'));
}
} catch (\UnexpectedValueException $e) {
$this->addFlash('error', t('project.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()]));
} catch (SyntaxError $e) {
$this->addFlash('error', t('project.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()]));
}
}
return $this->renderForm('projects/import_bom.html.twig', [
'project' => $project,
'form' => $form,
'errors' => $errors ?? null,
]);
}
/**
* @Route("/add_parts", name="project_add_parts_no_id")
* @Route("/{id}/add_parts", name="project_add_parts", requirements={"id"="\d+"})

View File

@@ -95,6 +95,25 @@ class SelectAPIController extends AbstractController
return $this->getResponseForClass(Project::class, false);
}
/**
* @Route("/export_level", name="select_export_level")
*/
public function exportLevel(): Response
{
$entries = [
1 => $this->translator->trans('export.level.simple'),
2 => $this->translator->trans('export.level.extended'),
3 => $this->translator->trans('export.level.full'),
];
return $this->json(array_map(function ($key, $value) {
return [
'text' => $value,
'value' => $key,
];
}, array_keys($entries), $entries));
}
/**
* @Route("/label_profiles", name="select_label_profiles")
* @return Response

View File

@@ -29,6 +29,7 @@ use App\Entity\Contracts\HasMasterAttachmentInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ORM\MappedSuperclass()
@@ -43,6 +44,7 @@ abstract class AttachmentContainingDBElement extends AbstractNamedDBElement impl
* //@ORM\OneToMany(targetEntity="Attachment", mappedBy="element")
*
* Mapping is done in sub classes like part
* @Groups({"full"})
*/
protected $attachments;

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Entity\Base;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use function is_string;
use Symfony\Component\Validator\Constraints as Assert;
@@ -36,18 +37,21 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
/**
* @var string The address of the company
* @ORM\Column(type="string")
* @Groups({"full"})
*/
protected string $address = '';
/**
* @var string The phone number of the company
* @ORM\Column(type="string")
* @Groups({"full"})
*/
protected string $phone_number = '';
/**
* @var string The fax number of the company
* @ORM\Column(type="string")
* @Groups({"full"})
*/
protected string $fax_number = '';
@@ -55,6 +59,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
* @var string The email address of the company
* @ORM\Column(type="string")
* @Assert\Email()
* @Groups({"full"})
*/
protected string $email_address = '';
@@ -62,6 +67,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
* @var string The website of the company
* @ORM\Column(type="string")
* @Assert\Url()
* @Groups({"full"})
*/
protected string $website = '';

View File

@@ -38,20 +38,37 @@ use Symfony\Component\Serializer\Annotation\Groups;
* @ORM\MappedSuperclass(repositoryClass="App\Repository\DBElementRepository")
*
* @DiscriminatorMap(typeProperty="type", mapping={
* "attachment_type" = "App\Entity\AttachmentType",
* "attachment" = "App\Entity\Attachment",
* "category" = "App\Entity\Attachment",
* "attachment_type" = "App\Entity\Attachments\AttachmentType",
* "attachment" = "App\Entity\Attachments\Attachment",
* "attachment_type_attachment" = "App\Entity\Attachments\AttachmentTypeAttachment",
* "category_attachment" = "App\Entity\Attachments\CategoryAttachment",
* "currency_attachment" = "App\Entity\Attachments\CurrencyAttachment",
* "footprint_attachment" = "App\Entity\Attachments\FootprintAttachment",
* "group_attachment" = "App\Entity\Attachments\GroupAttachment",
* "label_attachment" = "App\Entity\Attachments\LabelAttachment",
* "manufacturer_attachment" = "App\Entity\Attachments\ManufacturerAttachment",
* "measurement_unit_attachment" = "App\Entity\Attachments\MeasurementUnitAttachment",
* "part_attachment" = "App\Entity\Attachments\PartAttachment",
* "project_attachment" = "App\Entity\Attachments\ProjectAttachment",
* "storelocation_attachment" = "App\Entity\Attachments\StorelocationAttachment",
* "supplier_attachment" = "App\Entity\Attachments\SupplierAttachment",
* "user_attachment" = "App\Entity\Attachments\UserAttachment",
* "category" = "App\Entity\Parts\Category",
* "project" = "App\Entity\ProjectSystem\Project",
* "project_bom_entry" = "App\Entity\ProjectSystem\ProjectBOMEntry",
* "footprint" = "App\Entity\Footprint",
* "group" = "App\Entity\Group",
* "manufacturer" = "App\Entity\Manufacturer",
* "orderdetail" = "App\Entity\Orderdetail",
* "part" = "App\Entity\Part",
* "pricedetail" = "App\Entity\Pricedetail",
* "storelocation" = "App\Entity\Storelocation",
* "supplier" = "App\Entity\Supplier",
* "user" = "App\Entity\User"
* "footprint" = "App\Entity\Parts\Footprint",
* "group" = "App\Entity\UserSystem\Group",
* "manufacturer" = "App\Entity\Parts\Manufacturer",
* "orderdetail" = "App\Entity\PriceInformations\Orderdetail",
* "part" = "App\Entity\Parts\Part",
* "pricedetail" = "App\Entity\PriceInformation\Pricedetail",
* "storelocation" = "App\Entity\Parts\Storelocation",
* "part_lot" = "App\Entity\Parts\PartLot",
* "currency" = "App\Entity\PriceInformations\Currency",
* "measurement_unit" = "App\Entity\Parts\MeasurementUnit",
* "parameter" = "App\Entity\Parts\AbstractParameter",
* "supplier" = "App\Entity\Parts\Supplier",
* "user" = "App\Entity\UserSystem\User"
* })
*/
abstract class AbstractDBElement implements JsonSerializable

View File

@@ -42,7 +42,7 @@ abstract class AbstractNamedDBElement extends AbstractDBElement implements Named
* @var string the name of this element
* @ORM\Column(type="string")
* @Assert\NotBlank()
* @Groups({"simple", "extended", "full"})
* @Groups({"simple", "extended", "full", "import"})
*/
protected string $name = '';

View File

@@ -23,13 +23,15 @@ declare(strict_types=1);
namespace App\Entity\Base;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* Class PartsContainingDBElement.
*
* @ORM\MappedSuperclass(repositoryClass="App\Repository\AbstractPartsContainingRepository")
*/
abstract class
AbstractPartsContainingDBElement extends AbstractStructuralDBElement
abstract class AbstractPartsContainingDBElement extends AbstractStructuralDBElement
{
/** @Groups({"full"}) */
protected $parameters;
}

View File

@@ -63,7 +63,7 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement
/**
* @var string The comment info for this element
* @ORM\Column(type="text")
* @Groups({"simple", "extended", "full"})
* @Groups({"full", "import"})
*/
protected string $comment = '';
@@ -71,6 +71,7 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement
* @var bool If this property is set, this element can not be selected for part properties.
* Useful if this element should be used only for grouping, sorting.
* @ORM\Column(type="boolean")
* @Groups({"full", "import"})
*/
protected bool $not_selectable = false;
@@ -91,7 +92,7 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement
/**
* @var AbstractStructuralDBElement
* @NoneOfItsChildren()
* @Groups({"include_parents"})
* @Groups({"include_parents", "import"})
*/
protected $parent = null;

View File

@@ -46,6 +46,7 @@ use App\Entity\Base\AbstractNamedDBElement;
use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException;
use LogicException;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use function sprintf;
@@ -84,6 +85,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement
* @var string The mathematical symbol for this specification. Can be rendered pretty later. Should be short
* @Assert\Length(max=20)
* @ORM\Column(type="string", nullable=false)
* @Groups({"full"})
*/
protected string $symbol = '';
@@ -93,6 +95,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement
* @Assert\LessThanOrEqual(propertyPath="value_typical", message="parameters.validator.min_lesser_typical")
* @Assert\LessThan(propertyPath="value_max", message="parameters.validator.min_lesser_max")
* @ORM\Column(type="float", nullable=true)
* @Groups({"full"})
*/
protected ?float $value_min = null;
@@ -100,6 +103,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement
* @var float|null the typical value of this property
* @Assert\Type({"null", "float"})
* @ORM\Column(type="float", nullable=true)
* @Groups({"full"})
*/
protected ?float $value_typical = null;
@@ -108,24 +112,29 @@ abstract class AbstractParameter extends AbstractNamedDBElement
* @Assert\Type({"float", "null"})
* @Assert\GreaterThanOrEqual(propertyPath="value_typical", message="parameters.validator.max_greater_typical")
* @ORM\Column(type="float", nullable=true)
* @Groups({"full"})
*/
protected ?float $value_max = null;
/**
* @var string The unit in which the value values are given (e.g. V)
* @ORM\Column(type="string", nullable=false)
* @Groups({"full"})
*/
protected string $unit = '';
/**
* @var string a text value for the given property
* @ORM\Column(type="string", nullable=false)
* @Groups({"full"})
*/
protected string $value_text = '';
/**
* @var string the group this parameter belongs to
* @ORM\Column(type="string", nullable=false, name="param_group")
* @Groups({"full"})
* @Groups({"full"})
*/
protected string $group = '';

View File

@@ -27,6 +27,7 @@ use App\Entity\Base\AbstractPartsContainingDBElement;
use App\Entity\Parameters\CategoryParameter;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -56,48 +57,56 @@ class Category extends AbstractPartsContainingDBElement
/**
* @var string
* @ORM\Column(type="text")
* @Groups({"full", "import"})
*/
protected string $partname_hint = '';
/**
* @var string
* @ORM\Column(type="text")
* @Groups({"full", "import"})
*/
protected string $partname_regex = '';
/**
* @var bool
* @ORM\Column(type="boolean")
* @Groups({"full", "import"})
*/
protected bool $disable_footprints = false;
/**
* @var bool
* @ORM\Column(type="boolean")
* @Groups({"full", "import"})
*/
protected bool $disable_manufacturers = false;
/**
* @var bool
* @ORM\Column(type="boolean")
* @Groups({"full", "import"})
*/
protected bool $disable_autodatasheets = false;
/**
* @var bool
* @ORM\Column(type="boolean")
* @Groups({"full", "import"})
*/
protected bool $disable_properties = false;
/**
* @var string
* @ORM\Column(type="text")
* @Groups({"full", "import"})
*/
protected string $default_description = '';
/**
* @var string
* @ORM\Column(type="text")
* @Groups({"full", "import"})
*/
protected string $default_comment = '';
/**
@@ -105,6 +114,7 @@ class Category extends AbstractPartsContainingDBElement
* @ORM\OneToMany(targetEntity="App\Entity\Attachments\CategoryAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true)
* @ORM\OrderBy({"name" = "ASC"})
* @Assert\Valid()
* @Groups({"full"})
*/
protected $attachments;
@@ -112,6 +122,7 @@ class Category extends AbstractPartsContainingDBElement
* @ORM\OneToMany(targetEntity="App\Entity\Parameters\CategoryParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true)
* @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"})
* @Assert\Valid()
* @Groups({"full"})
*/
protected $parameters;

View File

@@ -28,6 +28,7 @@ use App\Entity\Parameters\MeasurementUnitParameter;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -48,6 +49,7 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
* or m (for meters).
* @ORM\Column(type="string", name="unit", nullable=true)
* @Assert\Length(max=10)
* @Groups({"extended", "full", "import"})
*/
protected ?string $unit = null;
@@ -55,6 +57,7 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
* @var bool Determines if the amount value associated with this unit should be treated as integer.
* Set to false, to measure continuous sizes likes masses or lengths.
* @ORM\Column(type="boolean", name="is_integer")
* @Groups({"extended", "full", "import"})
*/
protected bool $is_integer = false;
@@ -63,6 +66,7 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
* Useful for sizes like meters. For this the unit must be set
* @ORM\Column(type="boolean", name="use_si_prefix")
* @Assert\Expression("this.isUseSIPrefix() == false or this.getUnit() != null", message="validator.measurement_unit.use_si_prefix_needs_unit")
* @Groups({"full", "import"})
*/
protected bool $use_si_prefix = false;

View File

@@ -40,6 +40,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@@ -72,6 +73,7 @@ class Part extends AttachmentContainingDBElement
* @Assert\Valid()
* @ORM\OneToMany(targetEntity="App\Entity\Parameters\PartParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true)
* @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"})
* @Groups({"full"})
*/
protected $parameters;
@@ -96,6 +98,7 @@ class Part extends AttachmentContainingDBElement
* @ORM\OneToMany(targetEntity="App\Entity\Attachments\PartAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true)
* @ORM\OrderBy({"name" = "ASC"})
* @Assert\Valid()
* @Groups({"full"})
*/
protected $attachments;

View File

@@ -31,6 +31,7 @@ use App\Validator\Constraints\ValidPartLot;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Exception;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -52,12 +53,14 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
/**
* @var string A short description about this lot, shown in table
* @ORM\Column(type="text")
* @Groups({"simple", "extended", "full", "import"})
*/
protected string $description = '';
/**
* @var string a comment stored with this lot
* @ORM\Column(type="text")
* @Groups({"full", "import"})
*/
protected string $comment = '';
@@ -65,6 +68,7 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
* @var ?DateTime Set a time until when the lot must be used.
* Set to null, if the lot can be used indefinitely.
* @ORM\Column(type="datetime", name="expiration_date", nullable=true)
* @Groups({"extended", "full", "import"})
*/
protected ?DateTime $expiration_date = null;
@@ -73,12 +77,14 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
* @ORM\ManyToOne(targetEntity="Storelocation")
* @ORM\JoinColumn(name="id_store_location", referencedColumnName="id", nullable=true)
* @Selectable()
* @Groups({"simple", "extended", "full", "import"})
*/
protected ?Storelocation $storage_location = null;
/**
* @var bool If this is set to true, the instock amount is marked as not known
* @ORM\Column(type="boolean")
* @Groups({"simple", "extended", "full", "import"})
*/
protected bool $instock_unknown = false;
@@ -86,12 +92,14 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
* @var float For continuous sizes (length, volume, etc.) the instock is saved here.
* @ORM\Column(type="float")
* @Assert\PositiveOrZero()
* @Groups({"simple", "extended", "full", "import"})
*/
protected float $amount = 0.0;
/**
* @var bool determines if this lot was manually marked for refilling
* @ORM\Column(type="boolean")
* @Groups({"extended", "full", "import"})
*/
protected bool $needs_refill = false;

View File

@@ -24,6 +24,7 @@ namespace App\Entity\Parts\PartTraits;
use App\Entity\Parts\Part;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -34,12 +35,14 @@ trait AdvancedPropertyTrait
/**
* @var bool Determines if this part entry needs review (for example, because it is work in progress)
* @ORM\Column(type="boolean")
* @Groups({"extended", "full", "import"})
*/
protected bool $needs_review = false;
/**
* @var string a comma separated list of tags, associated with the part
* @ORM\Column(type="text")
* @Groups({"extended", "full", "import"})
*/
protected string $tags = '';
@@ -47,6 +50,7 @@ trait AdvancedPropertyTrait
* @var float|null how much a single part unit weighs in grams
* @ORM\Column(type="float", nullable=true)
* @Assert\PositiveOrZero()
* @Groups({"extended", "full", "import"})
*/
protected ?float $mass = null;
@@ -54,7 +58,7 @@ trait AdvancedPropertyTrait
* @var string The internal part number of the part
* @ORM\Column(type="string", length=100, nullable=true, unique=true)
* @Assert\Length(max="100")
*
* @Groups({"extended", "full", "import"})
*/
protected ?string $ipn = null;

View File

@@ -26,6 +26,7 @@ use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Validator\Constraints\Selectable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
trait BasicPropertyTrait
@@ -33,12 +34,14 @@ trait BasicPropertyTrait
/**
* @var string A text describing what this part does
* @ORM\Column(type="text")
* @Groups({"simple", "extended", "full", "import"})
*/
protected string $description = '';
/**
* @var string A comment/note related to this part
* @ORM\Column(type="text")
* @Groups({"extended", "full", "import"})
*/
protected string $comment = '';
@@ -51,6 +54,7 @@ trait BasicPropertyTrait
/**
* @var bool true, if the part is marked as favorite
* @ORM\Column(type="boolean")
* @Groups({"extended", "full", "import"})
*/
protected bool $favorite = false;
@@ -61,6 +65,7 @@ trait BasicPropertyTrait
* @ORM\JoinColumn(name="id_category", referencedColumnName="id", nullable=false)
* @Selectable()
* @Assert\NotNull(message="validator.select_valid_category")
* @Groups({"simple", "extended", "full", "import"})
*/
protected ?Category $category = null;
@@ -69,6 +74,7 @@ trait BasicPropertyTrait
* @ORM\ManyToOne(targetEntity="Footprint")
* @ORM\JoinColumn(name="id_footprint", referencedColumnName="id")
* @Selectable()
* @Groups({"simple", "extended", "full", "import"})
*/
protected ?Footprint $footprint = null;

View File

@@ -26,6 +26,7 @@ use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\PartLot;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -38,6 +39,7 @@ trait InstockTrait
* @ORM\OneToMany(targetEntity="PartLot", mappedBy="part", cascade={"persist", "remove"}, orphanRemoval=true)
* @Assert\Valid()
* @ORM\OrderBy({"amount" = "DESC"})
* @Groups({"extended", "full"})
*/
protected $partLots;
@@ -46,6 +48,7 @@ trait InstockTrait
* Given in the partUnit.
* @ORM\Column(type="float")
* @Assert\PositiveOrZero()
* @Groups({"extended", "full", "import"})
*/
protected float $minamount = 0;
@@ -53,6 +56,7 @@ trait InstockTrait
* @var ?MeasurementUnit the unit in which the part's amount is measured
* @ORM\ManyToOne(targetEntity="MeasurementUnit")
* @ORM\JoinColumn(name="id_part_unit", referencedColumnName="id", nullable=true)
* @Groups({"extended", "full", "import"})
*/
protected ?MeasurementUnit $partUnit = null;

View File

@@ -26,6 +26,7 @@ use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Part;
use App\Validator\Constraints\Selectable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -38,6 +39,7 @@ trait ManufacturerTrait
* @ORM\ManyToOne(targetEntity="Manufacturer")
* @ORM\JoinColumn(name="id_manufacturer", referencedColumnName="id")
* @Selectable()
* @Groups({"simple","extended", "full", "import"})
*/
protected ?Manufacturer $manufacturer = null;
@@ -45,12 +47,14 @@ trait ManufacturerTrait
* @var string the url to the part on the manufacturer's homepage
* @ORM\Column(type="string")
* @Assert\Url()
* @Groups({"full", "import"})
*/
protected string $manufacturer_product_url = '';
/**
* @var string The product number used by the manufacturer. If this is set to "", the name field is used.
* @ORM\Column(type="string")
* @Groups({"extended", "full", "import"})
*/
protected string $manufacturer_product_number = '';
@@ -58,6 +62,7 @@ trait ManufacturerTrait
* @var string The production status of this part. Can be one of the specified ones.
* @ORM\Column(type="string", length=255, nullable=true)
* @Assert\Choice({"announced", "active", "nrfnd", "eol", "discontinued", ""})
* @Groups({"extended", "full", "import"})
*/
protected ?string $manufacturing_status = '';

View File

@@ -24,6 +24,7 @@ namespace App\Entity\Parts\PartTraits;
use App\Entity\PriceInformations\Orderdetail;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use function count;
use Doctrine\Common\Collections\Collection;
@@ -39,6 +40,7 @@ trait OrderTrait
* @ORM\OneToMany(targetEntity="App\Entity\PriceInformations\Orderdetail", mappedBy="part", cascade={"persist", "remove"}, orphanRemoval=true)
* @Assert\Valid()
* @ORM\OrderBy({"supplierpartnr" = "ASC"})
* @Groups({"extended", "full"})
*/
protected $orderdetails;

View File

@@ -27,6 +27,7 @@ use App\Entity\Base\AbstractPartsContainingDBElement;
use App\Entity\Parameters\StorelocationParameter;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -70,20 +71,24 @@ class Storelocation extends AbstractPartsContainingDBElement
/**
* @var bool
* @ORM\Column(type="boolean")
* @Groups({"full", "import"})
*/
protected bool $is_full = false;
/**
* @var bool
* @ORM\Column(type="boolean")
* @Groups({"full", "import"})
*/
protected bool $only_single_part = false;
/**
* @var bool
* @ORM\Column(type="boolean")
* @Groups({"full", "import"})
*/
protected bool $limit_to_existing_parts = false;
/**
* @var Collection<int, StorelocationAttachment>
* @ORM\OneToMany(targetEntity="App\Entity\Attachments\StorelocationAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true)

View File

@@ -31,6 +31,7 @@ use App\Validator\Constraints\Selectable;
use Brick\Math\BigDecimal;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -74,6 +75,7 @@ class Supplier extends AbstractCompany
/**
* @var BigDecimal|null the shipping costs that have to be paid, when ordering via this supplier
* @ORM\Column(name="shipping_costs", nullable=true, type="big_decimal", precision=11, scale=5)
* @Groups({"extended", "full", "import"})
* @BigDecimalPositiveOrZero()
*/
protected ?BigDecimal $shipping_costs = null;

View File

@@ -32,6 +32,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -60,6 +61,7 @@ class Currency extends AbstractStructuralDBElement
* @var string the 3-letter ISO code of the currency
* @ORM\Column(type="string")
* @Assert\Currency()
* @Groups({"extended", "full", "import"})
*/
protected string $iso_code = "";

View File

@@ -34,6 +34,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -54,18 +55,21 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N
* @ORM\OneToMany(targetEntity="Pricedetail", mappedBy="orderdetail", cascade={"persist", "remove"}, orphanRemoval=true)
* @Assert\Valid()
* @ORM\OrderBy({"min_discount_quantity" = "ASC"})
* @Groups({"extended", "full", "import"})
*/
protected $pricedetails;
/**
* @var string
* @ORM\Column(type="string")
* @Groups({"extended", "full", "import"})
*/
protected string $supplierpartnr = '';
/**
* @var bool
* @ORM\Column(type="boolean")
* @Groups({"extended", "full", "import"})
*/
protected bool $obsolete = false;
@@ -73,6 +77,7 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N
* @var string
* @ORM\Column(type="string")
* @Assert\Url()
* @Groups({"full", "import"})
*/
protected string $supplier_product_url = '';
@@ -89,6 +94,7 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N
* @ORM\ManyToOne(targetEntity="App\Entity\Parts\Supplier", inversedBy="orderdetails")
* @ORM\JoinColumn(name="id_supplier", referencedColumnName="id")
* @Assert\NotNull(message="validator.orderdetail.supplier_must_not_be_null")
* @Groups({"extended", "full", "import"})
*/
protected ?Supplier $supplier = null;

View File

@@ -32,6 +32,7 @@ use Brick\Math\RoundingMode;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -55,6 +56,7 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface
* @var BigDecimal The price related to the detail. (Given in the selected currency)
* @ORM\Column(type="big_decimal", precision=11, scale=5)
* @BigDecimalPositive()
* @Groups({"extended", "full"})
*/
protected BigDecimal $price;
@@ -64,6 +66,7 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface
* @ORM\ManyToOne(targetEntity="Currency", inversedBy="pricedetails")
* @ORM\JoinColumn(name="id_currency", referencedColumnName="id", nullable=true)
* @Selectable()
* @Groups({"extended", "full", "import"})
*/
protected ?Currency $currency = null;
@@ -71,6 +74,7 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface
* @var float
* @ORM\Column(type="float")
* @Assert\Positive()
* @Groups({"extended", "full", "import"})
*/
protected float $price_related_quantity = 1.0;
@@ -78,6 +82,7 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface
* @var float
* @ORM\Column(type="float")
* @Assert\Positive()
* @Groups({"extended", "full", "import"})
*/
protected float $min_discount_quantity = 1.0;

View File

@@ -30,6 +30,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@@ -57,6 +58,7 @@ class Project extends AbstractStructuralDBElement
/**
* @ORM\OneToMany(targetEntity="ProjectBOMEntry", mappedBy="project", cascade={"persist", "remove"}, orphanRemoval=true)
* @Assert\Valid()
* @Groups({"extended", "full"})
*/
protected $bom_entries;
@@ -69,6 +71,7 @@ class Project extends AbstractStructuralDBElement
* @var string The current status of the project
* @ORM\Column(type="string", length=64, nullable=true)
* @Assert\Choice({"draft","planning","in_production","finished","archived"})
* @Groups({"extended", "full"})
*/
protected ?string $status = null;
@@ -86,6 +89,7 @@ class Project extends AbstractStructuralDBElement
/**
* @ORM\Column(type="text", nullable=false, columnDefinition="DEFAULT ''")
* @Groups({"simple", "extended", "full"})
*/
protected string $description = '';

View File

@@ -110,7 +110,8 @@ class ProjectBOMEntry extends AbstractDBElement
public function __construct()
{
$this->price = BigDecimal::zero()->toScale(5);
//$this->price = BigDecimal::zero()->toScale(5);
$this->price = null;
}
/**

View File

@@ -30,6 +30,7 @@ use App\Validator\Constraints\ValidPermission;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -65,6 +66,7 @@ class Group extends AbstractStructuralDBElement implements HasPermissionsInterfa
/**
* @var bool If true all users associated with this group must have enabled some kind of 2 factor authentication
* @ORM\Column(type="boolean", name="enforce_2fa")
* @Groups({"extended", "full", "import"})
*/
protected $enforce2FA = false;
/**
@@ -79,6 +81,7 @@ class Group extends AbstractStructuralDBElement implements HasPermissionsInterfa
* @var PermissionData|null
* @ValidPermission()
* @ORM\Embedded(class="PermissionData", columnPrefix="permissions_")
* @Groups({"full"})
*/
protected ?PermissionData $permissions = null;

View File

@@ -33,6 +33,7 @@ use App\Validator\Constraints\ValidTheme;
use Hslavich\OneloginSamlBundle\Security\User\SamlUserInterface;
use Jbtronics\TFAWebauthn\Model\LegacyU2FKeyInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Webauthn\PublicKeyCredentialUserEntity;
use function count;
use DateTime;
@@ -74,6 +75,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
/**
* @var bool Determines if the user is disabled (user can not log in)
* @ORM\Column(type="boolean")
* @Groups({"extended", "full", "import"})
*/
protected bool $disabled = false;
@@ -81,6 +83,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
* @var string|null The theme
* @ORM\Column(type="string", name="config_theme", nullable=true)
* @ValidTheme()
* @Groups({"full", "import"})
*/
protected ?string $theme = null;
@@ -124,6 +127,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
* @ORM\ManyToOne(targetEntity="Group", inversedBy="users")
* @ORM\JoinColumn(name="group_id", referencedColumnName="id")
* @Selectable()
* @Groups({"extended", "full", "import"})
*/
protected ?Group $group = null;
@@ -137,6 +141,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
* @var string|null The timezone the user prefers
* @ORM\Column(type="string", name="config_timezone", nullable=true)
* @Assert\Timezone()
* @Groups({"full", "import"})
*/
protected ?string $timezone = '';
@@ -144,6 +149,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
* @var string|null The language/locale the user prefers
* @ORM\Column(type="string", name="config_language", nullable=true)
* @Assert\Language()
* @Groups({"full", "import"})
*/
protected ?string $language = '';
@@ -151,30 +157,35 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
* @var string|null The email address of the user
* @ORM\Column(type="string", length=255, nullable=true)
* @Assert\Email()
* @Groups({"simple", "extended", "full", "import"})
*/
protected ?string $email = '';
/**
* @var string|null The department the user is working
* @ORM\Column(type="string", length=255, nullable=true)
* @Groups({"simple", "extended", "full", "import"})
*/
protected ?string $department = '';
/**
* @var string|null The last name of the User
* @ORM\Column(type="string", length=255, nullable=true)
* @Groups({"simple", "extended", "full", "import"})
*/
protected ?string $last_name = '';
/**
* @var string|null The first name of the User
* @ORM\Column(type="string", length=255, nullable=true)
* @Groups({"simple", "extended", "full", "import"})
*/
protected ?string $first_name = '';
/**
* @var bool True if the user needs to change password after log in
* @ORM\Column(type="boolean")
* @Groups({"extended", "full", "import"})
*/
protected bool $need_pw_change = true;
@@ -206,6 +217,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
/** @var DateTime|null The time when the backup codes were generated
* @ORM\Column(type="datetime", nullable=true)
* @Groups({"full"})
*/
protected ?DateTime $backupCodesGenerationDate = null;
@@ -228,6 +240,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
* @ORM\ManyToOne(targetEntity="App\Entity\PriceInformations\Currency")
* @ORM\JoinColumn(name="currency_id", referencedColumnName="id")
* @Selectable()
* @Groups({"extended", "full", "import"})
*/
protected $currency;
@@ -235,6 +248,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
* @var PermissionData
* @ValidPermission()
* @ORM\Embedded(class="PermissionData", columnPrefix="permissions_")
* @Groups({"simple", "extended", "full", "import"})
*/
protected ?PermissionData $permissions = null;
@@ -247,6 +261,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
/**
* @var bool True if the user was created by a SAML provider (and therefore cannot change its password)
* @ORM\Column(type="boolean")
* @Groups({"extended", "full"})
*/
protected bool $saml_user = false;

View File

@@ -23,6 +23,8 @@ declare(strict_types=1);
namespace App\Form\AdminPages;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parts\Category;
use App\Entity\Parts\Part;
use App\Form\Type\StructuralEntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -48,13 +50,14 @@ class ImportType extends AbstractType
//Disable import if user is not allowed to create elements.
$entity = new $data['entity_class']();
$perm_name = 'create';
$perm_name = 'import';
$disabled = !$this->security->isGranted($perm_name, $entity);
$builder
->add('format', ChoiceType::class, [
'choices' => [
'parts.import.format.auto' => 'auto',
'JSON' => 'json',
'XML' => 'xml',
'CSV' => 'csv',
@@ -63,7 +66,7 @@ class ImportType extends AbstractType
'label' => 'export.format',
'disabled' => $disabled,
])
->add('csv_separator', TextType::class, [
->add('csv_delimiter', TextType::class, [
'data' => ';',
'label' => 'import.csv_separator',
'disabled' => $disabled,
@@ -78,6 +81,51 @@ class ImportType extends AbstractType
]);
}
if ($entity instanceof Part) {
$builder->add('part_category', StructuralEntityType::class, [
'class' => Category::class,
'required' => false,
'label' => 'parts.import.part_category.label',
'help' => 'parts.import.part_category.help',
'disabled' => $disabled,
'disable_not_selectable' => true,
'allow_add' => true
]);
$builder->add('part_needs_review', CheckboxType::class, [
'data' => false,
'required' => false,
'label' => 'parts.import.part_needs_review.label',
'help' => 'parts.import.part_needs_review.help',
'disabled' => $disabled,
]);
}
if ($entity instanceof AbstractStructuralDBElement) {
$builder->add('preserve_children', CheckboxType::class, [
'data' => true,
'required' => false,
'label' => 'import.preserve_children',
'disabled' => $disabled,
]);
}
if ($entity instanceof Part) {
$builder->add('create_unknown_datastructures', CheckboxType::class, [
'data' => true,
'required' => false,
'label' => 'import.create_unknown_datastructures',
'help' => 'import.create_unknown_datastructures.help',
'disabled' => $disabled,
]);
$builder->add('path_delimiter', TextType::class, [
'data' => '->',
'label' => 'import.path_delimiter',
'help' => 'import.path_delimiter.help',
'disabled' => $disabled,
]);
}
$builder->add('file', FileType::class, [
'label' => 'import.file',
'attr' => [
@@ -86,21 +134,15 @@ class ImportType extends AbstractType
'data-show-upload' => 'false',
],
'disabled' => $disabled,
])
]);
->add('preserve_children', CheckboxType::class, [
'data' => true,
'required' => false,
'label' => 'import.preserve_children',
'disabled' => $disabled,
])
->add('abort_on_validation_error', CheckboxType::class, [
'data' => true,
'required' => false,
'label' => 'import.abort_on_validation',
'help' => 'import.abort_on_validation.help',
'disabled' => $disabled,
])
$builder->add('abort_on_validation_error', CheckboxType::class, [
'data' => true,
'required' => false,
'label' => 'import.abort_on_validation',
'help' => 'import.abort_on_validation.help',
'disabled' => $disabled,
])
//Buttons
->add('import', SubmitType::class, [

View File

@@ -29,6 +29,12 @@ use RecursiveIteratorIterator;
class StructuralDBElementRepository extends NamedDBElementRepository
{
/**
* @var array An array containing all new entities created by getNewEntityByPath.
* This is used to prevent creating multiple entities for the same path.
*/
private array $new_entity_cache = [];
/**
* Finds all nodes without a parent node. They are our root nodes.
*
@@ -91,7 +97,7 @@ class StructuralDBElementRepository extends NamedDBElementRepository
}
/**
* Creates a structure of AbsstractStructuralDBElements from a path separated by $separator, which splits the various levels.
* Creates a structure of AbstractStructuralDBElements from a path separated by $separator, which splits the various levels.
* This function will try to use existing elements, if they are already in the database. If not, they will be created.
* An array of the created elements will be returned, with the last element being the deepest element.
* @param string $path
@@ -108,14 +114,67 @@ class StructuralDBElementRepository extends NamedDBElementRepository
continue;
}
//See if we already have an element with this name and parent
$entity = $this->findOneBy(['name' => $name, 'parent' => $parent]);
//Use the cache to prevent creating multiple entities for the same path
$entity = $this->getNewEntityFromCache($name, $parent);
//See if we already have an element with this name and parent in the database
if (!$entity) {
$entity = $this->findOneBy(['name' => $name, 'parent' => $parent]);
}
if (null === $entity) {
$class = $this->getClassName();
/** @var AbstractStructuralDBElement $entity */
$entity = new $class;
$entity->setName($name);
$entity->setParent($parent);
$this->setNewEntityToCache($entity);
}
$result[] = $entity;
$parent = $entity;
}
return $result;
}
private function getNewEntityFromCache(string $name, ?AbstractStructuralDBElement $parent): ?AbstractStructuralDBElement
{
$key = $parent ? $parent->getFullPath('%->%').'%->%'.$name : $name;
if (isset($this->new_entity_cache[$key])) {
return $this->new_entity_cache[$key];
}
return null;
}
private function setNewEntityToCache(AbstractStructuralDBElement $entity): void
{
$key = $entity->getFullPath('%->%');
$this->new_entity_cache[$key] = $entity;
}
/**
* Returns an element of AbstractStructuralDBElements queried from a path separated by $separator, which splits the various levels.
* An array of the created elements will be returned, with the last element being the deepest element.
* If no element was found, an empty array will be returned.
* @param string $path
* @param string $separator
* @return AbstractStructuralDBElement[]
*/
public function getEntityByPath(string $path, string $separator = '->'): array
{
$parent = null;
$result = [];
foreach (explode($separator, $path) as $name) {
$name = trim($name);
if ('' === $name) {
continue;
}
//See if we already have an element with this name and parent
$entity = $this->findOneBy(['name' => $name, 'parent' => $parent]);
if (null === $entity) {
return [];
}
$result[] = $entity;

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Serializer;
use Brick\Math\BigDecimal;
use Brick\Math\BigNumber;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class BigNumberNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
{
public function supportsNormalization($data, string $format = null): bool
{
return $data instanceof BigNumber;
}
public function normalize($object, string $format = null, array $context = []): string
{
if (!$object instanceof BigNumber) {
throw new \InvalidArgumentException('This normalizer only supports BigNumber objects!');
}
return (string) $object;
}
public function hasCacheableSupportsMethod(): bool
{
return true;
}
}

View File

@@ -0,0 +1,176 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Serializer;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\Storelocation;
use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
use Brick\Math\BigDecimal;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class PartNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
{
private const DENORMALIZE_KEY_MAPPING = [
'notes' => 'comment',
'quantity' => 'instock',
'amount' => 'instock',
'mpn' => 'manufacturer_product_number',
'spn' => 'supplier_part_number',
'supplier_product_number' => 'supplier_part_number',
'storage_location' => 'storelocation',
];
private ObjectNormalizer $normalizer;
private StructuralElementFromNameDenormalizer $locationDenormalizer;
public function __construct(ObjectNormalizer $normalizer, StructuralElementFromNameDenormalizer $locationDenormalizer)
{
$this->normalizer = $normalizer;
$this->locationDenormalizer = $locationDenormalizer;
}
public function supportsNormalization($data, string $format = null): bool
{
return $data instanceof Part;
}
public function normalize($object, string $format = null, array $context = [])
{
if (!$object instanceof Part) {
throw new \InvalidArgumentException('This normalizer only supports Part objects!');
}
$data = $this->normalizer->normalize($object, $format, $context);
//Remove type field for CSV export
if ($format === 'csv') {
unset($data['type']);
}
$data['total_instock'] = $object->getAmountSum();
return $data;
}
public function supportsDenormalization($data, string $type, string $format = null): bool
{
return is_array($data) && is_a($type, Part::class, true);
}
private function normalizeKeys(array &$data): array
{
//Rename keys based on the mapping, while leaving the data untouched
foreach ($data as $key => $value) {
if (isset(self::DENORMALIZE_KEY_MAPPING[$key])) {
$data[self::DENORMALIZE_KEY_MAPPING[$key]] = $value;
unset($data[$key]);
}
}
return $data;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
$this->normalizeKeys($data);
//Empty IPN should be null, or we get a constraint error
if (isset($data['ipn']) && $data['ipn'] === '') {
$data['ipn'] = null;
}
//Fill empty needs_review and needs_review_comment fields with false
if (empty($data['needs_review'])) {
$data['needs_review'] = false;
}
if (empty($data['favorite'])) {
$data['favorite'] = false;
}
if (empty($data['minamount'])) {
$data['minamount'] = 0.0;
}
$object = $this->normalizer->denormalize($data, $type, $format, $context);
if (!$object instanceof Part) {
throw new \InvalidArgumentException('This normalizer only supports Part objects!');
}
if ((isset($data['instock']) && trim($data['instock']) !== "") || (isset($data['storelocation']) && trim($data['storelocation']) !== "")) {
$partLot = new PartLot();
if (isset($data['instock']) && $data['instock'] !== "") {
//Replace comma with dot
$instock = (float) str_replace(',', '.', $data['instock']);
$partLot->setAmount($instock);
} else {
$partLot->setInstockUnknown(true);
}
if (isset($data['storelocation']) && $data['storelocation'] !== "") {
$location = $this->locationDenormalizer->denormalize($data['storelocation'], Storelocation::class, $format, $context);
$partLot->setStorageLocation($location);
}
$object->addPartLot($partLot);
}
if (isset($data['supplier']) && $data['supplier'] !== "") {
$supplier = $this->locationDenormalizer->denormalize($data['supplier'], Supplier::class, $format, $context);
if ($supplier) {
$orderdetail = new Orderdetail();
$orderdetail->setSupplier($supplier);
if (isset($data['supplier_part_number']) && $data['supplier_part_number'] !== "") {
$orderdetail->setSupplierpartnr($data['supplier_part_number']);
}
$object->addOrderdetail($orderdetail);
if (isset($data['price']) && $data['price'] !== "") {
$pricedetail = new Pricedetail();
$pricedetail->setMinDiscountQuantity(1);
$pricedetail->setPriceRelatedQuantity(1);
$price = BigDecimal::of(str_replace(',', '.', $data['price']));
$pricedetail->setPrice($price);
$orderdetail->addPricedetail($pricedetail);
}
}
}
return $object;
}
public function hasCacheableSupportsMethod(): bool
{
//Must be false, because we rely on is_array($data) in supportsDenormalization()
return false;
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Serializer;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Form\Type\StructuralEntityType;
use App\Repository\StructuralDBElementRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
class StructuralElementFromNameDenormalizer implements DenormalizerInterface, CacheableSupportsMethodInterface
{
private EntityManagerInterface $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function supportsDenormalization($data, string $type, string $format = null)
{
return is_string($data) && is_subclass_of($type, AbstractStructuralDBElement::class);
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
//Retrieve the repository for the given type
/** @var StructuralDBElementRepository $repo */
$repo = $this->em->getRepository($type);
$path_delimiter = $context['path_delimiter'] ?? '->';
if ($context['create_unknown_datastructures'] ?? false) {
$elements = $repo->getNewEntityFromPath($data, $path_delimiter);
//Persist all new elements
foreach ($elements as $element) {
$this->em->persist($element);
}
if (empty($elements)) {
return null;
}
return end($elements);
}
$elements = $repo->getEntityByPath($data, $path_delimiter);
if (empty($elements)) {
return null;
}
return end($elements);
}
public function hasCacheableSupportsMethod(): bool
{
//Must be false, because we do a is_string check on data in supportsDenormalization
return false;
}
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Serializer;
use App\Entity\Base\AbstractStructuralDBElement;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class StructuralElementNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
{
private NormalizerInterface $normalizer;
public function __construct(ObjectNormalizer $normalizer)
{
$this->normalizer = $normalizer;
}
public function supportsNormalization($data, string $format = null): bool
{
return $data instanceof AbstractStructuralDBElement;
}
public function normalize($object, string $format = null, array $context = [])
{
if (!$object instanceof AbstractStructuralDBElement) {
throw new \InvalidArgumentException('This normalizer only supports AbstractStructural objects!');
}
$data = $this->normalizer->normalize($object, $format, $context);
//Remove type field for CSV export
if ($format === 'csv') {
unset($data['type']);
}
$data['full_name'] = $object->getFullPath('->');
return $data;
}
public function hasCacheableSupportsMethod(): bool
{
return true;
}
}

View File

@@ -164,23 +164,23 @@ class EntityURLGenerator
throw new EntityNotSupportedException('The given entity is not supported yet!');
}
public function viewURL(Attachment $entity): ?string
public function viewURL(Attachment $entity): string
{
if ($entity->isExternal()) { //For external attachments, return the link to external path
return $entity->getURL();
}
//return $this->urlGenerator->generate('attachment_view', ['id' => $entity->getID()]);
return $this->attachmentURLGenerator->getViewURL($entity);
return $this->attachmentURLGenerator->getViewURL($entity) ?? '';
}
public function downloadURL($entity): ?string
public function downloadURL($entity): string
{
if ($entity instanceof Attachment) {
if ($entity->isExternal()) { //For external attachments, return the link to external path
return $entity->getURL();
}
return $this->attachmentURLGenerator->getDownloadURL($entity);
return $this->attachmentURLGenerator->getDownloadURL($entity) ?? '';
}
//Otherwise throw an error

View File

@@ -0,0 +1,146 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\ImportExportSystem;
use App\Entity\ProjectSystem\Project;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use InvalidArgumentException;
use League\Csv\Reader;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class BOMImporter
{
private const MAP_KICAD_PCB_FIELDS = [
'ID' => 'Id',
'Bezeichner' => 'Designator',
'Footprint' => 'Package',
'Stückzahl' => 'Quantity',
'Bezeichnung' => 'Designation',
'Anbieter und Referenz' => 'Supplier and ref',
];
public function __construct()
{
}
protected function configureOptions(OptionsResolver $resolver): OptionsResolver
{
$resolver->setRequired('type');
$resolver->setAllowedValues('type', ['kicad_pcbnew']);
return $resolver;
}
/**
* Converts the given file into an array of BOM entries using the given options and save them into the given project.
* The changes are not saved into the database yet.
* @param File $file
* @param array $options
* @param Project $project
* @return ProjectBOMEntry[]
*/
public function importFileIntoProject(File $file, Project $project, array $options): array
{
$bom_entries = $this->fileToBOMEntries($file, $options);
//Assign the bom_entries to the project
foreach ($bom_entries as $bom_entry) {
$project->addBomEntry($bom_entry);
}
return $bom_entries;
}
/**
* Converts the given file into an array of BOM entries using the given options.
* @param File $file
* @param array $options
* @return ProjectBOMEntry[]
*/
public function fileToBOMEntries(File $file, array $options): array
{
return $this->stringToBOMEntries($file->getContent(), $options);
}
/**
* Import string data into an array of BOM entries, which are not yet assigned to a project.
* @param string $data The data to import
* @param array $options An array of options
* @return ProjectBOMEntry[] An array of imported entries
*/
public function stringToBOMEntries(string $data, array $options): array
{
$resolver = new OptionsResolver();
$resolver = $this->configureOptions($resolver);
$options = $resolver->resolve($options);
switch ($options['type']) {
case 'kicad_pcbnew':
return $this->parseKiCADPCB($data, $options);
default:
throw new InvalidArgumentException('Invalid import type!');
}
}
private function parseKiCADPCB(string $data, array $options = []): array
{
$csv = Reader::createFromString($data);
$csv->setDelimiter(';');
$csv->setHeaderOffset(0);
$bom_entries = [];
foreach ($csv->getRecords() as $offset => $entry) {
//Translate the german field names to english
$entry = array_combine(array_map(function ($key) {
return self::MAP_KICAD_PCB_FIELDS[$key] ?? $key;
}, array_keys($entry)), $entry);
//Ensure that the entry has all required fields
if (!isset ($entry['Designator'])) {
throw new \UnexpectedValueException('Designator missing at line '.($offset + 1).'!');
}
if (!isset ($entry['Package'])) {
throw new \UnexpectedValueException('Package missing at line '.($offset + 1).'!');
}
if (!isset ($entry['Designation'])) {
throw new \UnexpectedValueException('Designation missing at line '.($offset + 1).'!');
}
if (!isset ($entry['Quantity'])) {
throw new \UnexpectedValueException('Quantity missing at line '.($offset + 1).'!');
}
$bom_entry = new ProjectBOMEntry();
$bom_entry->setName($entry['Designation'] . ' (' . $entry['Package'] . ')');
$bom_entry->setMountnames($entry['Designator'] ?? '');
$bom_entry->setComment($entry['Supplier and ref'] ?? '');
$bom_entry->setQuantity((float) ($entry['Quantity'] ?? 1));
$bom_entries[] = $bom_entry;
}
return $bom_entries;
}
}

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Services\ImportExportSystem;
use App\Entity\Base\AbstractNamedDBElement;
use Symfony\Component\OptionsResolver\OptionsResolver;
use function in_array;
use InvalidArgumentException;
use function is_array;
@@ -32,6 +33,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Serializer\SerializerInterface;
use function Symfony\Component\String\u;
/**
* Use this class to export an entity to multiple file formats.
@@ -42,104 +44,136 @@ class EntityExporter
public function __construct(SerializerInterface $serializer)
{
/*$encoders = [new XmlEncoder(), new JsonEncoder(), new CSVEncoder(), new YamlEncoder()];
$normalizers = [new ObjectNormalizer(), new DateTimeNormalizer()];
$this->serializer = new Serializer($normalizers, $encoders);
$this->serializer-> */
$this->serializer = $serializer;
}
protected function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('format', 'csv');
$resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml']);
$resolver->setDefault('csv_delimiter', ';');
$resolver->setAllowedTypes('csv_delimiter', 'string');
$resolver->setDefault('level', 'extended');
$resolver->setAllowedValues('level', ['simple', 'extended', 'full']);
$resolver->setDefault('include_children', false);
$resolver->setAllowedTypes('include_children', 'bool');
}
/**
* Exports an Entity or an array of entities to multiple file formats.
* Export the given entities using the given options.
* @param AbstractNamedDBElement|AbstractNamedDBElement[] $entities The data to export
* @param array $options The options to use for exporting
* @return string The serialized data
*/
public function exportEntities($entities, array $options): string
{
if (!is_array($entities)) {
$entities = [$entities];
}
//Ensure that all entities are of type AbstractNamedDBElement
$entity_type = null;
foreach ($entities as $entity) {
if (!$entity instanceof AbstractNamedDBElement) {
throw new InvalidArgumentException('All entities must be of type AbstractNamedDBElement!');
}
}
$resolver = new OptionsResolver();
$this->configureOptions($resolver);
$options = $resolver->resolve($options);
//If include children is set, then we need to add the include_children group
$groups = [$options['level']];
if ($options['include_children']) {
$groups[] = 'include_children';
}
return $this->serializer->serialize($entities, $options['format'],
[
'groups' => $groups,
'as_collection' => true,
'csv_delimiter' => $options['csv_delimiter'],
'xml_root_node_name' => 'PartDBExport',
]
);
}
/**
* Exports an Entity or an array of entities to multiple file formats.
*
* @param Request $request the request that should be used for option resolving
* @param AbstractNamedDBElement|object[] $entity
* @param AbstractNamedDBElement|object[] $entities
*
* @return Response the generated response containing the exported data
*
* @throws ReflectionException
*/
public function exportEntityFromRequest($entity, Request $request): Response
public function exportEntityFromRequest($entities, Request $request): Response
{
$format = $request->get('format') ?? 'json';
$options = [
'format' => $request->get('format') ?? 'json',
'level' => $request->get('level') ?? 'extended',
'include_children' => $request->request->getBoolean('include_children') ?? false,
];
//Check if we have one of the supported formats
if (!in_array($format, ['json', 'csv', 'yaml', 'xml'], true)) {
throw new InvalidArgumentException('Given format is not supported!');
if (!is_array($entities)) {
$entities = [$entities];
}
//Check export verbosity level
$level = $request->get('level') ?? 'extended';
if (!in_array($level, ['simple', 'extended', 'full'], true)) {
throw new InvalidArgumentException('Given level is not supported!');
}
//Do the serialization with the given options
$serialized_data = $this->exportEntities($entities, $options);
//Check for include children option
$include_children = $request->get('include_children') ?? false;
$response = new Response($serialized_data);
//Check which groups we need to export, based on level and include_children
$groups = [$level];
if ($include_children) {
$groups[] = 'include_children';
}
//Resolve the format
$optionsResolver = new OptionsResolver();
$this->configureOptions($optionsResolver);
$options = $optionsResolver->resolve($options);
//Determine the content type for the response
//Plain text should work for all types
$content_type = 'text/plain';
//Try to use better content types based on the format
$format = $options['format'];
switch ($format) {
case 'xml':
$content_type = 'application/xml';
break;
case 'json':
$content_type = 'application/json';
break;
}
//Ensure that we always serialize an array. This makes it easier to import the data again.
if (is_array($entity)) {
$entity_array = $entity;
} else {
$entity_array = [$entity];
}
$serialized_data = $this->serializer->serialize($entity_array, $format,
[
'groups' => $groups,
'as_collection' => true,
'csv_delimiter' => ';', //Better for Excel
'xml_root_node_name' => 'PartDBExport',
]);
$response = new Response($serialized_data);
$response->headers->set('Content-Type', $content_type);
//If view option is not specified, then download the file.
if (!$request->get('view')) {
if ($entity instanceof AbstractNamedDBElement) {
$entity_name = $entity->getName();
} elseif (is_array($entity)) {
if (empty($entity)) {
throw new InvalidArgumentException('$entity must not be empty!');
}
//Use the class name of the first element for the filename
$reflection = new ReflectionClass($entity[0]);
$entity_name = $reflection->getShortName();
//Determine the filename
//When we only have one entity, then we can use the name of the entity
if (count($entities) === 1) {
$entity_name = $entities[0]->getName();
} else {
throw new InvalidArgumentException('$entity type is not supported!');
//Use the class name of the first element for the filename otherwise
$reflection = new ReflectionClass($entities[0]);
$entity_name = $reflection->getShortName();
}
$level = $options['level'];
$filename = 'export_'.$entity_name.'_'.$level.'.'.$format;
// Create the disposition of the file
$disposition = $response->headers->makeDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
$filename,
$string = preg_replace('![^'.preg_quote('-', '!').'a-z0-_9\s]+!', '', strtolower($filename))
u($filename)->ascii()->toString(),
);
// Set the content disposition
$response->headers->set('Content-Disposition', $disposition);

View File

@@ -24,6 +24,9 @@ namespace App\Services\ImportExportSystem;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parts\Category;
use App\Entity\Parts\Part;
use Symplify\EasyCodingStandard\ValueObject\Option;
use function count;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
@@ -48,7 +51,7 @@ class EntityImporter
/**
* Creates many entries at once, based on a (text) list of name.
* The created enties are not persisted to database yet, so you have to do it yourself.
* The created entities are not persisted to database yet, so you have to do it yourself.
*
* @param string $lines The list of names seperated by \n
* @param string $class_name The name of the class for which the entities should be created
@@ -130,87 +133,38 @@ class EntityImporter
}
/**
* This methods deserializes the given file and saves it database.
* The imported elements will be checked (validated) before written to database.
*
* @param File $file the file that should be used for importing
* @param string $class_name the class name of the enitity that should be imported
* @param array $options options for the import process
*
* @return array An associative array containing an ConstraintViolationList and the entity name as key are returned,
* if an error happened during validation. When everything was successfull, the array should be empty.
* Import data from a string.
* @param string $data The serialized data which should be imported
* @param array $options The options for the import process
* @param array $errors An array which will be filled with the validation errors, if any occurs during import
* @return array An array containing all valid imported entities
*/
public function fileToDBEntities(File $file, string $class_name, array $options = []): array
public function importString(string $data, array $options = [], array &$errors = []): array
{
$resolver = new OptionsResolver();
$this->configureOptions($resolver);
$options = $resolver->resolve($options);
$entities = $this->fileToEntityArray($file, $class_name, $options);
$errors = [];
//Iterate over each $entity write it to DB.
foreach ($entities as $entity) {
/** @var AbstractStructuralDBElement $entity */
//Move every imported entity to the selected parent
$entity->setParent($options['parent']);
//Validate entity
$tmp = $this->validator->validate($entity);
//When no validation error occured, persist entity to database (cascade must be set in entity)
if (null === $tmp) {
$this->em->persist($entity);
} else { //Log validation errors to global log.
$errors[$entity->getFullPath()] = $tmp;
}
if (!is_a($options['class'], AbstractNamedDBElement::class, true)) {
throw new InvalidArgumentException('$class_name must be an AbstractNamedDBElement type!');
}
//Save changes to database, when no error happened, or we should continue on error.
if (empty($errors) || false === $options['abort_on_validation_error']) {
$this->em->flush();
}
return $errors;
}
/**
* This method converts (deserialize) a (uploaded) file to an array of entities with the given class.
*
* The imported elements will NOT be validated. If you want to use the result array, you have to validate it by yourself.
*
* @param File $file the file that should be used for importing
* @param string $class_name the class name of the enitity that should be imported
* @param array $options options for the import process
*
* @return array an array containing the deserialized elements
*/
public function fileToEntityArray(File $file, string $class_name, array $options = []): array
{
$resolver = new OptionsResolver();
$this->configureOptions($resolver);
$options = $resolver->resolve($options);
//Read file contents
$content = file_get_contents($file->getRealPath());
$groups = ['simple'];
$groups = ['import']; //We can only import data, that is marked with the group "import"
//Add group when the children should be preserved
if ($options['preserve_children']) {
$groups[] = 'include_children';
}
//The [] behind class_name denotes that we expect an array.
$entities = $this->serializer->deserialize($content, $class_name.'[]', $options['format'],
$entities = $this->serializer->deserialize($data, $options['class'].'[]', $options['format'],
[
'groups' => $groups,
'csv_delimiter' => $options['csv_separator'],
'csv_delimiter' => $options['csv_delimiter'],
'create_unknown_datastructures' => $options['create_unknown_datastructures'],
'path_delimiter' => $options['path_delimiter'],
]);
//Ensure we have an array of entitity elements.
//Ensure we have an array of entity elements.
if (!is_array($entities)) {
$entities = [$entities];
}
@@ -220,18 +174,143 @@ class EntityImporter
$this->correctParentEntites($entities, null);
}
//Set the parent of the imported elements to the given options
foreach ($entities as $entity) {
if ($entity instanceof AbstractStructuralDBElement) {
$entity->setParent($options['parent']);
}
if ($entity instanceof Part) {
if ($options['part_category']) {
$entity->setCategory($options['part_category']);
}
if ($options['part_needs_review']) {
$entity->setNeedsReview(true);
}
}
}
//Validate the entities
$errors = [];
//Iterate over each $entity write it to DB.
foreach ($entities as $key => $entity) {
//Validate entity
$tmp = $this->validator->validate($entity);
if (count($tmp) > 0) { //Log validation errors to global log.
$name = $entity instanceof AbstractStructuralDBElement ? $entity->getFullPath() : $entity->getName();
$errors[$name] = [
'violations' => $tmp,
'entity' => $entity,
];
//Remove the invalid entity from the array
unset($entities[$key]);
}
}
return $entities;
}
protected function configureOptions(OptionsResolver $resolver): void
protected function configureOptions(OptionsResolver $resolver): OptionsResolver
{
$resolver->setDefaults([
'csv_separator' => ';',
'format' => 'json',
'csv_delimiter' => ';', //The separator to use when importing csv files
'format' => 'json', //The format of the file that should be imported
'class' => AbstractNamedDBElement::class,
'preserve_children' => true,
'parent' => null,
'parent' => null, //The parent element to which the imported elements should be added
'abort_on_validation_error' => true,
'part_category' => null,
'part_needs_review' => false, //If true, the imported parts will be marked as "needs review", otherwise the value from the file will be used
'create_unknown_datastructures' => true, //If true, unknown datastructures (categories, footprints, etc.) will be created on the fly
'path_delimiter' => '->', //The delimiter used to separate the path elements in the name of a structural element
]);
$resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml']);
$resolver->setAllowedTypes('csv_delimiter', 'string');
$resolver->setAllowedTypes('preserve_children', 'bool');
$resolver->setAllowedTypes('class', 'string');
$resolver->setAllowedTypes('part_category', [Category::class, 'null']);
$resolver->setAllowedTypes('part_needs_review', 'bool');
return $resolver;
}
/**
* This method deserializes the given file and writes the entities to the database (and flush the db).
* The imported elements will be checked (validated) before written to database.
*
* @param File $file the file that should be used for importing
* @param array $options options for the import process
* @param AbstractNamedDBElement[] $entities The imported entities are returned in this array
*
* @return array An associative array containing an ConstraintViolationList and the entity name as key are returned,
* if an error happened during validation. When everything was successfully, the array should be empty.
*/
public function importFileAndPersistToDB(File $file, array $options = [], array &$entities = []): array
{
$options = $this->configureOptions(new OptionsResolver())->resolve($options);
$errors = [];
$entities = $this->importFile($file, $options, $errors);
//When we should abort on validation error, do nothing and return the errors
if (!empty($errors) && $options['abort_on_validation_error']) {
return $errors;
}
//Iterate over each $entity write it to DB (the invalid entities were already filtered out).
foreach ($entities as $entity) {
$this->em->persist($entity);
}
//Save changes to database, when no error happened, or we should continue on error.
$this->em->flush();
return $errors;
}
/**
* This method converts (deserialize) a (uploaded) file to an array of entities with the given class.
* The imported elements are not persisted to database yet, so you have to do it yourself.
*
* @param File $file the file that should be used for importing
* @param array $options options for the import process
*
* @return array an array containing the deserialized elements
*/
public function importFile(File $file, array $options = [], array &$errors = []): array
{
return $this->importString($file->getContent(), $options, $errors);
}
/**
* Determines the format to import based on the file extension.
* @param string $extension The file extension to use
* @return string The format to use (json, xml, csv, yaml), or null if the extension is unknown
*/
public function determineFormat(string $extension): ?string
{
//Convert the extension to lower case
$extension = strtolower($extension);
switch ($extension) {
case 'json':
return 'json';
case 'xml':
return 'xml';
case 'csv':
case 'tsv':
return 'csv';
case 'yaml':
case 'yml':
return 'yaml';
default:
return null;
}
}
/**

View File

@@ -102,6 +102,34 @@ final class PartsTableActionHandler
);
}
//When action starts with "export_" we have to redirect to the export controller
$matches = [];
if (preg_match('/^export_(json|yaml|xml|csv)$/', $action, $matches)) {
$ids = implode(',', array_map(static fn (Part $part) => $part->getID(), $selected_parts));
switch ($target_id) {
case 1:
default:
$level = 'simple';
break;
case 2:
$level = 'extended';
break;
case 3:
$level = 'full';
break;
}
return new RedirectResponse(
$this->urlGenerator->generate('parts_export', [
'format' => $matches[1],
'level' => $level,
'ids' => $ids,
'_redirect' => $redirect_url
])
);
}
//Iterate over the parts and apply the action to it:
foreach ($selected_parts as $part) {

View File

@@ -143,6 +143,12 @@ class ToolsTreeBuilder
$this->urlGenerator->generate('tools_ic_logos')
))->setIcon('fa-treeview fa-fw fa-solid fa-flag');
}
if ($this->security->isGranted('@parts.import')) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('parts.import.title'),
$this->urlGenerator->generate('parts_import')
))->setIcon('fa-treeview fa-fw fa-solid fa-file-import');
}
return $nodes;
}

View File

@@ -271,6 +271,28 @@ class PermissionManager
}
}
/**
* This function sets all operations of the given permission to the given value, except the ones listed in the except array.
* @param HasPermissionsInterface $perm_holder
* @param string $permission
* @param bool|null $new_value
* @param array $except
* @return void
*/
public function setAllOperationsOfPermissionExcept(HasPermissionsInterface $perm_holder, string $permission, ?bool $new_value, array $except): void
{
if (!$this->isValidPermission($permission)) {
throw new InvalidArgumentException(sprintf('A permission with that name is not existing! Got %s.', $permission));
}
foreach ($this->permission_structure['perms'][$permission]['operations'] as $op_key => $op) {
if (in_array($op_key, $except, true)) {
continue;
}
$this->setPermission($perm_holder, $permission, $op_key, $new_value);
}
}
protected function generatePermissionStructure()
{
$cache = new ConfigCache($this->cache_file, $this->is_debug);

View File

@@ -93,6 +93,20 @@ class PermissionPresetsHelper
//Allow access to system log and server infos
$this->permissionResolver->setPermission($perm_holder, 'system', 'show_logs', PermissionData::ALLOW);
$this->permissionResolver->setPermission($perm_holder, 'system', 'server_infos', PermissionData::ALLOW);
//Allow import for all datastructures
$this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'parts', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'parts_stock', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'categories', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'storelocations', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'footprints', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'manufacturers', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'attachment_types', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'currencies', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'measurement_units', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'suppliers', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'projects', PermissionData::ALLOW);
}
private function editor(HasPermissionsInterface $permHolder): HasPermissionsInterface
@@ -101,17 +115,18 @@ class PermissionPresetsHelper
$this->readOnly($permHolder);
//Set datastructures
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'parts', PermissionData::ALLOW);
//By default import is restricted to administrators, as it allows to fill up the database very fast
$this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'parts', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'parts_stock', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'categories', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'storelocations', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'footprints', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'manufacturers', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'attachment_types', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'currencies', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'measurement_units', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'suppliers', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'projects', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'categories', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'storelocations', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'footprints', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'manufacturers', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'attachment_types', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'currencies', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'measurement_units', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'suppliers', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'projects', PermissionData::ALLOW, ['import']);
//Attachments permissions
$this->permissionResolver->setPermission($permHolder, 'attachments', 'show_private', PermissionData::ALLOW);

View File

@@ -179,7 +179,7 @@
<hr>
<fieldset>
<legend>{% trans %}export_all.label{% endtrans %}</legend>
{% include 'admin/_export_form.html.twig' with {'path' : path('attachment_type_export_all')} %}
{% include 'admin/_export_form.html.twig' with {'path' : path(route_base ~ '_export_all')} %}
</fieldset>
</div>

View File

@@ -51,5 +51,12 @@
{% form_theme form.bom_entries with ['form/collection_types_layout.html.twig'] %}
{{ form_errors(form.bom_entries) }}
{{ form_widget(form.bom_entries) }}
{% if entity.id %}
<a href="{{ path('project_import_bom', {'id': entity.id}) }}" class="btn btn-secondary mb-2"
{% if not is_granted('edit', entity) %}disabled="disabled"{% endif %}>
<i class="fa-solid fa-file-import fa-fw"></i>
{% trans %}project.edit.bom.import_bom{% endtrans %}
</a>
{% endif %}
</div>
{% endblock %}

View File

@@ -47,10 +47,10 @@
<option {% if not is_granted('@parts.edit') %}disabled{% endif %} value="unset_needs_review">{% trans %}part_list.action.action.unset_needs_review{% endtrans %}</option>
</optgroup>
<optgroup label="{% trans %}part_list.action.action.group.change_field{% endtrans %}">
<option {% if not is_granted('@categories.read') %}disabled{% endif %} value="change_category" data-url="{{ path('select_category') }}">{% trans %}part_list.action.action.change_category{% endtrans %}</option>
<option {% if not is_granted('@footprints.read') %}disabled{% endif %} value="change_footprint" data-url="{{ path('select_footprint') }}">{% trans %}part_list.action.action.change_footprint{% endtrans %}</option>
<option {% if not is_granted('@manufacturers.read') %}disabled{% endif %} value="change_manufacturer" data-url="{{ path('select_manufacturer') }}">{% trans %}part_list.action.action.change_manufacturer{% endtrans %}</option>
<option {% if not is_granted('@measurement_units.read') %}disabled{% endif %} value="change_unit" data-url="{{ path('select_measurement_unit') }}">{% trans %}part_list.action.action.change_unit{% endtrans %}</option>
<option {% if not is_granted('@parts.edit') %}disabled{% endif %} value="change_category" data-url="{{ path('select_category') }}">{% trans %}part_list.action.action.change_category{% endtrans %}</option>
<option {% if not is_granted('@parts.edit') %}disabled{% endif %} value="change_footprint" data-url="{{ path('select_footprint') }}">{% trans %}part_list.action.action.change_footprint{% endtrans %}</option>
<option {% if not is_granted('@parts.edit') %}disabled{% endif %} value="change_manufacturer" data-url="{{ path('select_manufacturer') }}">{% trans %}part_list.action.action.change_manufacturer{% endtrans %}</option>
<option {% if not is_granted('@parts.edit') %}disabled{% endif %} value="change_unit" data-url="{{ path('select_measurement_unit') }}">{% trans %}part_list.action.action.change_unit{% endtrans %}</option>
</optgroup>
<optgroup label="{% trans %}part_list.action.group.labels{% endtrans %}">
<option {% if not is_granted('@labels.create_labels') %}disabled{% endif %} value="generate_label_lot" data-url="{{ path('select_label_profiles_lot')}}">{% trans %}part_list.action.projects.generate_label_lot{% endtrans %}</option>
@@ -63,13 +63,19 @@
<optgroup label="{% trans %}part_list.action.action.delete{% endtrans %}">
<option {% if not is_granted('@parts.delete') %}disabled{% endif %} value="delete">{% trans %}part_list.action.action.delete{% endtrans %}</option>
</optgroup>
<optgroup label="{% trans %}part_list.action.action.export{% endtrans %}">
<option {% if not is_granted('@parts.read') %}disabled{% endif %} value="export_json" data-url="{{ path('select_export_level')}}" data-turbo="false">{% trans %}part_list.action.export_json{% endtrans %}</option>
<option {% if not is_granted('@parts.read') %}disabled{% endif %} value="export_csv" data-url="{{ path('select_export_level')}}" data-turbo="false">{% trans %}part_list.action.export_csv{% endtrans %}</option>
<option {% if not is_granted('@parts.read') %}disabled{% endif %} value="export_yaml" data-url="{{ path('select_export_level')}}" data-turbo="false">{% trans %}part_list.action.export_yaml{% endtrans %}</option>
<option {% if not is_granted('@parts.read') %}disabled{% endif %} value="export_xml" data-url="{{ path('select_export_level')}}" data-turbo="false">{% trans %}part_list.action.export_xml{% endtrans %}</option>
</optgroup>
</select>
<select class="form-select d-none" data-controller="elements--structural-entity-select" name="target" {{ stimulus_target('elements/datatables/parts', 'selectTargetPicker') }}>
{# This is left empty, as this will be filled by Javascript #}
</select>
<button type="submit" class="btn btn-secondary" {% if not is_granted('@parts.edit') %}disabled{% endif %}>{% trans %}part_list.action.submit{% endtrans %}</button>
<button type="submit" class="btn btn-secondary">{% trans %}part_list.action.submit{% endtrans %}</button>
</div>
</div>

View File

@@ -1,6 +1,8 @@
{% extends "base.html.twig" %}
{% block content %}
{% block before_card %}{% endblock %}
<div class="card {% block card_border %}border-primary{% endblock %}">
{% block card_header %}
<div class="card-header {% block card_type %}bg-primary text-white{% endblock %}">
@@ -14,5 +16,7 @@
{% endblock %}
</div>
{% block after_card %}{% endblock %}
{% block additional_content %}{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,48 @@
{% extends "main_card.html.twig" %}
{% block title %}{% trans %}parts.import.title{% endtrans %}{% endblock %}
{% block card_title %}
<i class="fa-solid fa-file-import fa-fw"></i> {% trans %}parts.import.title{% endtrans %}
{% endblock %}
{% block before_card %}
{% if import_errors %}
<div class="alert alert-danger">
<h4><i class="fa-solid fa-exclamation-triangle fa-fw"></i> {% trans %}parts.import.errors.title{% endtrans %}</h4>
<ul>
{% for name, error in import_errors %}
<li>
<b>{{ name }}: </b>
{% for violation in error.violations %}
<i>{{ violation.propertyPath }}</i>: {{ violation.message|trans(violation.parameters, 'validators') }}<br>
{% endfor %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock %}
{% block card_content %}
<p class="text-muted offset-sm-3">
{% trans %}parts.import.help{% endtrans %}<br>
{% trans with {'%link%': 'https://docs.part-db.de/usage/import_export.html'} %}parts.import.help_documentation{% endtrans %}
</p>
{{ form(import_form) }}
{% if imported_entities %}
<hr>
<h4>{% trans %}parts.import.errors.imported_entities{% endtrans %} ({{ imported_entities | length }}):</h4>
<ul>
{% for entity in imported_entities %}
{# @var \App\Entity\Parts\Part entity #}
{% if entity.id %}
<li><a href="{{ entity_url(entity) }}">{{ entity.name }}</a> (ID: {{ entity.iD }})</li>
{% else %}
<li>{{ entity.name }}</li>
{% endif %}
{% endfor %}
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,31 @@
{% extends "main_card.html.twig" %}
{% block title %}{% trans %}project.import_bom{% endtrans %}{% endblock %}
{% block before_card %}
{% if errors %}
<div class="alert alert-danger">
<h4><i class="fa-solid fa-exclamation-triangle fa-fw"></i> {% trans %}parts.import.errors.title{% endtrans %}</h4>
<ul>
{% for violation in errors %}
<li>
<b>{{ violation.propertyPath }}: </b>
{{ violation.message|trans(violation.parameters, 'validators') }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock %}
{% block card_title %}
<i class="fa-solid fa-file-import fa-fw"></i>
{% trans %}project.import_bom{% endtrans %}{% if project %}: <i>{{ project.name }}</i>{% endif %}
{% endblock %}
{% block card_content %}
{{ form(form) }}
{% endblock %}

View File

@@ -4,8 +4,21 @@
{{ datatables.datatable(datatable, 'elements/datatables/datatables', 'projects') }}
<a class="btn btn-success" {% if not is_granted('@projects.edit') %}disabled{% endif %}
href="{{ path('project_add_parts', {"id": project.id, "_redirect": app.request.requestUri}) }}">
<i class="fa-solid fa-square-plus fa-fw"></i>
{% trans %}project.info.bom_add_parts{% endtrans %}
</a>
<div class="btn-group">
<a class="btn btn-success" {% if not is_granted('@projects.edit') %}disabled{% endif %}
href="{{ path('project_add_parts', {"id": project.id, "_redirect": app.request.requestUri}) }}">
<i class="fa-solid fa-square-plus fa-fw"></i>
{% trans %}project.info.bom_add_parts{% endtrans %}
</a>
<button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ path('project_import_bom', {'id': project.id}) }}" {% if not is_granted('edit', project) %}disabled="disabled"{% endif %}>
<i class="fa-solid fa-file-import fa-fw"></i>
{% trans %}project.edit.bom.import_bom{% endtrans %}
</a>
</li>
</ul>
</div>

View File

@@ -8,15 +8,14 @@
{% extends "main_card.html.twig" %}
{% block card_content %}
<ul class="pagination">
<li class="page-item"><a class="link-anchor page-link" href="#A">A</a></li> <li class="page-item"><a class="link-anchor page-link" href="#B">B</a></li> <li class="page-item"><a class="link-anchor page-link" href="#C">C</a></li> <li class="page-item"><a class="link-anchor page-link" href="#D">D</a></li> <li class="page-item"><a class="link-anchor page-link" href="#E">E</a></li> <li class="page-item"><a class="link-anchor page-link" href="#F">F</a></li>
<li class="page-item"><a class="link-anchor page-link" href="#G">G</a></li> <li class="page-item"><a class="link-anchor page-link" href="#H">H</a></li> <li class="page-item"><a class="link-anchor page-link" href="#I">I</a></li> <li class="page-item"><a class="link-anchor page-link" href="#J">J</a></li> <li class="page-item"><a class="link-anchor page-link" href="#K">K</a></li> <li class="page-item"><a class="link-anchor page-link" href="#L">L</a></li>
<li class="page-item"><a class="link-anchor page-link" href="#M">M</a></li> <li class="page-item"><a class="link-anchor page-link" href="#N">N</a></li> <li class="page-item"><a class="link-anchor page-link" href="#O">O</a></li> <li class="page-item"><a class="link-anchor page-link" href="#P">P</a></li> <li class="page-item"><a class="link-anchor page-link" href="#Q">Q</a></li> <li class="page-item"><a class="link-anchor page-link" href="#R">R</a></li>
<li class="page-item"><a class="link-anchor page-link" href="#S">S</a></li> <li class="page-item"><a class="link-anchor page-link" href="#T">T</a></li> <li class="page-item"><a class="link-anchor page-link" href="#U">U</a></li> <li class="page-item"><a class="link-anchor page-link" href="#V">V</a></li> <li class="page-item"><a class="link-anchor page-link" href="#W">W</a></li> <li class="page-item"><a class="link-anchor page-link" href="#X">X</a></li>
<li class="page-item"><a class="link-anchor page-link" href="#Y">Y</a></li> <li class="page-item"><a class="link-anchor page-link" href="#Z">Z</a></li>
{% for letter in "abcdefghijklmnopqrstuvwxyz"|upper|split("") %}
<li class="page-item"><a class="link-anchor page-link" data-turbo="false" href="#{{ letter }}">{{ letter }}</a></li>
{% endfor %}
</ul>
</div>
<table class="table table-striped table-hover">
<table class="table table-striped table-hover tools-ic-logos">
<thead><tr class="bg-info"><th colspan="2"><a id="A">A</a></th></tr></thead>
<tr><td><img src="{{ asset("img/iclogos/acer.png") }}" alt=""></td><td>Acer Integrated Circuit Designs</td></tr>

View File

@@ -94,6 +94,9 @@ class ApplicationAvailabilityFunctionalTest extends WebTestCase
yield ['/part/new'];
yield ['/part/new?category=1&footprint=1&manufacturer=1&storelocation=1&supplier=1'];
//Parts import
yield ['/parts/import'];
//Statistics
yield ['/statistics'];
@@ -134,5 +137,6 @@ class ApplicationAvailabilityFunctionalTest extends WebTestCase
yield ['/project/1/add_parts'];
yield ['/project/1/add_parts?parts=1,2'];
yield ['/project/1/build?n=1'];
yield ['/project/1/import_bom'];
}
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Tests\Serializer;
use App\Serializer\BigNumberNormalizer;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Brick\Math\BigDecimal;
use Brick\Math\BigNumber;
class BigNumberNormalizerTest extends WebTestCase
{
/** @var BigNumberNormalizer */
protected $service;
protected function setUp(): void
{
parent::setUp();
//Get an service instance.
self::bootKernel();
$this->service = self::getContainer()->get(BigNumberNormalizer::class);
}
public function testNormalize()
{
$bigDecimal = BigDecimal::of('1.23456789');
$this->assertSame('1.23456789', $this->service->normalize($bigDecimal));
}
public function testSupportsNormalization()
{
//Normalizer must only support BigNumber objects (and child classes)
$this->assertFalse($this->service->supportsNormalization(new \stdClass()));
$bigNumber = BigNumber::of(1);
$this->assertTrue($this->service->supportsNormalization($bigNumber));
$bigDecimal = BigDecimal::of(1);
$this->assertTrue($this->service->supportsNormalization($bigDecimal));
}
}

View File

@@ -0,0 +1,132 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Tests\Serializer;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
use App\Serializer\PartNormalizer;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class PartNormalizerTest extends WebTestCase
{
/** @var PartNormalizer */
protected $service;
protected function setUp(): void
{
parent::setUp();
//Get an service instance.
self::bootKernel();
$this->service = self::getContainer()->get(PartNormalizer::class);
}
public function testSupportsNormalization()
{
//Normalizer must only support Part objects (and child classes)
$this->assertFalse($this->service->supportsNormalization(new \stdClass()));
$this->assertTrue($this->service->supportsNormalization(new Part()));
}
public function testNormalize()
{
$part = new Part();
$part->setName('Test Part');
$partLot1 = new PartLot();
$partLot1->setAmount(1);
$partLot2 = new PartLot();
$partLot2->setAmount(5);
$part->addPartLot($partLot1);
$part->addPartLot($partLot2);
$data = $this->service->normalize($part, 'json', ['groups' => ['simple']]);
$this->assertSame('Test Part', $data['name']);
$this->assertSame(6.0, $data['total_instock']);
$this->assertSame('part', $data['type']);
//Check that type field is not present in CSV export
$data = $this->service->normalize($part, 'csv', ['groups' => ['simple']]);
$this->assertSame('Test Part', $data['name']);
$this->assertArrayNotHasKey('type', $data);
}
public function testSupportsDenormalization()
{
//Normalizer must only support Part type with array as input
$this->assertFalse($this->service->supportsDenormalization(new \stdClass(), Part::class));
$this->assertFalse($this->service->supportsDenormalization('string', Part::class));
$this->assertFalse($this->service->supportsDenormalization(['a' => 'b'], \stdClass::class));
$this->assertTrue($this->service->supportsDenormalization(['a' => 'b'], Part::class));
}
public function testDenormalize()
{
$input = [
'name' => 'Test Part',
'description' => 'Test Description',
'notes' => 'Test Note', //Test key normalization
'ipn' => 'Test IPN',
'mpn' => 'Test MPN',
'instock' => '5',
'storage_location' => 'Test Storage Location',
'supplier' => 'Test Supplier',
'price' => '5.5',
'supplier_part_number' => 'TEST123'
];
$part = $this->service->denormalize($input, Part::class, 'json', ['groups' => ['import'], 'create_unknown_datastructures' => true]);
$this->assertInstanceOf(Part::class, $part);
$this->assertSame('Test Part', $part->getName());
$this->assertSame('Test Description', $part->getDescription());
$this->assertSame('Test Note', $part->getComment());
$this->assertSame('Test IPN', $part->getIpn());
$this->assertSame('Test MPN', $part->getManufacturerProductNumber());
//Check that a new PartLot was created
$this->assertCount(1, $part->getPartLots());
/** @var PartLot $partLot */
$partLot = $part->getPartLots()->first();
$this->assertSame(5.0, $partLot->getAmount());
$this->assertNotNull($partLot->getStorageLocation());
$this->assertSame('Test Storage Location', $partLot->getStorageLocation()->getName());
//Check that a new orderdetail was created
$this->assertCount(1, $part->getOrderdetails());
/** @var Orderdetail $orderDetail */
$orderDetail = $part->getOrderdetails()->first();
$this->assertNotNull($orderDetail->getSupplier());
$this->assertSame('Test Supplier', $orderDetail->getSupplier()->getName());
$this->assertSame('TEST123', $orderDetail->getSupplierPartNr());
//Check that a pricedetail was created
$this->assertCount(1, $orderDetail->getPricedetails());
/** @var Pricedetail $priceDetail */
$priceDetail = $orderDetail->getPricedetails()->first();
$this->assertSame("5.50000", (string) $priceDetail->getPrice());
//Must be in base currency
$this->assertNull($priceDetail->getCurrency());
//Must be for 1 part and 1 minimum order quantity
$this->assertSame(1.0, $priceDetail->getPriceRelatedQuantity());
$this->assertSame(1.0, $priceDetail->getMinDiscountQuantity());
}
}

View File

@@ -0,0 +1,123 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Tests\Serializer;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parts\Category;
use App\Serializer\StructuralElementFromNameDenormalizer;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class StructuralElementFromNameDenormalizerTest extends WebTestCase
{
/** @var StructuralElementFromNameDenormalizer */
protected $service;
protected function setUp(): void
{
parent::setUp();
//Get an service instance.
self::bootKernel();
$this->service = self::getContainer()->get(StructuralElementFromNameDenormalizer::class);
}
public function testSupportsDenormalization(): void
{
//Only the combination of string data and StructuralElement class as type is supported.
$this->assertFalse($this->service->supportsDenormalization('doesnt_matter', \stdClass::class));
$this->assertFalse($this->service->supportsDenormalization(['a' => 'b'], Category::class));
$this->assertTrue($this->service->supportsDenormalization('doesnt_matter', Category::class));
}
public function testDenormalizeCreateNew(): void
{
$context = [
'groups' => ['simple'],
'path_delimiter' => '->',
'create_unknown_datastructures' => true,
];
//Test for simple category
$category = $this->service->denormalize('New Category', Category::class, null, $context);
$this->assertInstanceOf(Category::class, $category);
$this->assertSame('New Category', $category->getName());
//Test for nested category
$category = $this->service->denormalize('New Category->Sub Category', Category::class, null, $context);
$this->assertInstanceOf(Category::class, $category);
$this->assertSame('Sub Category', $category->getName());
$this->assertInstanceOf(Category::class, $category->getParent());
$this->assertSame('New Category', $category->getParent()->getName());
//Test with existing category
$category = $this->service->denormalize('Node 1->Node 1.1', Category::class, null, $context);
$this->assertInstanceOf(Category::class, $category);
$this->assertSame('Node 1.1', $category->getName());
$this->assertInstanceOf(Category::class, $category->getParent());
$this->assertSame('Node 1', $category->getParent()->getName());
//Both categories should be in DB (have an ID)
$this->assertNotNull($category->getID());
$this->assertNotNull($category->getParent()->getID());
//Test with other path_delimiter
$context['path_delimiter'] = '/';
$category = $this->service->denormalize('New Category/Sub Category', Category::class, null, $context);
$this->assertInstanceOf(Category::class, $category);
$this->assertSame('Sub Category', $category->getName());
$this->assertInstanceOf(Category::class, $category->getParent());
$this->assertSame('New Category', $category->getParent()->getName());
//Test with empty path
$category = $this->service->denormalize('', Category::class, null, $context);
$this->assertNull($category);
}
public function testDenormalizeOnlyExisting(): void
{
$context = [
'groups' => ['simple'],
'path_delimiter' => '->',
'create_unknown_datastructures' => false,
];
//Test with existing category
$category = $this->service->denormalize('Node 1->Node 1.1', Category::class, null, $context);
$this->assertInstanceOf(Category::class, $category);
$this->assertSame('Node 1.1', $category->getName());
$this->assertInstanceOf(Category::class, $category->getParent());
$this->assertSame('Node 1', $category->getParent()->getName());
//Both categories should be in DB (have an ID)
$this->assertNotNull($category->getID());
$this->assertNotNull($category->getParent()->getID());
//Test with non existing category
$category = $this->service->denormalize('New category', Category::class, null, $context);
$this->assertNull($category);
//Test with empty path
$category = $this->service->denormalize('', Category::class, null, $context);
$this->assertNull($category);
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Tests\Serializer;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Part;
use App\Serializer\BigNumberNormalizer;
use App\Serializer\StructuralElementNormalizer;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class StructuralElementNormalizerTest extends WebTestCase
{
/** @var StructuralElementNormalizer */
protected $service;
protected function setUp(): void
{
parent::setUp();
//Get an service instance.
self::bootKernel();
$this->service = self::getContainer()->get(StructuralElementNormalizer::class);
}
public function testNormalize()
{
$category1 = (new Category())->setName('Category 1');
$category11 = (new Category())->setName('Category 1.1');
$category11->setParent($category1);
//Serialize category 1
$data1 = $this->service->normalize($category1, 'json', ['groups' => ['simple']]);
$this->assertArrayHasKey('full_name', $data1);
$this->assertSame('Category 1', $data1['full_name']);
//Json export must contain type attribute
$this->assertArrayHasKey('type', $data1);
//Serialize category 1.1
$data11 = $this->service->normalize($category11, 'json', ['groups' => ['simple']]);
$this->assertArrayHasKey('full_name', $data11);
$this->assertSame('Category 1->Category 1.1', $data11['full_name']);
//Test that type attribute is removed for CSV export
$data11 = $this->service->normalize($category11, 'csv', ['groups' => ['simple']]);
$this->assertArrayNotHasKey('type', $data11);
}
public function testSupportsNormalization()
{
//Normalizer must only support StructuralElement objects (and child classes)
$this->assertFalse($this->service->supportsNormalization(new \stdClass()));
$this->assertFalse($this->service->supportsNormalization(new Part()));
$this->assertTrue($this->service->supportsNormalization(new Category()));
$this->assertTrue($this->service->supportsNormalization(new Footprint()));
}
}

View File

@@ -0,0 +1,123 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Tests\Services\ImportExportSystem;
use App\Entity\ProjectSystem\Project;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Services\ImportExportSystem\BOMImporter;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\File\File;
class BOMImporterTest extends WebTestCase
{
/**
* @var BOMImporter
*/
protected $service;
protected function setUp(): void
{
parent::setUp();
//Get an service instance.
self::bootKernel();
$this->service = self::getContainer()->get(BOMImporter::class);
}
public function testImportFileIntoProject(): void
{
$inpute = $input = <<<CSV
"ID";"Bezeichner";"Footprint";"Stückzahl";"Bezeichnung";"Anbieter und Referenz";
1;"R19,R17";"R_0805_2012Metric_Pad1.20x1.40mm_HandSolder";2;"4.7k";Test;;
2;"D1";"D_DO-41_SOD81_P10.16mm_Horizontal";1;"1N5059";;;
3;"J3,J5";"JST_XH_B5B-XH-AM_1x05_P2.50mm_Vertical";2;"DISPLAY";;;
4;"C6";"CP_Radial_D6.3mm_P2.50mm";1;"47uF";;;
CSV;
$file = $this->createMock(File::class);
$file->method('getContent')->willReturn($input);
$project = new Project();
$this->assertCount(0, $project->getBOMEntries());
$bom_entries = $this->service->importFileIntoProject($file, $project, ['type' => 'kicad_pcbnew']);
$this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries);
$this->assertCount(4, $bom_entries);
//Check that the BOM entries are added to the project
$this->assertCount(4, $project->getBOMEntries());
}
public function testStringToBOMEntriesKiCADPCB(): void
{
//Test for german input
$input = <<<CSV
"ID";"Bezeichner";"Footprint";"Stückzahl";"Bezeichnung";"Anbieter und Referenz";
1;"R19,R17";"R_0805_2012Metric_Pad1.20x1.40mm_HandSolder";2;"4.7k";Test;;
2;"D1";"D_DO-41_SOD81_P10.16mm_Horizontal";1;"1N5059";;;
3;"J3,J5";"JST_XH_B5B-XH-AM_1x05_P2.50mm_Vertical";2;"DISPLAY";;;
4;"C6";"CP_Radial_D6.3mm_P2.50mm";1;"47uF";;;
CSV;
$bom = $this->service->stringToBOMEntries($input, ['type' => 'kicad_pcbnew']);
$this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom);
$this->assertCount(4, $bom);
$this->assertEquals('R19,R17', $bom[0]->getMountnames());
$this->assertEquals(2.0, $bom[0]->getQuantity());
$this->assertSame('4.7k (R_0805_2012Metric_Pad1.20x1.40mm_HandSolder)', $bom[0]->getName());
$this->assertSame('Test', $bom[0]->getComment());
//Test for english input
$input = <<<CSV
"Id";"Designator";"Package";"Quantity";"Designation";"Supplier and ref";
1;"R19,R17";"R_0805_2012Metric_Pad1.20x1.40mm_HandSolder";2;"4.7k";Test;;
2;"D1";"D_DO-41_SOD81_P10.16mm_Horizontal";1;"1N5059";;;
3;"J3,J5";"JST_XH_B5B-XH-AM_1x05_P2.50mm_Vertical";2;"DISPLAY";;;
4;"C6";"CP_Radial_D6.3mm_P2.50mm";1;"47uF";;;
CSV;
$bom = $this->service->stringToBOMEntries($input, ['type' => 'kicad_pcbnew']);
$this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom);
$this->assertCount(4, $bom);
$this->assertEquals('R19,R17', $bom[0]->getMountnames());
$this->assertEquals(2.0, $bom[0]->getQuantity());
$this->assertSame('4.7k (R_0805_2012Metric_Pad1.20x1.40mm_HandSolder)', $bom[0]->getName());
$this->assertSame('Test', $bom[0]->getComment());
}
public function testStringToBOMEntriesKiCADPCBError(): void
{
$input = <<<CSV
"ID";"Test";
1;"R19,R17";"R_0805_2012Metric_Pad1.20x1.40mm_HandSolder";2;"4.7k";Test;;
CSV;
$this->expectException(\UnexpectedValueException::class);
$this->service->stringToBOMEntries($input, ['type' => 'kicad_pcbnew']);
}
}

View File

@@ -0,0 +1,81 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Tests\Services\ImportExportSystem;
use App\Entity\Parts\Category;
use App\Services\ImportExportSystem\EntityExporter;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;
class EntityExporterTest extends WebTestCase
{
/**
* @var EntityExporter
*/
protected $service;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->service = self::getContainer()->get(EntityExporter::class);
}
private function getEntities(): array
{
$entity1 = (new Category())->setName('Enitity 1')->setComment('Test');
$entity1_1 = (new Category())->setName('Enitity 1.1')->setParent($entity1);
$entity2 = (new Category())->setName('Enitity 2');
return [$entity1, $entity1_1, $entity2];
}
public function testExportStructuralEntities(): void
{
$entities = $this->getEntities();
$json_without_children = $this->service->exportEntities($entities, ['format' => 'json', 'level' => 'simple']);
$this->assertJsonStringEqualsJsonString('[{"name":"Enitity 1","type":"category","full_name":"Enitity 1"},{"name":"Enitity 1.1","type":"category","full_name":"Enitity 1->Enitity 1.1"},{"name":"Enitity 2","type":"category","full_name":"Enitity 2"}]',
$json_without_children);
$json_with_children = $this->service->exportEntities($entities,
['format' => 'json', 'level' => 'simple', 'include_children' => true]);
$this->assertJsonStringEqualsJsonString('[{"children":[{"children":[],"name":"Enitity 1.1","type":"category","full_name":"Enitity 1->Enitity 1.1"}],"name":"Enitity 1","type":"category","full_name":"Enitity 1"},{"children":[],"name":"Enitity 1.1","type":"category","full_name":"Enitity 1->Enitity 1.1"},{"children":[],"name":"Enitity 2","type":"category","full_name":"Enitity 2"}]',
$json_with_children);
}
public function testExportEntityFromRequest(): void
{
$entities = $this->getEntities();
$request = new Request();
$request->request->set('format', 'json');
$request->request->set('level', 'simple');
$response = $this->service->exportEntityFromRequest($entities, $request);
$this->assertJson($response->getContent());
$this->assertSame('application/json', $response->headers->get('Content-Type'));
$this->assertNotEmpty($response->headers->get('Content-Disposition'));
}
}

View File

@@ -23,10 +23,13 @@ declare(strict_types=1);
namespace App\Tests\Services\ImportExportSystem;
use App\Entity\Attachments\AttachmentType;
use App\Entity\Parts\Category;
use App\Entity\Parts\Part;
use App\Entity\UserSystem\User;
use App\Services\Formatters\AmountFormatter;
use App\Services\ImportExportSystem\EntityImporter;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Validator\ConstraintViolation;
/**
* @group DB
@@ -34,7 +37,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class EntityImporterTest extends WebTestCase
{
/**
* @var AmountFormatter
* @var EntityImporter
*/
protected $service;
@@ -151,4 +154,95 @@ EOT;
$this->assertCount(2, $errors);
$this->assertSame('Node 1', $errors[0]['entity']->getName());
}
public function formatDataProvider(): array
{
return [
['csv', 'csv'],
['csv', 'CSV'],
['xml', 'Xml'],
['json', 'json'],
['yaml', 'yml'],
['yaml', 'YAML'],
];
}
/**
* @dataProvider formatDataProvider
*/
public function testDetermineFormat(string $expected, string $extension): void
{
$this->assertSame($expected, $this->service->determineFormat($extension));
}
public function testImportStringParts(): void
{
$input = <<<EOT
name,description,notes,manufacturer
Test 1,Test 1 description,Test 1 notes,Test 1 manufacturer
Test 2,Test 2 description,Test 2 notes,Test 2 manufacturer
EOT;
$category = new Category();
$category->setName('Test category');
$errors = [];
$results = $this->service->importString($input, [
'class' => Part::class,
'format' => 'csv',
'csv_delimiter' => ',',
'create_unknown_datastructures' => true,
'part_category' => $category,
], $errors);
$this->assertCount(2, $results);
//No errors must be present
$this->assertEmpty($errors);
$this->assertContainsOnlyInstancesOf(Part::class, $results);
$this->assertSame('Test 1', $results[0]->getName());
$this->assertSame('Test 1 description', $results[0]->getDescription());
$this->assertSame('Test 1 notes', $results[0]->getComment());
$this->assertSame('Test 1 manufacturer', $results[0]->getManufacturer()->getName());
$this->assertSame($category, $results[0]->getCategory());
$this->assertSame('Test 2', $results[1]->getName());
$this->assertSame('Test 2 description', $results[1]->getDescription());
$this->assertSame('Test 2 notes', $results[1]->getComment());
$this->assertSame('Test 2 manufacturer', $results[1]->getManufacturer()->getName());
$this->assertSame($category, $results[1]->getCategory());
$input = <<<EOT
[{"name":"Test 1","description":"Test 1 description","notes":"Test 1 notes","manufacturer":"Test 1 manufacturer", "tags": "test,test2"},{"name":"Test 2","description":"Test 2 description","notes":"Test 2 notes","manufacturer":"Test 2 manufacturer", "manufacturing_status": "invalid"}]
EOT;
$errors = [];
$results = $this->service->importString($input, [
'class' => Part::class,
'format' => 'json',
'create_unknown_datastructures' => true,
'part_category' => $category,
], $errors);
//We have 2 elements, but one is invalid
$this->assertCount(1, $results);
$this->assertCount(1, $errors);
$this->assertContainsOnlyInstancesOf(Part::class, $results);
//Check the format of the error
$error = reset($errors);
$this->assertInstanceOf(Part::class, $error['entity']);
$this->assertSame('Test 2', $error['entity']->getName());
$this->assertContainsOnlyInstancesOf(ConstraintViolation::class, $error['violations']);
//Element name must be element name
$this->assertArrayHasKey('Test 2', $errors);
//Check the valid element
$this->assertSame('Test 1', $results[0]->getName());
$this->assertSame('Test 1 description', $results[0]->getDescription());
$this->assertSame('Test 1 notes', $results[0]->getComment());
$this->assertSame('Test 1 manufacturer', $results[0]->getManufacturer()->getName());
$this->assertSame($category, $results[0]->getCategory());
$this->assertSame('test,test2', $results[0]->getTags());
}
}

View File

@@ -240,6 +240,36 @@ class PermissionManagerTest extends WebTestCase
$this->assertNull($this->service->dontInherit($user, 'parts', 'edit'));
}
public function testSetAllOperationsOfPermissionExcept(): void
{
$user = new User();
//Set all operations of permission to true (except import and delete)
$this->service->setAllOperationsOfPermissionExcept($user, 'parts', true, ['import', 'delete']);
$this->assertTrue($this->service->dontInherit($user, 'parts', 'read'));
$this->assertTrue($this->service->dontInherit($user, 'parts', 'create'));
$this->assertTrue($this->service->dontInherit($user, 'parts', 'edit'));
$this->assertNull($this->service->dontInherit($user, 'parts', 'import'));
$this->assertNull($this->service->dontInherit($user, 'parts', 'delete'));
//Set all operations of permission to false
$this->service->setAllOperationsOfPermissionExcept($user, 'parts', false, ['import', 'delete']);
$this->assertFalse($this->service->dontInherit($user, 'parts', 'read'));
$this->assertFalse($this->service->dontInherit($user, 'parts', 'create'));
$this->assertFalse($this->service->dontInherit($user, 'parts', 'edit'));
$this->assertNull($this->service->dontInherit($user, 'parts', 'import'));
$this->assertNull($this->service->dontInherit($user, 'parts', 'delete'));
//Set all operations of permission to null
$this->service->setAllOperationsOfPermissionExcept($user, 'parts', null, ['import', 'delete']);
$this->assertNull($this->service->dontInherit($user, 'parts', 'read'));
$this->assertNull($this->service->dontInherit($user, 'parts', 'create'));
$this->assertNull($this->service->dontInherit($user, 'parts', 'edit'));
$this->assertNull($this->service->dontInherit($user, 'parts', 'import'));
$this->assertNull($this->service->dontInherit($user, 'parts', 'delete'));
}
public function testEnsureCorrectSetOperations(): void
{
//Create an empty user (all permissions are inherit)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6607,16 +6607,6 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia
<target>Le parent ne peut pas être un de ses propres enfants.</target>
</segment>
</unit>
<unit id="0IF0VIF" name="validator.isSelectable">
<notes>
<note priority="1">obsolete</note>
<note category="state" priority="1">obsolete</note>
</notes>
<segment state="translated">
<source>validator.isSelectable</source>
<target>L'élément doit être sélectionnable !</target>
</segment>
</unit>
<unit id="nd207H6" name="validator.part_lot.location_full.no_increasment">
<notes>
<note priority="1">obsolete</note>

View File

@@ -6607,16 +6607,6 @@
<target>要素は自身の子とすることはできません。</target>
</segment>
</unit>
<unit id="0IF0VIF" name="validator.isSelectable">
<notes>
<note priority="1">obsolete</note>
<note category="state" priority="1">obsolete</note>
</notes>
<segment state="translated">
<source>validator.isSelectable</source>
<target>要素は選択可能である必要があります。</target>
</segment>
</unit>
<unit id="nd207H6" name="validator.part_lot.location_full.no_increasment">
<notes>
<note priority="1">obsolete</note>

View File

@@ -6608,16 +6608,6 @@
<target>Родитель не может быть дочерним по отношению к себе</target>
</segment>
</unit>
<unit id="0IF0VIF" name="validator.isSelectable">
<notes>
<note priority="1">obsolete</note>
<note category="state" priority="1">obsolete</note>
</notes>
<segment state="translated">
<source>validator.isSelectable</source>
<target>Элемент должен быть выбираемым!</target>
</segment>
</unit>
<unit id="nd207H6" name="validator.part_lot.location_full.no_increasment">
<notes>
<note priority="1">obsolete</note>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="de">
<file id="security.de">
<file id="security.en">
<unit id="aazoCks" name="user.login_error.user_disabled">
<segment>
<segment state="translated">
<source>user.login_error.user_disabled</source>
<target>Ihr Account ist deaktiviert! Kontaktiere einen Administrator, wenn Sie denken, dass dies ein Fehler ist.</target>
</segment>

View File

@@ -2,13 +2,13 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="en">
<file id="security.en">
<unit id="aazoCks" name="user.login_error.user_disabled">
<segment>
<segment state="translated">
<source>user.login_error.user_disabled</source>
<target>Your account is disabled! Contact an administrator if you think this is wrong.</target>
</segment>
</unit>
<unit id="Dpb9AmY" name="saml.error.cannot_login_local_user_per_saml">
<segment state="translated">
<segment>
<source>saml.error.cannot_login_local_user_per_saml</source>
<target>You can not login as local user via SSO! Use your local user password instead.</target>
</segment>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="de">
<file id="validators.de">
<file id="validators.en">
<unit id="xevSdCK" name="part.master_attachment.must_be_picture">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentContainingDBElement.php:0</note>
@@ -37,7 +37,7 @@
<note priority="1">Part-DB1\src\Entity\UserSystem\Group.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>part.master_attachment.must_be_picture</source>
<target>Der Vorschauanhang muss ein gültiges Bild sein!</target>
</segment>
@@ -82,7 +82,7 @@
<note priority="1">src\Entity\StructuralDBElement.php:0</note>
<note priority="1">src\Entity\Supplier.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>structural.entity.unique_name</source>
<target>Es kann auf jeder Ebene nur ein Objekt mit dem gleichem Namen geben!</target>
</segment>
@@ -102,7 +102,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>parameters.validator.min_lesser_typical</source>
<target>Wert muss kleiner oder gleich als der typische Wert sein ({{ compared_value }}).</target>
</segment>
@@ -122,7 +122,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>parameters.validator.min_lesser_max</source>
<target>Wert muss kleiner als der Maximalwert sein ({{ compared_value }}).</target>
</segment>
@@ -142,7 +142,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>parameters.validator.max_greater_typical</source>
<target>Wert muss größer oder gleich dem typischen Wert sein ({{ compared_value }}).</target>
</segment>
@@ -152,7 +152,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>validator.user.username_already_used</source>
<target>Es existiert bereits ein Benutzer mit diesem Namen.</target>
</segment>
@@ -162,7 +162,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>user.invalid_username</source>
<target>Der Benutzername darf nur Buchstaben, Zahlen, Unterstriche, Punkte, Plus- oder Minuszeichen enthalten.</target>
</segment>
@@ -171,7 +171,7 @@
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment>
<segment state="translated">
<source>validator.noneofitschild.self</source>
<target>Ein Element kann nicht sein eigenenes übergeordnetes Element sein!</target>
</segment>
@@ -180,121 +180,121 @@
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment>
<segment state="final">
<source>validator.noneofitschild.children</source>
<target>Ein Kindelement kann nicht das übergeordnete Element sein!</target>
</segment>
</unit>
<unit id="ayNr6QK" name="validator.select_valid_category">
<segment>
<segment state="translated">
<source>validator.select_valid_category</source>
<target>Bitte wählen Sie eine gültige Kategorie.</target>
</segment>
</unit>
<unit id="6vIlN5q" name="validator.part_lot.only_existing">
<segment>
<segment state="translated">
<source>validator.part_lot.only_existing</source>
<target>Der Lagerort wurde als "nur bestehende Teile" markiert, daher können keine neuen Teile hinzugefügt werden.</target>
</segment>
</unit>
<unit id="3xoKOIS" name="validator.part_lot.location_full.no_increase">
<segment>
<segment state="translated">
<source>validator.part_lot.location_full.no_increase</source>
<target>Lagerort ist voll. Bestand kann nicht erhöht werden (neuer Wert muss kleiner sein als {{old_amount}}).</target>
</segment>
</unit>
<unit id="R6Ov4Yt" name="validator.part_lot.location_full">
<segment>
<segment state="final">
<source>validator.part_lot.location_full</source>
<target>Der Lagerort ist voll, daher können keine neue Teile hinzugefügt werden.</target>
</segment>
</unit>
<unit id="BNQk2e7" name="validator.part_lot.single_part">
<segment>
<segment state="final">
<source>validator.part_lot.single_part</source>
<target>Der Lagerort wurde als "Nur ein Bauteil" markiert, daher kann kein neues Bauteil hinzugefügt werden.</target>
</segment>
</unit>
<unit id="4gPskOG" name="validator.attachment.must_not_be_null">
<segment>
<segment state="translated">
<source>validator.attachment.must_not_be_null</source>
<target>Sie müssen ein Dateitypen auswählen!</target>
</segment>
</unit>
<unit id="cDDVrWT" name="validator.orderdetail.supplier_must_not_be_null">
<segment>
<segment state="translated">
<source>validator.orderdetail.supplier_must_not_be_null</source>
<target>Sie müssen einen Lieferanten auswählen!</target>
</segment>
</unit>
<unit id="k5DDdB4" name="validator.measurement_unit.use_si_prefix_needs_unit">
<segment>
<segment state="translated">
<source>validator.measurement_unit.use_si_prefix_needs_unit</source>
<target>Um SI-Prefixe zu aktivieren, müssen Sie einen Einheitensymbol setzen!</target>
</segment>
</unit>
<unit id="DuzIOCr" name="part.ipn.must_be_unique">
<segment>
<segment state="translated">
<source>part.ipn.must_be_unique</source>
<target>Die Internal Part Number (IPN) muss einzigartig sein. Der Wert {{value}} wird bereits benutzt!</target>
</segment>
</unit>
<unit id="Z4Kuuo2" name="validator.project.bom_entry.name_or_part_needed">
<segment>
<segment state="translated">
<source>validator.project.bom_entry.name_or_part_needed</source>
<target>Sie müssen ein Bauteil auswählen, oder einen Namen für ein nicht-Bauteil BOM-Eintrag setzen!</target>
</segment>
</unit>
<unit id="WF_v4ih" name="project.bom_entry.name_already_in_bom">
<segment>
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>
<target>Es gibt bereits einen BOM Eintrag mit diesem Namen!</target>
</segment>
</unit>
<unit id="5v4p85H" name="project.bom_entry.part_already_in_bom">
<segment>
<segment state="translated">
<source>project.bom_entry.part_already_in_bom</source>
<target>Dieses Bauteil existiert bereits in der BOM!</target>
</segment>
</unit>
<unit id="3lM32Tw" name="project.bom_entry.mountnames_quantity_mismatch">
<segment>
<segment state="translated">
<source>project.bom_entry.mountnames_quantity_mismatch</source>
<target>Die Anzahl der Bestückungsnamen muss mit der Menge der zu bestückenden Bauteile übereinstimmen!</target>
</segment>
</unit>
<unit id="x47D5WT" name="project.bom_entry.can_not_add_own_builds_part">
<segment>
<segment state="translated">
<source>project.bom_entry.can_not_add_own_builds_part</source>
<target>Die BOM eines Projektes kann nicht das eigene Produktionsbauteil enthalten!</target>
</segment>
</unit>
<unit id="2x2XDI_" name="project.bom_has_to_include_all_subelement_parts">
<segment>
<segment state="translated">
<source>project.bom_has_to_include_all_subelement_parts</source>
<target>Die Projekt-BOM muss alle Produktionsbauteile der Unterprojekte enthalten. Bauteil %part_name% des Projektes %project_name% fehlt!</target>
</segment>
</unit>
<unit id="U9b1EzD" name="project.bom_entry.price_not_allowed_on_parts">
<segment>
<segment state="translated">
<source>project.bom_entry.price_not_allowed_on_parts</source>
<target>Sie können keinen Preis für Bauteil-BOM-Einträge definieren. Definieren Sie die Preise stattdessen auf dem Bauteil.</target>
</segment>
</unit>
<unit id="ID056SR" name="validator.project_build.lot_bigger_than_needed">
<segment>
<segment state="translated">
<source>validator.project_build.lot_bigger_than_needed</source>
<target>Sie haben mehr zur Entnahme ausgewählt als notwendig. Entfernen Sie die überflüssige Anzahl.</target>
</segment>
</unit>
<unit id="6hV5UqD" name="validator.project_build.lot_smaller_than_needed">
<segment>
<segment state="translated">
<source>validator.project_build.lot_smaller_than_needed</source>
<target>Sie haben weniger zur Entnahme ausgewählt, als zum Bau notwendig ist! Fügen Sie mehr hinzu.</target>
</segment>
</unit>
<unit id="G9ZKt.4" name="part.name.must_match_category_regex">
<segment>
<segment state="translated">
<source>part.name.must_match_category_regex</source>
<target>Der Bauteilename entspricht nicht dem regulären Ausdruck, der von der Kategorie vorgegeben wurde: %regex%</target>
</segment>
@@ -305,5 +305,25 @@
<target>Wählen Sie einen Wert, oder laden Sie eine Datei hoch, um dessen Dateiname automatisch als Namen für diesen Anhang zu nutzen.</target>
</segment>
</unit>
<unit id="0IF0VIF" name="validator.isSelectable">
<notes>
<note priority="1">obsolete</note>
<note category="state" priority="1">obsolete</note>
</notes>
<segment>
<source>validator.isSelectable</source>
<target>Das Element muss auswählbar sein!</target>
</segment>
</unit>
<unit id="0IF0VIF" name="validator.isSelectable">
<notes>
<note priority="1">obsolete</note>
<note category="state" priority="1">obsolete</note>
</notes>
<segment>
<source>validator.isSelectable</source>
<target>The element must be selectable.</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -37,7 +37,7 @@
<note priority="1">Part-DB1\src\Entity\UserSystem\Group.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>part.master_attachment.must_be_picture</source>
<target>The preview attachment must be a valid picture!</target>
</segment>
@@ -82,7 +82,7 @@
<note priority="1">src\Entity\StructuralDBElement.php:0</note>
<note priority="1">src\Entity\Supplier.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>structural.entity.unique_name</source>
<target>An element with this name already exists on this level!</target>
</segment>
@@ -102,7 +102,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>parameters.validator.min_lesser_typical</source>
<target>Value must be lesser or equal the the typical value ({{ compared_value }}).</target>
</segment>
@@ -122,7 +122,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>parameters.validator.min_lesser_max</source>
<target>Value must be lesser than the maximum value ({{ compared_value }}).</target>
</segment>
@@ -142,7 +142,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>parameters.validator.max_greater_typical</source>
<target>Value must be greater or equal than the typical value ({{ compared_value }}).</target>
</segment>
@@ -152,7 +152,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>validator.user.username_already_used</source>
<target>A user with this name is already exisiting</target>
</segment>
@@ -162,7 +162,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>user.invalid_username</source>
<target>The username must contain only letters, numbers, underscores, dots, pluses or minuses!</target>
</segment>
@@ -171,7 +171,7 @@
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment>
<segment state="translated">
<source>validator.noneofitschild.self</source>
<target>An element can not be its own parent!</target>
</segment>
@@ -180,127 +180,127 @@
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment>
<segment state="translated">
<source>validator.noneofitschild.children</source>
<target>You can not assign children element as parent (This would cause loops)!</target>
</segment>
</unit>
<unit id="ayNr6QK" name="validator.select_valid_category">
<segment>
<segment state="translated">
<source>validator.select_valid_category</source>
<target>Please select a valid category!</target>
</segment>
</unit>
<unit id="6vIlN5q" name="validator.part_lot.only_existing">
<segment>
<segment state="translated">
<source>validator.part_lot.only_existing</source>
<target>Can not add new parts to this location as it is marked as "Only Existing"</target>
</segment>
</unit>
<unit id="3xoKOIS" name="validator.part_lot.location_full.no_increase">
<segment>
<segment state="translated">
<source>validator.part_lot.location_full.no_increase</source>
<target>Location is full. Amount can not be increased (new value must be smaller than {{ old_amount }}).</target>
</segment>
</unit>
<unit id="R6Ov4Yt" name="validator.part_lot.location_full">
<segment>
<segment state="translated">
<source>validator.part_lot.location_full</source>
<target>Location is full. Can not add new parts to it.</target>
</segment>
</unit>
<unit id="BNQk2e7" name="validator.part_lot.single_part">
<segment>
<segment state="translated">
<source>validator.part_lot.single_part</source>
<target>This location can only contain a single part and it is already full!</target>
</segment>
</unit>
<unit id="4gPskOG" name="validator.attachment.must_not_be_null">
<segment>
<segment state="translated">
<source>validator.attachment.must_not_be_null</source>
<target>You must select an attachment type!</target>
</segment>
</unit>
<unit id="cDDVrWT" name="validator.orderdetail.supplier_must_not_be_null">
<segment>
<segment state="translated">
<source>validator.orderdetail.supplier_must_not_be_null</source>
<target>You must select an supplier!</target>
</segment>
</unit>
<unit id="k5DDdB4" name="validator.measurement_unit.use_si_prefix_needs_unit">
<segment>
<segment state="translated">
<source>validator.measurement_unit.use_si_prefix_needs_unit</source>
<target>To enable SI prefixes, you have to set a unit symbol!</target>
</segment>
</unit>
<unit id="DuzIOCr" name="part.ipn.must_be_unique">
<segment>
<segment state="translated">
<source>part.ipn.must_be_unique</source>
<target>The internal part number must be unique. {{ value }} is already in use!</target>
</segment>
</unit>
<unit id="Z4Kuuo2" name="validator.project.bom_entry.name_or_part_needed">
<segment>
<segment state="translated">
<source>validator.project.bom_entry.name_or_part_needed</source>
<target>You have to choose a part for a part BOM entry or set a name for a non-part BOM entry.</target>
</segment>
</unit>
<unit id="WF_v4ih" name="project.bom_entry.name_already_in_bom">
<segment>
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>
<target>There is already an BOM entry with this name!</target>
</segment>
</unit>
<unit id="5v4p85H" name="project.bom_entry.part_already_in_bom">
<segment>
<segment state="translated">
<source>project.bom_entry.part_already_in_bom</source>
<target>This part already exists in the BOM!</target>
</segment>
</unit>
<unit id="3lM32Tw" name="project.bom_entry.mountnames_quantity_mismatch">
<segment>
<segment state="translated">
<source>project.bom_entry.mountnames_quantity_mismatch</source>
<target>The number of mountnames has to match the BOMs quantity!</target>
</segment>
</unit>
<unit id="x47D5WT" name="project.bom_entry.can_not_add_own_builds_part">
<segment>
<segment state="translated">
<source>project.bom_entry.can_not_add_own_builds_part</source>
<target>You can not add a project's own builds part to the BOM.</target>
</segment>
</unit>
<unit id="2x2XDI_" name="project.bom_has_to_include_all_subelement_parts">
<segment>
<segment state="translated">
<source>project.bom_has_to_include_all_subelement_parts</source>
<target>The project BOM has to include all subprojects builds parts. Part %part_name% of project %project_name% missing!</target>
</segment>
</unit>
<unit id="U9b1EzD" name="project.bom_entry.price_not_allowed_on_parts">
<segment>
<segment state="translated">
<source>project.bom_entry.price_not_allowed_on_parts</source>
<target>Prices are not allowed on BOM entries associated with a part. Define the price on the part instead.</target>
</segment>
</unit>
<unit id="ID056SR" name="validator.project_build.lot_bigger_than_needed">
<segment>
<segment state="translated">
<source>validator.project_build.lot_bigger_than_needed</source>
<target>You have selected more quantity to withdraw than needed! Remove unnecessary quantity.</target>
</segment>
</unit>
<unit id="6hV5UqD" name="validator.project_build.lot_smaller_than_needed">
<segment>
<segment state="translated">
<source>validator.project_build.lot_smaller_than_needed</source>
<target>You have selected less quantity to withdraw than needed for the build! Add additional quantity.</target>
</segment>
</unit>
<unit id="G9ZKt.4" name="part.name.must_match_category_regex">
<segment>
<segment state="translated">
<source>part.name.must_match_category_regex</source>
<target>The part name does not match the regular expression stated by the category: %regex%</target>
</segment>
</unit>
<unit id="m8kMFhf" name="validator.attachment.name_not_blank">
<segment state="translated">
<segment>
<source>validator.attachment.name_not_blank</source>
<target>Set a value here, or upload a file to automatically use its filename as name for the attachment.</target>
</segment>

306
yarn.lock
View File

@@ -23,32 +23,32 @@
integrity sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==
"@babel/core@^7.19.6":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.0.tgz#1341aefdcc14ccc7553fcc688dd8986a2daffc13"
integrity sha512-PuxUbxcW6ZYe656yL3EAhpy7qXKq0DmYsrJLpbB8XrsCP9Nm+XCg9XFMb5vIDliPD7+U/+M+QJlH17XOcB7eXA==
version "7.21.3"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.3.tgz#cf1c877284a469da5d1ce1d1e53665253fae712e"
integrity sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.18.6"
"@babel/generator" "^7.21.0"
"@babel/generator" "^7.21.3"
"@babel/helper-compilation-targets" "^7.20.7"
"@babel/helper-module-transforms" "^7.21.0"
"@babel/helper-module-transforms" "^7.21.2"
"@babel/helpers" "^7.21.0"
"@babel/parser" "^7.21.0"
"@babel/parser" "^7.21.3"
"@babel/template" "^7.20.7"
"@babel/traverse" "^7.21.0"
"@babel/types" "^7.21.0"
"@babel/traverse" "^7.21.3"
"@babel/types" "^7.21.3"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.2"
semver "^6.3.0"
"@babel/generator@^7.21.0", "@babel/generator@^7.21.1":
version "7.21.1"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.1.tgz#951cc626057bc0af2c35cd23e9c64d384dea83dd"
integrity sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA==
"@babel/generator@^7.21.3":
version "7.21.3"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.3.tgz#232359d0874b392df04045d72ce2fd9bb5045fce"
integrity sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==
dependencies:
"@babel/types" "^7.21.0"
"@babel/types" "^7.21.3"
"@jridgewell/gen-mapping" "^0.3.2"
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
@@ -154,7 +154,7 @@
dependencies:
"@babel/types" "^7.18.6"
"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.0", "@babel/helper-module-transforms@^7.21.2":
"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.2":
version "7.21.2"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2"
integrity sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==
@@ -266,10 +266,10 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@babel/parser@^7.18.9", "@babel/parser@^7.20.7", "@babel/parser@^7.21.0", "@babel/parser@^7.21.2":
version "7.21.2"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.2.tgz#dacafadfc6d7654c3051a66d6fe55b6cb2f2a0b3"
integrity sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ==
"@babel/parser@^7.18.9", "@babel/parser@^7.20.7", "@babel/parser@^7.21.3":
version "7.21.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.3.tgz#1d285d67a19162ff9daa358d4cb41d50c06220b3"
integrity sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
version "7.18.6"
@@ -575,9 +575,9 @@
"@babel/template" "^7.20.7"
"@babel/plugin-transform-destructuring@^7.20.2":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz#8bda578f71620c7de7c93af590154ba331415454"
integrity sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==
version "7.21.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz#73b46d0fd11cd6ef57dea8a381b1215f4959d401"
integrity sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==
dependencies:
"@babel/helper-plugin-utils" "^7.20.2"
@@ -693,9 +693,9 @@
"@babel/helper-replace-supers" "^7.18.6"
"@babel/plugin-transform-parameters@^7.20.1", "@babel/plugin-transform-parameters@^7.20.7":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz#0ee349e9d1bc96e78e3b37a7af423a4078a7083f"
integrity sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==
version "7.21.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz#18fc4e797cf6d6d972cb8c411dbe8a809fa157db"
integrity sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==
dependencies:
"@babel/helper-plugin-utils" "^7.20.2"
@@ -885,26 +885,26 @@
"@babel/parser" "^7.20.7"
"@babel/types" "^7.20.7"
"@babel/traverse@^7.18.9", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2":
version "7.21.2"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.2.tgz#ac7e1f27658750892e815e60ae90f382a46d8e75"
integrity sha512-ts5FFU/dSUPS13tv8XiEObDu9K+iagEKME9kAbaP7r0Y9KtZJZ+NGndDvWoRAYNpeWafbpFeki3q9QoMD6gxyw==
"@babel/traverse@^7.18.9", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.3":
version "7.21.3"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.3.tgz#4747c5e7903d224be71f90788b06798331896f67"
integrity sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==
dependencies:
"@babel/code-frame" "^7.18.6"
"@babel/generator" "^7.21.1"
"@babel/generator" "^7.21.3"
"@babel/helper-environment-visitor" "^7.18.9"
"@babel/helper-function-name" "^7.21.0"
"@babel/helper-hoist-variables" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6"
"@babel/parser" "^7.21.2"
"@babel/types" "^7.21.2"
"@babel/parser" "^7.21.3"
"@babel/types" "^7.21.3"
debug "^4.1.0"
globals "^11.1.0"
"@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.4.4":
version "7.21.2"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.2.tgz#92246f6e00f91755893c2876ad653db70c8310d1"
integrity sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==
"@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.3", "@babel/types@^7.4.4":
version "7.21.3"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.3.tgz#4865a5357ce40f64e3400b0f3b737dc6d4f64d05"
integrity sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==
dependencies:
"@babel/helper-string-parser" "^7.19.4"
"@babel/helper-validator-identifier" "^7.19.1"
@@ -1399,10 +1399,10 @@
dependencies:
"@sinclair/typebox" "^0.25.16"
"@jest/types@^29.4.3":
version "29.4.3"
resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.4.3.tgz#9069145f4ef09adf10cec1b2901b2d390031431f"
integrity sha512-bPYfw8V65v17m2Od1cv44FH+SiKW7w2Xu7trhcdTLUmSv85rfKsP+qXSjO4KGJr4dtPSzl/gvslZBXctf1qGEA==
"@jest/types@^29.5.0":
version "29.5.0"
resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593"
integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==
dependencies:
"@jest/schemas" "^29.4.3"
"@types/istanbul-lib-coverage" "^2.0.0"
@@ -1451,7 +1451,7 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
"@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
version "0.3.17"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985"
integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==
@@ -1627,9 +1627,9 @@
"@types/estree" "*"
"@types/eslint@*":
version "8.21.1"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.21.1.tgz#110b441a210d53ab47795124dbc3e9bb993d1e7c"
integrity sha512-rc9K8ZpVjNcLs8Fp0dkozd5Pt2Apk1glO4Vgz8ix1u6yFByxfqo5Yavpy65o+93TAe24jr7v+eSBtFLvOQtCRQ==
version "8.21.2"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.21.2.tgz#2b61b43a8b0e66006856a2a4c8e51f6f773ead27"
integrity sha512-EMpxUyystd3uZVByZap1DACsMXvb82ypQnGn89e1Y0a+LYu3JJscUd/gqhRsVFDkaD2MIiWo0MT8EfXr3DGRKw==
dependencies:
"@types/estree" "*"
"@types/json-schema" "*"
@@ -1713,9 +1713,9 @@
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
"@types/node@*":
version "18.14.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.6.tgz#ae1973dd2b1eeb1825695bb11ebfb746d27e3e93"
integrity sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==
version "18.15.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014"
integrity sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==
"@types/parse-json@^4.0.0":
version "4.0.0"
@@ -2403,9 +2403,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449:
version "1.0.30001460"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz#31d2e26f0a2309860ed3eff154e03890d9d851a7"
integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ==
version "1.0.30001467"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001467.tgz#1afc9c16ed61f50dd87139da87ca43a3e0051c77"
integrity sha512-cEdN/5e+RPikvl9AHm4uuLXxeCNq8rFsQ+lPHTfe/OtypP3WwnVVbjn+6uBV7PaFL6xUFzTh+sSCOz1rKhcO+Q==
chalk@^2.0.0, chalk@^2.3.2:
version "2.4.2"
@@ -2669,16 +2669,16 @@ cookie@0.5.0:
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
core-js-compat@^3.25.1:
version "3.29.0"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.29.0.tgz#1b8d9eb4191ab112022e7f6364b99b65ea52f528"
integrity sha512-ScMn3uZNAFhK2DGoEfErguoiAHhV2Ju+oJo/jK08p7B3f3UhocUrCCkTvnZaiS+edl5nlIoiBXKcwMc6elv4KQ==
version "3.29.1"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.29.1.tgz#15c0fb812ea27c973c18d425099afa50b934b41b"
integrity sha512-QmchCua884D8wWskMX8tW5ydINzd8oSJVx38lx/pVkFGqztxt73GYre3pm/hyYq8bPf+MW5In4I/uRShFDsbrA==
dependencies:
browserslist "^4.21.5"
core-js@^3.23.0:
version "3.29.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.29.0.tgz#0273e142b67761058bcde5615c503c7406b572d6"
integrity sha512-VG23vuEisJNkGl6XQmFJd3rEG/so/CNatqeE+7uZAwTSwFeB/qaO0be8xZYUNWprJ/GIwL8aMt9cj1kvbpTZhg==
version "3.29.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.29.1.tgz#40ff3b41588b091aaed19ca1aa5cb111803fa9a6"
integrity sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==
core-util-is@~1.0.0:
version "1.0.3"
@@ -2871,102 +2871,102 @@ data-urls@^2.0.0:
whatwg-url "^8.0.0"
datatables.net-bs5@>=1.12.1, datatables.net-bs5@^1.10.20:
version "1.13.3"
resolved "https://registry.yarnpkg.com/datatables.net-bs5/-/datatables.net-bs5-1.13.3.tgz#5b5a51fe48f26f5b8e37f28c8b2aee485291c3ac"
integrity sha512-yaZ89SjP1INIotvCVaBkgQ0VPvWTAfywG3dmBaVIosa4u0vUAqlx1+aYyL8WXgqmklSQda3nXdNwJu0OrPw84A==
version "1.13.4"
resolved "https://registry.yarnpkg.com/datatables.net-bs5/-/datatables.net-bs5-1.13.4.tgz#c73058782484cb84d9bc3ae9b8c0f1950b78bf2b"
integrity sha512-+gtaiau4vJeuGvnsYWmQy9gqa5XQ15XmkdwpK5EjwYCMzZZEXMQ3wfu2FddBcX5tX9Ual8C+Tf1s2gmqLGNbKQ==
dependencies:
datatables.net ">=1.12.1"
jquery ">=1.7"
datatables.net-buttons-bs5@^2.2.2:
version "2.3.5"
resolved "https://registry.yarnpkg.com/datatables.net-buttons-bs5/-/datatables.net-buttons-bs5-2.3.5.tgz#c19665420f4e3c476b5749e54dd7f9a3c72106f9"
integrity sha512-6ZjOaRdNjDFElnLtmDfyCYrzCwd80FAWt/BrFrzvp9cSJMfiNL6dBUsPCR9NXUoiIwWZLcIu4++VAkTkymTrFA==
version "2.3.6"
resolved "https://registry.yarnpkg.com/datatables.net-buttons-bs5/-/datatables.net-buttons-bs5-2.3.6.tgz#c44796326fbe614ee7e47aebdfa284d0b14a5486"
integrity sha512-rAeK04RvvtfV9Wm893t7jsIcqT/ew1uwLus37uCg2CL2gcXZcb4qR7/mLcL1I80Rz5g+8LeO5ANPAWUnfzaUqg==
dependencies:
datatables.net-bs5 ">=1.12.1"
datatables.net-buttons ">=2.2.3"
jquery ">=1.7"
datatables.net-buttons@>=2.2.3:
version "2.3.5"
resolved "https://registry.yarnpkg.com/datatables.net-buttons/-/datatables.net-buttons-2.3.5.tgz#70bbcdf529035b735da3be58da6cee0829a7cd26"
integrity sha512-iY4BC10zNWuyzgdiPQ4b8n9Cz74oEk4OzO8FTDeg0yytmsZVT/JncIS2mBt08JW4DLWaB6a0+6UPd+0UHwvIUA==
version "2.3.6"
resolved "https://registry.yarnpkg.com/datatables.net-buttons/-/datatables.net-buttons-2.3.6.tgz#68d74ccfb282a558e4a9d525871b20e017f7fea3"
integrity sha512-9eid5D2kbTNfGCOiEx5WBHlwfK38W1LMz0AiNZHoSqKAiO0aXGfzrH7L2eY6reHgVMaPvHPAnqeRAjvOul2V/Q==
dependencies:
datatables.net ">=1.12.1"
jquery ">=1.7"
datatables.net-colreorder-bs5@^1.5.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/datatables.net-colreorder-bs5/-/datatables.net-colreorder-bs5-1.6.1.tgz#471cb30da3f33a2db2d67c00130ceba7fec7d016"
integrity sha512-wzAAmP1p3VrVEIfkJMh2Dx4qGdQV1lU8Wcnk5DgQI3aRqvVMdsM/6yXzj1IH2j1SnyRzvtzzxF869Bj3rBwZ3g==
version "1.6.2"
resolved "https://registry.yarnpkg.com/datatables.net-colreorder-bs5/-/datatables.net-colreorder-bs5-1.6.2.tgz#1553c1f1a46b1ff3910c4d70b9bb68eeb4822de8"
integrity sha512-NDNWmI/Wq0Jo6ZHZKynnmXeAoN4jAbjcEstOz3m5JFO7BsJoC+XdpfO3aPGuYVpwHfnVZJFb0ZG/mBBwr604Zg==
dependencies:
datatables.net-bs5 ">=1.12.1"
datatables.net-colreorder ">=1.5.6"
jquery ">=1.7"
datatables.net-colreorder@>=1.5.6:
version "1.6.1"
resolved "https://registry.yarnpkg.com/datatables.net-colreorder/-/datatables.net-colreorder-1.6.1.tgz#effecd14fb8c42299b4a120d31b6ab04ccdd5dca"
integrity sha512-ae0gdkG0OmrEGUrXYm0XgWDzDkuEhEuNrfvQsmtCTl0j+1nxtXPsecIiWBCqv/dM0X4x8PT0dJY8furqPCzXXw==
version "1.6.2"
resolved "https://registry.yarnpkg.com/datatables.net-colreorder/-/datatables.net-colreorder-1.6.2.tgz#27a29d2ea34fc60ce831bade51633d90293404d3"
integrity sha512-PrBzZA2mzBsI6NAMbgUykSdmZ3VJsf46chkeBy/1oiyArGc1e1/a5PLyb0HybkbZaFPWxeGxDAEJDVesC7j9pA==
dependencies:
datatables.net ">=1.12.1"
jquery ">=1.7"
datatables.net-fixedheader-bs5@^3.1.5:
version "3.3.1"
resolved "https://registry.yarnpkg.com/datatables.net-fixedheader-bs5/-/datatables.net-fixedheader-bs5-3.3.1.tgz#9a1b556fb51621477c28b6dd2cadd1bf36332d30"
integrity sha512-o/u3GsPq9cdZ0dhdcbwayWIP3nzGAAPnB41n1V1KaQsIYitO4KNHZw4VafENVRValKeuRMvkdZt60LCTUIB/4A==
version "3.3.2"
resolved "https://registry.yarnpkg.com/datatables.net-fixedheader-bs5/-/datatables.net-fixedheader-bs5-3.3.2.tgz#32bc2e2c550a6251a7c5803729cd26513d20f210"
integrity sha512-OlZf2fCPFiv3c/u37zRLUMvalpbDtqunh/y3GQp2uQ282jQ+lcIB1JD3dWJO//x2jnOfxWUlUWRGc/PMZJHVTw==
dependencies:
datatables.net-bs5 ">=1.12.1"
datatables.net-fixedheader ">=3.2.4"
jquery ">=1.7"
datatables.net-fixedheader@>=3.2.4:
version "3.3.1"
resolved "https://registry.yarnpkg.com/datatables.net-fixedheader/-/datatables.net-fixedheader-3.3.1.tgz#71aafa169b91f0fc1b3fa353d50f575ad67ad50d"
integrity sha512-m1ip5dOOsdjaFw2e5G77o+XLjqy5wWKnBnp+BwbnFCq4J5hFbMqcIV1r5z9X+NeAiKlADqZqteeLoO2xYRXBVA==
version "3.3.2"
resolved "https://registry.yarnpkg.com/datatables.net-fixedheader/-/datatables.net-fixedheader-3.3.2.tgz#ee9ce9572d1d34b44d6bd517ed261704f12634fb"
integrity sha512-jhXC3Ce/hj34zZWCLXLtHUcnTqzV3LYq97xdpIBzvIbKTA/dPwVNP+vvrr2vNh6iWkohljuq8Jvf/IR2BmGn5Q==
dependencies:
datatables.net ">=1.12.1"
jquery ">=1.7"
datatables.net-responsive-bs5@^2.2.3:
version "2.4.0"
resolved "https://registry.yarnpkg.com/datatables.net-responsive-bs5/-/datatables.net-responsive-bs5-2.4.0.tgz#ab3a6fb147e5886ae043c9ecd54652f3a5d6d5c0"
integrity sha512-2kxigvftgVgdyH+BZ17jbR+BcWYZsacnlTt4PeOH/P98bMuqJKrziss7CLz/6qud/ehHdLEQTjMDoJddN9NfxA==
version "2.4.1"
resolved "https://registry.yarnpkg.com/datatables.net-responsive-bs5/-/datatables.net-responsive-bs5-2.4.1.tgz#8342ea3d97f49b2d4ce3af9742367d2feef31000"
integrity sha512-gtW0IjNMWCWQ+KHpIPcgXiQrq2Dhxz8yKql4sS2WfuYz+Z7645mk7xmqEhy4aqpWEGXdv4rcaZhr4tt49NLOpQ==
dependencies:
datatables.net-bs5 ">=1.12.1"
datatables.net-responsive ">=2.3.0"
jquery ">=1.7"
datatables.net-responsive@>=2.3.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/datatables.net-responsive/-/datatables.net-responsive-2.4.0.tgz#91658679483e3ef6cda8b9bc5c1b7eff076a8e70"
integrity sha512-Acws4aEPJZX/+CQsXpuDBHfrwgl3XxWEc/zlsnJCXE/GGbqjVAtQt7SM6EBcdReMv1FbyWUlF/Uw+de11NT46A==
version "2.4.1"
resolved "https://registry.yarnpkg.com/datatables.net-responsive/-/datatables.net-responsive-2.4.1.tgz#3396ec812e779424bc9261cc1ce527a57a2d9e42"
integrity sha512-6aGZybNb65lRPtd+hPHRyaBgs7D3R2Hw+RQP7he3qH5BTpMH9BG4FxVvyAud0Q4gyopO9I52uf60p2GT++4qOA==
dependencies:
datatables.net ">=1.12.1"
jquery ">=1.7"
datatables.net-select-bs5@^1.2.7:
version "1.6.1"
resolved "https://registry.yarnpkg.com/datatables.net-select-bs5/-/datatables.net-select-bs5-1.6.1.tgz#c32ac7760c217f071aa4f4190160c7cec8402809"
integrity sha512-Dz7zaRqcBklIO36I16uMtPJ6WO3H+2czV1DkjhQ7Zzgvf6RXI51U3002h/QM5JtMcjsDc2Di7c+keR9TSXKUng==
version "1.6.2"
resolved "https://registry.yarnpkg.com/datatables.net-select-bs5/-/datatables.net-select-bs5-1.6.2.tgz#94ca2d5354cd9ed56c08e94f41089364e272447c"
integrity sha512-lq1LyZRZzp4pNIm548m2B7zVPG/OKzq3nWAddAWkqEz1MJuOu5I4d58TwN0YmWuOOYyj1gADbM4qM9bzqfS+5A==
dependencies:
datatables.net-bs5 ">=1.12.1"
datatables.net-select ">=1.4.0"
jquery ">=1.7"
datatables.net-select@>=1.4.0:
version "1.6.1"
resolved "https://registry.yarnpkg.com/datatables.net-select/-/datatables.net-select-1.6.1.tgz#dc322112802cb36007e5f6429cdbd586a368e558"
integrity sha512-0Ghr0fPFwUYFVcQRU7njnms0e12iIkhuC8cv6nDjsoby2w/JK+xgBlmW+7fEcqF8pxwZzFZb8bHdK9f4p4s9ag==
version "1.6.2"
resolved "https://registry.yarnpkg.com/datatables.net-select/-/datatables.net-select-1.6.2.tgz#f782a63369a75dfd5d416fa7192d473cce71cc9d"
integrity sha512-mGkWFGEN8vLJGp9POHA+CHDBIcWGOpos6mtuZ/S3hKF+niTZivqRuXybd79iDG8OHw3yRbafBIsHtcXcZxoG/g==
dependencies:
datatables.net ">=1.12.1"
jquery ">=1.7"
datatables.net@>=1.12.1:
version "1.13.3"
resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.13.3.tgz#ee7d7b16b479b5075412b104d980184693b4325b"
integrity sha512-YVnz02oJsaP/OfnclBlqHkuV1il60sSVa+a0Xvs5gyiDLftmAxc+rvVAwCm7O0OpKo09N43k6EcCAf3L9WYI7g==
version "1.13.4"
resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.13.4.tgz#9a809cee82eca0a884e10b4d47a3a3d6e65e9fe7"
integrity sha512-yzhArTOB6tPO2QFKm1z3hA4vabtt2hRvgw8XLsT1xqEirinfGYqWDiWXlkTPTaJv2e7gG+Kf985sXkzBFlGrGQ==
dependencies:
jquery ">=1.7"
@@ -3174,9 +3174,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.4.284:
version "1.4.320"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.320.tgz#4d83a90ff74f93939c5413c2ac5a16c696600632"
integrity sha512-h70iRscrNluMZPVICXYl5SSB+rBKo22XfuIS1ER0OQxQZpKTnFpuS6coj7wY9M/3trv7OR88rRMOlKmRvDty7Q==
version "1.4.333"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.333.tgz#ebb21f860f8a29923717b06ec0cb54e77ed34c04"
integrity sha512-YyE8+GKyGtPEP1/kpvqsdhD6rA/TP1DUFDN4uiU/YI52NzDxmwHkEb3qjId8hLBa5siJvG0sfC3O66501jMruQ==
emoji-regex@^8.0.0:
version "8.0.0"
@@ -3736,9 +3736,9 @@ good-listener@^1.2.2:
delegate "^3.1.2"
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9:
version "4.2.10"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
growly@^1.3.0:
version "1.3.0"
@@ -4153,12 +4153,12 @@ javascript-stringify@^1.6.0:
resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3"
integrity sha512-fnjC0up+0SjEJtgmmG+teeel68kutkvzfctO/KxE3qJlbunkJYAshgH3boU++gSBHP8z5/r0ts0qRIrHf0RTQQ==
jest-util@^29.4.3:
version "29.4.3"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.4.3.tgz#851a148e23fc2b633c55f6dad2e45d7f4579f496"
integrity sha512-ToSGORAz4SSSoqxDSylWX8JzkOQR7zoBtNRsA7e+1WUX5F8jrOwaNpuh1YfJHJKDHXLHmObv5eOjejUd+/Ws+Q==
jest-util@^29.5.0:
version "29.5.0"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f"
integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==
dependencies:
"@jest/types" "^29.4.3"
"@jest/types" "^29.5.0"
"@types/node" "*"
chalk "^4.0.0"
ci-info "^3.2.0"
@@ -4184,19 +4184,19 @@ jest-worker@^27.4.5:
supports-color "^8.0.0"
jest-worker@^29.1.2:
version "29.4.3"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.4.3.tgz#9a4023e1ea1d306034237c7133d7da4240e8934e"
integrity sha512-GLHN/GTAAMEy5BFdvpUfzr9Dr80zQqBrh0fz1mtRMe05hqP45+HfQltu7oTBfduD0UeZs09d+maFtFYAXFWvAA==
version "29.5.0"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.5.0.tgz#bdaefb06811bd3384d93f009755014d8acb4615d"
integrity sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==
dependencies:
"@types/node" "*"
jest-util "^29.4.3"
jest-util "^29.5.0"
merge-stream "^2.0.0"
supports-color "^8.0.0"
jquery@>=1.7, jquery@^3.5.1:
version "3.6.3"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.3.tgz#23ed2ffed8a19e048814f13391a19afcdba160e6"
integrity sha512-bZ5Sy3YzKo9Fyc8wH2iIQK4JImJ6R0GWI9kL1/k7Z91ZBNgkRXE6U0JfHIizZbort8ZunhSI3jw9I6253ahKfg==
version "3.6.4"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.4.tgz#ba065c188142100be4833699852bf7c24dc0252f"
integrity sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ==
js-tokens@^4.0.0:
version "4.0.0"
@@ -4300,6 +4300,14 @@ klona@^2.0.4:
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22"
integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==
launch-editor@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.0.tgz#4c0c1a6ac126c572bd9ff9a30da1d2cae66defd7"
integrity sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==
dependencies:
picocolors "^1.0.0"
shell-quote "^1.7.3"
levn@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@@ -4492,9 +4500,9 @@ mimic-fn@^2.1.0:
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
mini-css-extract-plugin@^2.6.0:
version "2.7.2"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.2.tgz#e049d3ea7d3e4e773aad585c6cb329ce0c7b72d7"
integrity sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw==
version "2.7.5"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.5.tgz#afbb344977659ec0f1f6e050c7aea456b121cfc5"
integrity sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ==
dependencies:
schema-utils "^4.0.0"
@@ -4544,9 +4552,9 @@ minipass@^3.0.0, minipass@^3.1.1:
yallist "^4.0.0"
minipass@^4.0.0:
version "4.2.4"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.4.tgz#7d0d97434b6a19f59c5c3221698b48bbf3b2cd06"
integrity sha512-lwycX3cBMTvcejsHITUgYj6Gy6A7Nh4Q6h9NP4sTHY1ccJlC7yKzDmiShEHsJ16Jf1nKGDEaiHxiltsJEvk0nQ==
version "4.2.5"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.5.tgz#9e0e5256f1e3513f8c34691dd68549e85b2c8ceb"
integrity sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q==
minizlib@^2.1.1:
version "2.1.2"
@@ -5350,9 +5358,9 @@ read-cache@^1.0.0:
pify "^2.3.0"
"readable-stream@2 || 3", readable-stream@^3.0.6:
version "3.6.1"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.1.tgz#f9f9b5f536920253b3d26e7660e7da4ccff9bb62"
integrity sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==
version "3.6.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
@@ -5431,9 +5439,9 @@ regexp.prototype.flags@^1.2.0:
functions-have-names "^1.2.2"
regexpu-core@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.1.tgz#66900860f88def39a5cb79ebd9490e84f17bcdfb"
integrity sha512-nCOzW2V/X15XpLsK2rlgdwrysrBq+AauCn+omItIz4R1pIcmeot5zvjdmOBRLzEH/CkC6IxMJVmxDe3QcMuNVQ==
version "5.3.2"
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b"
integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==
dependencies:
"@babel/regjsgen" "^0.8.0"
regenerate "^1.4.2"
@@ -5674,7 +5682,7 @@ serialize-javascript@^5.0.1:
dependencies:
randombytes "^2.1.0"
serialize-javascript@^6.0.0:
serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c"
integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==
@@ -5743,6 +5751,11 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-quote@^1.7.3:
version "1.8.0"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba"
integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==
shelljs@^0.8.1:
version "0.8.5"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c"
@@ -5962,9 +5975,9 @@ style-loader@^2.0.0:
schema-utils "^3.0.0"
style-loader@^3.3.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575"
integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==
version "3.3.2"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.2.tgz#eaebca714d9e462c19aa1e3599057bc363924899"
integrity sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==
stylehacks@^5.1.1:
version "5.1.1"
@@ -6063,20 +6076,20 @@ terser-webpack-plugin@^4.2.3:
webpack-sources "^1.4.3"
terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.3.0:
version "5.3.6"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz#5590aec31aa3c6f771ce1b1acca60639eab3195c"
integrity sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==
version "5.3.7"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz#ef760632d24991760f339fe9290deb936ad1ffc7"
integrity sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==
dependencies:
"@jridgewell/trace-mapping" "^0.3.14"
"@jridgewell/trace-mapping" "^0.3.17"
jest-worker "^27.4.5"
schema-utils "^3.1.1"
serialize-javascript "^6.0.0"
terser "^5.14.1"
serialize-javascript "^6.0.1"
terser "^5.16.5"
terser@^5.14.1, terser@^5.3.4:
version "5.16.5"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.5.tgz#1c285ca0655f467f92af1bbab46ab72d1cb08e5a"
integrity sha512-qcwfg4+RZa3YvlFh0qjifnzBHjKGNbtDo9yivMqMFDy9Q6FSaQWSB/j1xKhsoUFJIqDOM3TsN6D5xbrMrFcHbg==
terser@^5.16.5, terser@^5.3.4:
version "5.16.6"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.6.tgz#f6c7a14a378ee0630fbe3ac8d1f41b4681109533"
integrity sha512-IBZ+ZQIA9sMaXmRZCUMDjNH0D5AQQfdn4WUjHL0+1lF4TP1IHRJbrhb6fNaXWikrYQTSkb7SLxkeXAiy1p7mbg==
dependencies:
"@jridgewell/source-map" "^0.3.2"
acorn "^8.5.0"
@@ -6431,9 +6444,9 @@ webpack-dev-middleware@^5.3.1:
schema-utils "^4.0.0"
webpack-dev-server@^4.8.0:
version "4.11.1"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz#ae07f0d71ca0438cf88446f09029b92ce81380b5"
integrity sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==
version "4.13.1"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.13.1.tgz#6417a9b5d2f528e7644b68d6ed335e392dccffe8"
integrity sha512-5tWg00bnWbYgkN+pd5yISQKDejRBYGEw15RaEEslH+zdbNDxxaZvEAO2WulaSaFKb5n3YG8JXsGaDsut1D0xdA==
dependencies:
"@types/bonjour" "^3.5.9"
"@types/connect-history-api-fallback" "^1.3.5"
@@ -6454,6 +6467,7 @@ webpack-dev-server@^4.8.0:
html-entities "^2.3.2"
http-proxy-middleware "^2.0.3"
ipaddr.js "^2.0.1"
launch-editor "^2.6.0"
open "^8.0.9"
p-retry "^4.5.0"
rimraf "^3.0.2"
@@ -6463,7 +6477,7 @@ webpack-dev-server@^4.8.0:
sockjs "^0.3.24"
spdy "^4.0.2"
webpack-dev-middleware "^5.3.1"
ws "^8.4.2"
ws "^8.13.0"
webpack-merge@^5.7.3:
version "5.8.0"
@@ -6503,9 +6517,9 @@ webpack-sources@^3.2.3:
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.74.0:
version "5.75.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152"
integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==
version "5.76.2"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.2.tgz#6f80d1c1d1e3bf704db571b2504a0461fac80230"
integrity sha512-Th05ggRm23rVzEOlX8y67NkYCHa9nTNcwHPBhdg+lKG+mtiW7XgggjAeeLnADAe7mLjJ6LUNfgHAuRRh+Z6J7w==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^0.0.51"
@@ -6594,10 +6608,10 @@ ws@^7.3.1, ws@^7.4.6:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
ws@^8.4.2:
version "8.12.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.1.tgz#c51e583d79140b5e42e39be48c934131942d4a8f"
integrity sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==
ws@^8.13.0:
version "8.13.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
xml-name-validator@^3.0.0:
version "3.0.0"