Merge branch 'part_import'

This commit is contained in:
Jan Böhmer 2023-03-15 21:52:08 +01:00
commit 54276e19e9
61 changed files with 2100 additions and 210 deletions

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) //Hide the select element (the tomselect button is the sibling of the select element)
select_target.nextElementSibling.classList.add('d-none'); 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) { confirmDeletionAtSubmit(event) {

View file

@ -28,7 +28,9 @@ class ErrorHandlerHelper {
console.log('Error Handler registered'); console.log('Error Handler registered');
const content = document.getElementById('content'); 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)); $(document).ajaxError(this.handleJqueryErrror.bind(this));
} }
@ -87,8 +89,10 @@ class ErrorHandlerHelper {
} }
handleError(event) { handleError(event) {
const fetchResponse = event.detail.fetchResponse; //Prevent default error handling
const response = fetchResponse.response; event.preventDefault();
const response = event.detail.response;
//Ignore aborted requests. //Ignore aborted requests.
if (response.statusText === 'abort' || response.status == 0) { if (response.statusText === 'abort' || response.status == 0) {
@ -100,11 +104,11 @@ class ErrorHandlerHelper {
return; return;
} }
if(fetchResponse.failed) { if(!response.ok) {
response.text().then(responseHTML => { 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 => { }).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

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

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\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@ -338,20 +339,39 @@ abstract class BaseAdminController extends AbstractController
$file = $import_form['file']->getData(); $file = $import_form['file']->getData();
$data = $import_form->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 = [ $options = [
'parent' => $data['parent'], 'parent' => $data['parent'] ?? null,
'preserve_children' => $data['preserve_children'], 'preserve_children' => $data['preserve_children'] ?? false,
'format' => $data['format'], 'format' => $format,
'csv_separator' => $data['csv_separator'], 'class' => $this->entity_class,
'csv_delimiter' => $data['csv_delimiter'],
'abort_on_validation_error' => $data['abort_on_validation_error'],
]; ];
$this->commentHelper->setMessage('Import '.$file->getClientOriginalName()); $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 */ /** @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(); $em->flush();
} }
ret:
return $this->renderForm($this->twig_template, [ return $this->renderForm($this->twig_template, [
'entity' => $new_entity, 'entity' => $new_entity,
'form' => $form, '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

@ -95,6 +95,25 @@ class SelectAPIController extends AbstractController
return $this->getResponseForClass(Project::class, false); 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") * @Route("/label_profiles", name="select_label_profiles")
* @return Response * @return Response

View file

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

View file

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

View file

@ -38,20 +38,37 @@ use Symfony\Component\Serializer\Annotation\Groups;
* @ORM\MappedSuperclass(repositoryClass="App\Repository\DBElementRepository") * @ORM\MappedSuperclass(repositoryClass="App\Repository\DBElementRepository")
* *
* @DiscriminatorMap(typeProperty="type", mapping={ * @DiscriminatorMap(typeProperty="type", mapping={
* "attachment_type" = "App\Entity\AttachmentType", * "attachment_type" = "App\Entity\Attachments\AttachmentType",
* "attachment" = "App\Entity\Attachment", * "attachment" = "App\Entity\Attachments\Attachment",
* "category" = "App\Entity\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" = "App\Entity\ProjectSystem\Project",
* "project_bom_entry" = "App\Entity\ProjectSystem\ProjectBOMEntry", * "project_bom_entry" = "App\Entity\ProjectSystem\ProjectBOMEntry",
* "footprint" = "App\Entity\Footprint", * "footprint" = "App\Entity\Parts\Footprint",
* "group" = "App\Entity\Group", * "group" = "App\Entity\UserSystem\Group",
* "manufacturer" = "App\Entity\Manufacturer", * "manufacturer" = "App\Entity\Parts\Manufacturer",
* "orderdetail" = "App\Entity\Orderdetail", * "orderdetail" = "App\Entity\PriceInformations\Orderdetail",
* "part" = "App\Entity\Part", * "part" = "App\Entity\Parts\Part",
* "pricedetail" = "App\Entity\Pricedetail", * "pricedetail" = "App\Entity\PriceInformation\Pricedetail",
* "storelocation" = "App\Entity\Storelocation", * "storelocation" = "App\Entity\Parts\Storelocation",
* "supplier" = "App\Entity\Supplier", * "part_lot" = "App\Entity\Parts\PartLot",
* "user" = "App\Entity\User" * "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 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 * @var string the name of this element
* @ORM\Column(type="string") * @ORM\Column(type="string")
* @Assert\NotBlank() * @Assert\NotBlank()
* @Groups({"simple", "extended", "full"}) * @Groups({"simple", "extended", "full", "import"})
*/ */
protected string $name = ''; protected string $name = '';

View file

@ -23,13 +23,15 @@ declare(strict_types=1);
namespace App\Entity\Base; namespace App\Entity\Base;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/** /**
* Class PartsContainingDBElement. * Class PartsContainingDBElement.
* *
* @ORM\MappedSuperclass(repositoryClass="App\Repository\AbstractPartsContainingRepository") * @ORM\MappedSuperclass(repositoryClass="App\Repository\AbstractPartsContainingRepository")
*/ */
abstract class abstract class AbstractPartsContainingDBElement extends AbstractStructuralDBElement
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 * @var string The comment info for this element
* @ORM\Column(type="text") * @ORM\Column(type="text")
* @Groups({"simple", "extended", "full"}) * @Groups({"full", "import"})
*/ */
protected string $comment = ''; 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. * @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. * Useful if this element should be used only for grouping, sorting.
* @ORM\Column(type="boolean") * @ORM\Column(type="boolean")
* @Groups({"full", "import"})
*/ */
protected bool $not_selectable = false; protected bool $not_selectable = false;
@ -91,7 +92,7 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement
/** /**
* @var AbstractStructuralDBElement * @var AbstractStructuralDBElement
* @NoneOfItsChildren() * @NoneOfItsChildren()
* @Groups({"include_parents"}) * @Groups({"include_parents", "import"})
*/ */
protected $parent = null; protected $parent = null;

View file

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

View file

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

View file

@ -28,6 +28,7 @@ use App\Entity\Parameters\MeasurementUnitParameter;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@ -48,6 +49,7 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
* or m (for meters). * or m (for meters).
* @ORM\Column(type="string", name="unit", nullable=true) * @ORM\Column(type="string", name="unit", nullable=true)
* @Assert\Length(max=10) * @Assert\Length(max=10)
* @Groups({"extended", "full", "import"})
*/ */
protected ?string $unit = null; 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. * @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. * Set to false, to measure continuous sizes likes masses or lengths.
* @ORM\Column(type="boolean", name="is_integer") * @ORM\Column(type="boolean", name="is_integer")
* @Groups({"extended", "full", "import"})
*/ */
protected bool $is_integer = false; 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 * Useful for sizes like meters. For this the unit must be set
* @ORM\Column(type="boolean", name="use_si_prefix") * @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") * @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; protected bool $use_si_prefix = false;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,6 +27,7 @@ use App\Entity\Base\AbstractPartsContainingDBElement;
use App\Entity\Parameters\StorelocationParameter; use App\Entity\Parameters\StorelocationParameter;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@ -70,20 +71,24 @@ class Storelocation extends AbstractPartsContainingDBElement
/** /**
* @var bool * @var bool
* @ORM\Column(type="boolean") * @ORM\Column(type="boolean")
* @Groups({"full", "import"})
*/ */
protected bool $is_full = false; protected bool $is_full = false;
/** /**
* @var bool * @var bool
* @ORM\Column(type="boolean") * @ORM\Column(type="boolean")
* @Groups({"full", "import"})
*/ */
protected bool $only_single_part = false; protected bool $only_single_part = false;
/** /**
* @var bool * @var bool
* @ORM\Column(type="boolean") * @ORM\Column(type="boolean")
* @Groups({"full", "import"})
*/ */
protected bool $limit_to_existing_parts = false; protected bool $limit_to_existing_parts = false;
/** /**
* @var Collection<int, StorelocationAttachment> * @var Collection<int, StorelocationAttachment>
* @ORM\OneToMany(targetEntity="App\Entity\Attachments\StorelocationAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) * @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 Brick\Math\BigDecimal;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert; 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 * @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) * @ORM\Column(name="shipping_costs", nullable=true, type="big_decimal", precision=11, scale=5)
* @Groups({"extended", "full", "import"})
* @BigDecimalPositiveOrZero() * @BigDecimalPositiveOrZero()
*/ */
protected ?BigDecimal $shipping_costs = null; protected ?BigDecimal $shipping_costs = null;

View file

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

View file

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

View file

@ -32,6 +32,7 @@ use Brick\Math\RoundingMode;
use DateTime; use DateTime;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert; 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) * @var BigDecimal The price related to the detail. (Given in the selected currency)
* @ORM\Column(type="big_decimal", precision=11, scale=5) * @ORM\Column(type="big_decimal", precision=11, scale=5)
* @BigDecimalPositive() * @BigDecimalPositive()
* @Groups({"extended", "full"})
*/ */
protected BigDecimal $price; protected BigDecimal $price;
@ -64,6 +66,7 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface
* @ORM\ManyToOne(targetEntity="Currency", inversedBy="pricedetails") * @ORM\ManyToOne(targetEntity="Currency", inversedBy="pricedetails")
* @ORM\JoinColumn(name="id_currency", referencedColumnName="id", nullable=true) * @ORM\JoinColumn(name="id_currency", referencedColumnName="id", nullable=true)
* @Selectable() * @Selectable()
* @Groups({"extended", "full", "import"})
*/ */
protected ?Currency $currency = null; protected ?Currency $currency = null;
@ -71,6 +74,7 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface
* @var float * @var float
* @ORM\Column(type="float") * @ORM\Column(type="float")
* @Assert\Positive() * @Assert\Positive()
* @Groups({"extended", "full", "import"})
*/ */
protected float $price_related_quantity = 1.0; protected float $price_related_quantity = 1.0;
@ -78,6 +82,7 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface
* @var float * @var float
* @ORM\Column(type="float") * @ORM\Column(type="float")
* @Assert\Positive() * @Assert\Positive()
* @Groups({"extended", "full", "import"})
*/ */
protected float $min_discount_quantity = 1.0; 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\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException; use InvalidArgumentException;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface; 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) * @ORM\OneToMany(targetEntity="ProjectBOMEntry", mappedBy="project", cascade={"persist", "remove"}, orphanRemoval=true)
* @Assert\Valid() * @Assert\Valid()
* @Groups({"extended", "full"})
*/ */
protected $bom_entries; protected $bom_entries;
@ -69,6 +71,7 @@ class Project extends AbstractStructuralDBElement
* @var string The current status of the project * @var string The current status of the project
* @ORM\Column(type="string", length=64, nullable=true) * @ORM\Column(type="string", length=64, nullable=true)
* @Assert\Choice({"draft","planning","in_production","finished","archived"}) * @Assert\Choice({"draft","planning","in_production","finished","archived"})
* @Groups({"extended", "full"})
*/ */
protected ?string $status = null; protected ?string $status = null;
@ -86,6 +89,7 @@ class Project extends AbstractStructuralDBElement
/** /**
* @ORM\Column(type="text", nullable=false, columnDefinition="DEFAULT ''") * @ORM\Column(type="text", nullable=false, columnDefinition="DEFAULT ''")
* @Groups({"simple", "extended", "full"})
*/ */
protected string $description = ''; protected string $description = '';

View file

@ -30,6 +30,7 @@ use App\Validator\Constraints\ValidPermission;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert; 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 * @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") * @ORM\Column(type="boolean", name="enforce_2fa")
* @Groups({"extended", "full", "import"})
*/ */
protected $enforce2FA = false; protected $enforce2FA = false;
/** /**
@ -79,6 +81,7 @@ class Group extends AbstractStructuralDBElement implements HasPermissionsInterfa
* @var PermissionData|null * @var PermissionData|null
* @ValidPermission() * @ValidPermission()
* @ORM\Embedded(class="PermissionData", columnPrefix="permissions_") * @ORM\Embedded(class="PermissionData", columnPrefix="permissions_")
* @Groups({"full"})
*/ */
protected ?PermissionData $permissions = null; protected ?PermissionData $permissions = null;

View file

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

View file

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace App\Form\AdminPages; namespace App\Form\AdminPages;
use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parts\Category;
use App\Entity\Parts\Part;
use App\Form\Type\StructuralEntityType; use App\Form\Type\StructuralEntityType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; 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. //Disable import if user is not allowed to create elements.
$entity = new $data['entity_class'](); $entity = new $data['entity_class']();
$perm_name = 'create'; $perm_name = 'import';
$disabled = !$this->security->isGranted($perm_name, $entity); $disabled = !$this->security->isGranted($perm_name, $entity);
$builder $builder
->add('format', ChoiceType::class, [ ->add('format', ChoiceType::class, [
'choices' => [ 'choices' => [
'parts.import.format.auto' => 'auto',
'JSON' => 'json', 'JSON' => 'json',
'XML' => 'xml', 'XML' => 'xml',
'CSV' => 'csv', 'CSV' => 'csv',
@ -63,7 +66,7 @@ class ImportType extends AbstractType
'label' => 'export.format', 'label' => 'export.format',
'disabled' => $disabled, 'disabled' => $disabled,
]) ])
->add('csv_separator', TextType::class, [ ->add('csv_delimiter', TextType::class, [
'data' => ';', 'data' => ';',
'label' => 'import.csv_separator', 'label' => 'import.csv_separator',
'disabled' => $disabled, '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, [ $builder->add('file', FileType::class, [
'label' => 'import.file', 'label' => 'import.file',
'attr' => [ 'attr' => [
@ -86,15 +134,9 @@ class ImportType extends AbstractType
'data-show-upload' => 'false', 'data-show-upload' => 'false',
], ],
'disabled' => $disabled, 'disabled' => $disabled,
]) ]);
->add('preserve_children', CheckboxType::class, [ $builder->add('abort_on_validation_error', CheckboxType::class, [
'data' => true,
'required' => false,
'label' => 'import.preserve_children',
'disabled' => $disabled,
])
->add('abort_on_validation_error', CheckboxType::class, [
'data' => true, 'data' => true,
'required' => false, 'required' => false,
'label' => 'import.abort_on_validation', 'label' => 'import.abort_on_validation',

View file

@ -29,6 +29,12 @@ use RecursiveIteratorIterator;
class StructuralDBElementRepository extends NamedDBElementRepository 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. * 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. * 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. * An array of the created elements will be returned, with the last element being the deepest element.
* @param string $path * @param string $path
@ -108,14 +114,67 @@ class StructuralDBElementRepository extends NamedDBElementRepository
continue; continue;
} }
//See if we already have an element with this name and 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]); $entity = $this->findOneBy(['name' => $name, 'parent' => $parent]);
}
if (null === $entity) { if (null === $entity) {
$class = $this->getClassName(); $class = $this->getClassName();
/** @var AbstractStructuralDBElement $entity */ /** @var AbstractStructuralDBElement $entity */
$entity = new $class; $entity = new $class;
$entity->setName($name); $entity->setName($name);
$entity->setParent($parent); $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; $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

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Services\ImportExportSystem; namespace App\Services\ImportExportSystem;
use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractNamedDBElement;
use Symfony\Component\OptionsResolver\OptionsResolver;
use function in_array; use function in_array;
use InvalidArgumentException; use InvalidArgumentException;
use function is_array; use function is_array;
@ -32,6 +33,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\SerializerInterface;
use function Symfony\Component\String\u;
/** /**
* Use this class to export an entity to multiple file formats. * Use this class to export an entity to multiple file formats.
@ -42,96 +44,128 @@ class EntityExporter
public function __construct(SerializerInterface $serializer) 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; $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');
}
/**
* 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. * 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 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 * @return Response the generated response containing the exported data
* *
* @throws ReflectionException * @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 (!is_array($entities)) {
if (!in_array($format, ['json', 'csv', 'yaml', 'xml'], true)) { $entities = [$entities];
throw new InvalidArgumentException('Given format is not supported!');
} }
//Check export verbosity level //Do the serialization with the given options
$level = $request->get('level') ?? 'extended'; $serialized_data = $this->exportEntities($entities, $options);
if (!in_array($level, ['simple', 'extended', 'full'], true)) {
throw new InvalidArgumentException('Given level is not supported!');
}
//Check for include children option $response = new Response($serialized_data);
$include_children = $request->get('include_children') ?? false;
//Check which groups we need to export, based on level and include_children //Resolve the format
$groups = [$level]; $optionsResolver = new OptionsResolver();
if ($include_children) { $this->configureOptions($optionsResolver);
$groups[] = 'include_children'; $options = $optionsResolver->resolve($options);
}
//Determine the content type for the response
//Plain text should work for all types //Plain text should work for all types
$content_type = 'text/plain'; $content_type = 'text/plain';
//Try to use better content types based on the format //Try to use better content types based on the format
$format = $options['format'];
switch ($format) { switch ($format) {
case 'xml': case 'xml':
$content_type = 'application/xml'; $content_type = 'application/xml';
break; break;
case 'json': case 'json':
$content_type = 'application/json'; $content_type = 'application/json';
break; 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); $response->headers->set('Content-Type', $content_type);
//If view option is not specified, then download the file. //If view option is not specified, then download the file.
if (!$request->get('view')) { if (!$request->get('view')) {
if ($entity instanceof AbstractNamedDBElement) {
$entity_name = $entity->getName(); //Determine the filename
} elseif (is_array($entity)) { //When we only have one entity, then we can use the name of the entity
if (empty($entity)) { if (count($entities) === 1) {
throw new InvalidArgumentException('$entity must not be empty!'); $entity_name = $entities[0]->getName();
} else {
//Use the class name of the first element for the filename otherwise
$reflection = new ReflectionClass($entities[0]);
$entity_name = $reflection->getShortName();
} }
//Use the class name of the first element for the filename $level = $options['level'];
$reflection = new ReflectionClass($entity[0]);
$entity_name = $reflection->getShortName();
} else {
throw new InvalidArgumentException('$entity type is not supported!');
}
$filename = 'export_'.$entity_name.'_'.$level.'.'.$format; $filename = 'export_'.$entity_name.'_'.$level.'.'.$format;
@ -139,7 +173,7 @@ class EntityExporter
$disposition = $response->headers->makeDisposition( $disposition = $response->headers->makeDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT, ResponseHeaderBag::DISPOSITION_ATTACHMENT,
$filename, $filename,
$string = preg_replace('![^'.preg_quote('-', '!').'a-z0-_9\s]+!', '', strtolower($filename)) u($filename)->ascii()->toString(),
); );
// Set the content disposition // Set the content disposition
$response->headers->set('Content-Disposition', $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\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parts\Category;
use App\Entity\Parts\Part;
use Symplify\EasyCodingStandard\ValueObject\Option;
use function count; use function count;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException; use InvalidArgumentException;
@ -48,7 +51,7 @@ class EntityImporter
/** /**
* Creates many entries at once, based on a (text) list of name. * 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 $lines The list of names seperated by \n
* @param string $class_name The name of the class for which the entities should be created * @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. * Import data from a string.
* The imported elements will be checked (validated) before written to database. * @param string $data The serialized data which should be imported
* * @param array $options The options for the import process
* @param File $file the file that should be used for importing * @param array $errors An array which will be filled with the validation errors, if any occurs during import
* @param string $class_name the class name of the enitity that should be imported * @return array An array containing all valid imported entities
* @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.
*/ */
public function fileToDBEntities(File $file, string $class_name, array $options = []): array public function importString(string $data, array $options = [], array &$errors = []): array
{ {
$resolver = new OptionsResolver(); $resolver = new OptionsResolver();
$this->configureOptions($resolver); $this->configureOptions($resolver);
$options = $resolver->resolve($options); $options = $resolver->resolve($options);
$entities = $this->fileToEntityArray($file, $class_name, $options); if (!is_a($options['class'], AbstractNamedDBElement::class, true)) {
throw new InvalidArgumentException('$class_name must be an AbstractNamedDBElement type!');
$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;
}
} }
//Save changes to database, when no error happened, or we should continue on error. $groups = ['import']; //We can only import data, that is marked with the group "import"
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'];
//Add group when the children should be preserved //Add group when the children should be preserved
if ($options['preserve_children']) { if ($options['preserve_children']) {
$groups[] = 'include_children'; $groups[] = 'include_children';
} }
//The [] behind class_name denotes that we expect an array. //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, '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)) { if (!is_array($entities)) {
$entities = [$entities]; $entities = [$entities];
} }
@ -220,18 +174,143 @@ class EntityImporter
$this->correctParentEntites($entities, null); $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; return $entities;
} }
protected function configureOptions(OptionsResolver $resolver): void protected function configureOptions(OptionsResolver $resolver): OptionsResolver
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'csv_separator' => ';', 'csv_delimiter' => ';', //The separator to use when importing csv files
'format' => 'json', 'format' => 'json', //The format of the file that should be imported
'class' => AbstractNamedDBElement::class,
'preserve_children' => true, 'preserve_children' => true,
'parent' => null, 'parent' => null, //The parent element to which the imported elements should be added
'abort_on_validation_error' => true, '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: //Iterate over the parts and apply the action to it:
foreach ($selected_parts as $part) { foreach ($selected_parts as $part) {

View file

@ -143,6 +143,12 @@ class ToolsTreeBuilder
$this->urlGenerator->generate('tools_ic_logos') $this->urlGenerator->generate('tools_ic_logos')
))->setIcon('fa-treeview fa-fw fa-solid fa-flag'); ))->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; 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() protected function generatePermissionStructure()
{ {
$cache = new ConfigCache($this->cache_file, $this->is_debug); $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 //Allow access to system log and server infos
$this->permissionResolver->setPermission($perm_holder, 'system', 'show_logs', PermissionData::ALLOW); $this->permissionResolver->setPermission($perm_holder, 'system', 'show_logs', PermissionData::ALLOW);
$this->permissionResolver->setPermission($perm_holder, 'system', 'server_infos', 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 private function editor(HasPermissionsInterface $permHolder): HasPermissionsInterface
@ -101,17 +115,18 @@ class PermissionPresetsHelper
$this->readOnly($permHolder); $this->readOnly($permHolder);
//Set datastructures //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, 'parts_stock', PermissionData::ALLOW);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'categories', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'categories', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'storelocations', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'storelocations', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'footprints', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'footprints', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'manufacturers', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'manufacturers', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'attachment_types', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'attachment_types', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'currencies', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'currencies', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'measurement_units', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'measurement_units', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'suppliers', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'suppliers', PermissionData::ALLOW, ['import']);
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'projects', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'projects', PermissionData::ALLOW, ['import']);
//Attachments permissions //Attachments permissions
$this->permissionResolver->setPermission($permHolder, 'attachments', 'show_private', PermissionData::ALLOW); $this->permissionResolver->setPermission($permHolder, 'attachments', 'show_private', PermissionData::ALLOW);

View file

@ -179,7 +179,7 @@
<hr> <hr>
<fieldset> <fieldset>
<legend>{% trans %}export_all.label{% endtrans %}</legend> <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> </fieldset>
</div> </div>

View file

@ -63,6 +63,12 @@
<optgroup label="{% trans %}part_list.action.action.delete{% endtrans %}"> <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> <option {% if not is_granted('@parts.delete') %}disabled{% endif %} value="delete">{% trans %}part_list.action.action.delete{% endtrans %}</option>
</optgroup> </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>
<select class="form-select d-none" data-controller="elements--structural-entity-select" name="target" {{ stimulus_target('elements/datatables/parts', 'selectTargetPicker') }}> <select class="form-select d-none" data-controller="elements--structural-entity-select" name="target" {{ stimulus_target('elements/datatables/parts', 'selectTargetPicker') }}>

View file

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

@ -94,6 +94,9 @@ class ApplicationAvailabilityFunctionalTest extends WebTestCase
yield ['/part/new']; yield ['/part/new'];
yield ['/part/new?category=1&footprint=1&manufacturer=1&storelocation=1&supplier=1']; yield ['/part/new?category=1&footprint=1&manufacturer=1&storelocation=1&supplier=1'];
//Parts import
yield ['/parts/import'];
//Statistics //Statistics
yield ['/statistics']; yield ['/statistics'];

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,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; namespace App\Tests\Services\ImportExportSystem;
use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentType;
use App\Entity\Parts\Category;
use App\Entity\Parts\Part;
use App\Entity\UserSystem\User; use App\Entity\UserSystem\User;
use App\Services\Formatters\AmountFormatter; use App\Services\Formatters\AmountFormatter;
use App\Services\ImportExportSystem\EntityImporter; use App\Services\ImportExportSystem\EntityImporter;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Validator\ConstraintViolation;
/** /**
* @group DB * @group DB
@ -151,4 +154,95 @@ EOT;
$this->assertCount(2, $errors); $this->assertCount(2, $errors);
$this->assertSame('Node 1', $errors[0]['entity']->getName()); $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')); $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 public function testEnsureCorrectSetOperations(): void
{ {
//Create an empty user (all permissions are inherit) //Create an empty user (all permissions are inherit)

View file

@ -6673,16 +6673,6 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr
<target>Ein Kindelement kann nicht das übergeordnete Element sein!</target> <target>Ein Kindelement kann nicht das übergeordnete Element sein!</target>
</segment> </segment>
</unit> </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="nd207H6" name="validator.part_lot.location_full.no_increasment"> <unit id="nd207H6" name="validator.part_lot.location_full.no_increasment">
<notes> <notes>
<note priority="1">obsolete</note> <note priority="1">obsolete</note>

View file

@ -6674,16 +6674,6 @@ If you have done this incorrectly or if a computer is no longer trusted, you can
<target>The parent can not be one of the children of itself.</target> <target>The parent can not be one of the children of itself.</target>
</segment> </segment>
</unit> </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>
<unit id="nd207H6" name="validator.part_lot.location_full.no_increasment"> <unit id="nd207H6" name="validator.part_lot.location_full.no_increasment">
<notes> <notes>
<note priority="1">obsolete</note> <note priority="1">obsolete</note>
@ -10970,34 +10960,178 @@ Element 3</target>
</segment> </segment>
</unit> </unit>
<unit id="Qt585vm" name="attachment.max_file_size"> <unit id="Qt585vm" name="attachment.max_file_size">
<segment state="translated"> <segment>
<source>attachment.max_file_size</source> <source>attachment.max_file_size</source>
<target>Maximum file size</target> <target>Maximum file size</target>
</segment> </segment>
</unit> </unit>
<unit id="tkkbiag" name="user.saml_user"> <unit id="tkkbiag" name="user.saml_user">
<segment state="translated"> <segment>
<source>user.saml_user</source> <source>user.saml_user</source>
<target>SSO / SAML user</target> <target>SSO / SAML user</target>
</segment> </segment>
</unit> </unit>
<unit id="fhepjKr" name="user.saml_user.pw_change_hint"> <unit id="fhepjKr" name="user.saml_user.pw_change_hint">
<segment state="translated"> <segment>
<source>user.saml_user.pw_change_hint</source> <source>user.saml_user.pw_change_hint</source>
<target>Your user uses single sign-on (SSO). You can not change the password and 2FA settings here. Configure them on your central SSO provider instead!</target> <target>Your user uses single sign-on (SSO). You can not change the password and 2FA settings here. Configure them on your central SSO provider instead!</target>
</segment> </segment>
</unit> </unit>
<unit id="32beTBH" name="login.sso_saml_login"> <unit id="32beTBH" name="login.sso_saml_login">
<segment state="translated"> <segment>
<source>login.sso_saml_login</source> <source>login.sso_saml_login</source>
<target>Single Sign-On Login (SSO)</target> <target>Single Sign-On Login (SSO)</target>
</segment> </segment>
</unit> </unit>
<unit id="wnMLanX" name="login.local_login_hint"> <unit id="wnMLanX" name="login.local_login_hint">
<segment state="translated"> <segment>
<source>login.local_login_hint</source> <source>login.local_login_hint</source>
<target>The form below is only for log in with a local user. If you want to log in via single sign-on, press the button above.</target> <target>The form below is only for log in with a local user. If you want to log in via single sign-on, press the button above.</target>
</segment> </segment>
</unit> </unit>
<unit id="fa76Qc9" name="part_list.action.action.export">
<segment>
<source>part_list.action.action.export</source>
<target>Export parts</target>
</segment>
</unit>
<unit id="OfOI7tn" name="part_list.action.export_json">
<segment>
<source>part_list.action.export_json</source>
<target>Export to JSON</target>
</segment>
</unit>
<unit id="8Y5uz7l" name="part_list.action.export_csv">
<segment>
<source>part_list.action.export_csv</source>
<target>Export to CSV</target>
</segment>
</unit>
<unit id="gtllBTO" name="part_list.action.export_yaml">
<segment>
<source>part_list.action.export_yaml</source>
<target>Export to YAML</target>
</segment>
</unit>
<unit id="IW9wGBS" name="part_list.action.export_xml">
<segment>
<source>part_list.action.export_xml</source>
<target>Export to XML</target>
</segment>
</unit>
<unit id="kCT2Emc" name="parts.import.title">
<segment>
<source>parts.import.title</source>
<target>Import parts</target>
</segment>
</unit>
<unit id="6oVjTY." name="parts.import.errors.title">
<segment>
<source>parts.import.errors.title</source>
<target>Import violations</target>
</segment>
</unit>
<unit id="2sWmr2k" name="parts.import.flash.error">
<segment>
<source>parts.import.flash.error</source>
<target>Errors during import. This is most likely caused by some invalid data.</target>
</segment>
</unit>
<unit id="hJHxH3J" name="parts.import.format.auto">
<segment>
<source>parts.import.format.auto</source>
<target>Automatic (based on file extension)</target>
</segment>
</unit>
<unit id="E1zm0rb" name="parts.import.flash.error.unknown_format">
<segment>
<source>parts.import.flash.error.unknown_format</source>
<target>Could not determine the format from the given file!</target>
</segment>
</unit>
<unit id="y2UaCL7" name="parts.import.flash.error.invalid_file">
<segment>
<source>parts.import.flash.error.invalid_file</source>
<target>File invalid / malformatted. Please check that you have selected the right format!</target>
</segment>
</unit>
<unit id="ih.wsVn" name="parts.import.part_category.label">
<segment>
<source>parts.import.part_category.label</source>
<target>Category override</target>
</segment>
</unit>
<unit id="mLczcax" name="parts.import.part_category.help">
<segment>
<source>parts.import.part_category.help</source>
<target>If you select a value here, all imported parts will be assigned to this category. No matter what was set in the data.</target>
</segment>
</unit>
<unit id="W09n3nV" name="import.create_unknown_datastructures">
<segment>
<source>import.create_unknown_datastructures</source>
<target>Create unknown datastructures</target>
</segment>
</unit>
<unit id="QkzTk.8" name="import.create_unknown_datastructures.help">
<segment>
<source>import.create_unknown_datastructures.help</source>
<target>If this is selected, datastructures (like categories, footprints, etc.) which does not exist in the database yet, will be automatically created. If this is not selected, only existing data structures will be used, and if no matching data structure is found, the part will get assigned nothing</target>
</segment>
</unit>
<unit id="p6fTkCQ" name="import.path_delimiter">
<segment>
<source>import.path_delimiter</source>
<target>Path delimiter</target>
</segment>
</unit>
<unit id="gSn6XRk" name="import.path_delimiter.help">
<segment>
<source>import.path_delimiter.help</source>
<target>The delimiter used to mark different levels in data structure pathes like category, footprint, etc.</target>
</segment>
</unit>
<unit id="aNR2tK9" name="parts.import.help_documentation">
<segment>
<source>parts.import.help_documentation</source>
<target><![CDATA[See the <a href="%link%">documentation</a> for more information on the file format.]]></target>
</segment>
</unit>
<unit id="bOHORjK" name="parts.import.help">
<segment>
<source>parts.import.help</source>
<target>You can import parts from existing files with this tool. The parts will be directly written to database, so please check your file beforehand for correct content before uploading it here.</target>
</segment>
</unit>
<unit id="sweByB7" name="parts.import.flash.success">
<segment>
<source>parts.import.flash.success</source>
<target>Part import successful!</target>
</segment>
</unit>
<unit id="j7NAkR." name="parts.import.errors.imported_entities">
<segment>
<source>parts.import.errors.imported_entities</source>
<target>Imported parts</target>
</segment>
</unit>
<unit id="W7NWPFx" name="perm.import">
<segment>
<source>perm.import</source>
<target>Import data</target>
</segment>
</unit>
<unit id="sYQswdX" name="parts.import.part_needs_review.label">
<segment>
<source>parts.import.part_needs_review.label</source>
<target>Mark all imported parts as "Needs review"</target>
</segment>
</unit>
<unit id="5DhI5ZW" name="parts.import.part_needs_review.help">
<segment>
<source>parts.import.part_needs_review.help</source>
<target>If this option is selected, then all parts will be marked as "Needs review", no matter what was set in the data.</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -8,7 +8,7 @@
</segment> </segment>
</unit> </unit>
<unit id="Dpb9AmY" name="saml.error.cannot_login_local_user_per_saml"> <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> <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> <target>You can not login as local user via SSO! Use your local user password instead.</target>
</segment> </segment>

View file

@ -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> <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> </segment>
</unit> </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> </file>
</xliff> </xliff>

View file

@ -300,7 +300,7 @@
</segment> </segment>
</unit> </unit>
<unit id="m8kMFhf" name="validator.attachment.name_not_blank"> <unit id="m8kMFhf" name="validator.attachment.name_not_blank">
<segment state="translated"> <segment>
<source>validator.attachment.name_not_blank</source> <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> <target>Set a value here, or upload a file to automatically use its filename as name for the attachment.</target>
</segment> </segment>