From 10f013296b66436b1df92c93a970faf0e5699c21 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 19 Mar 2025 08:13:45 +0100 Subject: [PATCH 01/83] =?UTF-8?q?Assemblies=20einf=C3=BChren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elements/assembly_select_controller.js | 70 ++ .../elements/part_select_controller.js | 2 +- ...dont_check_quantity_checkbox_controller.js | 2 +- config/permissions.yaml | 4 + migrations/Version20250304081039.php | 33 + migrations/Version20250304154507.php | 25 + migrations/Version20250310160354.php | 27 + .../Migrations/ConvertBBCodeCommand.php | 2 + .../AdminPages/AssemblyAdminController.php | 80 ++ src/Controller/AssemblyController.php | 302 ++++++++ src/Controller/PartController.php | 24 +- src/Controller/TreeController.php | 14 + src/Controller/TypeaheadController.php | 64 +- .../AssemblyBomEntriesDataTable.php | 209 +++++ .../Helpers/AssemblyDataTableHelper.php | 48 ++ src/DataTables/ProjectBomEntriesDataTable.php | 34 +- src/Entity/AssemblySystem/Assembly.php | 358 +++++++++ .../AssemblySystem/AssemblyBOMEntry.php | 302 ++++++++ src/Entity/Attachments/AssemblyAttachment.php | 48 ++ src/Entity/Attachments/Attachment.php | 5 +- src/Entity/Base/AbstractDBElement.php | 5 +- .../LogSystem/CollectionElementDeleted.php | 5 + src/Entity/LogSystem/LogTargetType.php | 6 + src/Entity/Parameters/AbstractParameter.php | 4 +- src/Entity/Parameters/AssemblyParameter.php | 65 ++ src/Entity/Parts/Part.php | 3 + src/Entity/Parts/PartTraits/AssemblyTrait.php | 83 ++ src/Entity/ProjectSystem/Project.php | 1 + src/Entity/ProjectSystem/ProjectBOMEntry.php | 40 +- src/Form/AdminPages/AssemblyAdminForm.php | 64 ++ src/Form/AdminPages/BaseEntityAdminForm.php | 3 +- .../AssemblySystem/AssemblyAddPartsType.php | 88 +++ .../AssemblyBOMEntryCollectionType.php | 32 + .../AssemblySystem/AssemblyBOMEntryType.php | 90 +++ src/Form/AssemblySystem/AssemblyBuildType.php | 183 +++++ src/Form/Filters/AttachmentFilterType.php | 2 + .../ProjectSystem/ProjectAddPartsType.php | 1 + .../ProjectSystem/ProjectBOMEntryType.php | 15 +- src/Form/ProjectSystem/ProjectBuildType.php | 38 +- src/Form/Type/AssemblySelectType.php | 125 +++ src/Form/Type/PartSelectType.php | 5 +- .../Assemblies/AssemblyBuildRequest.php | 306 ++++++++ src/Helpers/Projects/ProjectBuildRequest.php | 102 ++- src/Repository/AssemblyRepository.php | 69 ++ src/Repository/DBElementRepository.php | 10 + src/Security/Voter/AttachmentVoter.php | 3 + src/Security/Voter/StructureVoter.php | 2 + .../AssemblySystem/AssemblyBuildHelper.php | 154 ++++ .../AssemblyBuildPartHelper.php | 40 + .../Attachments/AssemblyPreviewGenerator.php | 93 +++ .../Attachments/AttachmentSubmitHandler.php | 2 + src/Services/ElementTypeNameGenerator.php | 6 + src/Services/EntityURLGenerator.php | 8 + .../ImportExportSystem/BOMImporter.php | 228 +++++- .../ProjectSystem/ProjectBuildHelper.php | 71 +- src/Services/Trees/ToolsTreeBuilder.php | 7 + src/Services/Trees/TreeViewGenerator.php | 7 + src/Twig/EntityExtension.php | 2 + .../ValidAssemblyBuildRequest.php | 37 + .../ValidAssemblyBuildRequestValidator.php | 84 ++ templates/admin/assembly_admin.html.twig | 62 ++ templates/admin/project_admin.html.twig | 2 +- templates/assemblies/add_parts.html.twig | 22 + templates/assemblies/build/_form.html.twig | 88 +++ templates/assemblies/build/build.html.twig | 40 + templates/assemblies/import_bom.html.twig | 60 ++ templates/assemblies/info/_bom.html.twig | 22 + templates/assemblies/info/_builds.html.twig | 40 + templates/assemblies/info/_info.html.twig | 77 ++ .../assemblies/info/_info_card.html.twig | 133 ++++ .../assemblies/info/_subassemblies.html.twig | 28 + templates/assemblies/info/info.html.twig | 105 +++ .../components/assemblies.macro.html.twig | 8 + templates/components/tree_macros.html.twig | 1 + .../form/collection_types_layout.html.twig | 8 +- ...collection_types_layout_assembly.html.twig | 80 ++ templates/helper.twig | 16 + templates/projects/build/_form.html.twig | 55 +- tests/Entity/Attachments/AttachmentTest.php | 3 + .../Assemblies/AssemblyBuildRequestTest.php | 177 +++++ .../AssemblyBuildHelperTest.php | 117 +++ .../AssemblyBuildPartHelperTest.php | 52 ++ translations/messages.cs.xlf | 700 +++++++++++++++++ translations/messages.da.xlf | 700 +++++++++++++++++ translations/messages.de.xlf | 688 +++++++++++++++++ translations/messages.el.xlf | 688 +++++++++++++++++ translations/messages.en.xlf | 688 +++++++++++++++++ translations/messages.es.xlf | 688 +++++++++++++++++ translations/messages.fr.xlf | 690 ++++++++++++++++- translations/messages.it.xlf | 688 +++++++++++++++++ translations/messages.ja.xlf | 652 ++++++++++++++++ translations/messages.nl.xlf | 724 ++++++++++++++++++ translations/messages.pl.xlf | 688 +++++++++++++++++ translations/messages.ru.xlf | 688 +++++++++++++++++ translations/messages.zh.xlf | 688 +++++++++++++++++ translations/validators.cs.xlf | 24 + translations/validators.da.xlf | 24 + translations/validators.de.xlf | 26 +- translations/validators.el.xlf | 24 + translations/validators.en.xlf | 26 +- translations/validators.fr.xlf | 24 + translations/validators.hr.xlf | 24 + translations/validators.it.xlf | 24 + translations/validators.ja.xlf | 24 + translations/validators.pl.xlf | 24 + translations/validators.ru.xlf | 24 + translations/validators.zh.xlf | 24 + 107 files changed, 14099 insertions(+), 101 deletions(-) create mode 100644 assets/controllers/elements/assembly_select_controller.js create mode 100644 migrations/Version20250304081039.php create mode 100644 migrations/Version20250304154507.php create mode 100644 migrations/Version20250310160354.php create mode 100644 src/Controller/AdminPages/AssemblyAdminController.php create mode 100644 src/Controller/AssemblyController.php create mode 100644 src/DataTables/AssemblyBomEntriesDataTable.php create mode 100644 src/DataTables/Helpers/AssemblyDataTableHelper.php create mode 100644 src/Entity/AssemblySystem/Assembly.php create mode 100644 src/Entity/AssemblySystem/AssemblyBOMEntry.php create mode 100644 src/Entity/Attachments/AssemblyAttachment.php create mode 100644 src/Entity/Parameters/AssemblyParameter.php create mode 100644 src/Entity/Parts/PartTraits/AssemblyTrait.php create mode 100644 src/Form/AdminPages/AssemblyAdminForm.php create mode 100644 src/Form/AssemblySystem/AssemblyAddPartsType.php create mode 100644 src/Form/AssemblySystem/AssemblyBOMEntryCollectionType.php create mode 100644 src/Form/AssemblySystem/AssemblyBOMEntryType.php create mode 100644 src/Form/AssemblySystem/AssemblyBuildType.php create mode 100644 src/Form/Type/AssemblySelectType.php create mode 100644 src/Helpers/Assemblies/AssemblyBuildRequest.php create mode 100644 src/Repository/AssemblyRepository.php create mode 100644 src/Services/AssemblySystem/AssemblyBuildHelper.php create mode 100644 src/Services/AssemblySystem/AssemblyBuildPartHelper.php create mode 100644 src/Services/Attachments/AssemblyPreviewGenerator.php create mode 100644 src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequest.php create mode 100644 src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequestValidator.php create mode 100644 templates/admin/assembly_admin.html.twig create mode 100644 templates/assemblies/add_parts.html.twig create mode 100644 templates/assemblies/build/_form.html.twig create mode 100644 templates/assemblies/build/build.html.twig create mode 100644 templates/assemblies/import_bom.html.twig create mode 100644 templates/assemblies/info/_bom.html.twig create mode 100644 templates/assemblies/info/_builds.html.twig create mode 100644 templates/assemblies/info/_info.html.twig create mode 100644 templates/assemblies/info/_info_card.html.twig create mode 100644 templates/assemblies/info/_subassemblies.html.twig create mode 100644 templates/assemblies/info/info.html.twig create mode 100644 templates/components/assemblies.macro.html.twig create mode 100644 templates/form/collection_types_layout_assembly.html.twig create mode 100644 tests/Helpers/Assemblies/AssemblyBuildRequestTest.php create mode 100644 tests/Services/AssemblySystem/AssemblyBuildHelperTest.php create mode 100644 tests/Services/AssemblySystem/AssemblyBuildPartHelperTest.php diff --git a/assets/controllers/elements/assembly_select_controller.js b/assets/controllers/elements/assembly_select_controller.js new file mode 100644 index 000000000..98702d419 --- /dev/null +++ b/assets/controllers/elements/assembly_select_controller.js @@ -0,0 +1,70 @@ +import {Controller} from "@hotwired/stimulus"; + +import "tom-select/dist/css/tom-select.bootstrap5.css"; +import '../../css/components/tom-select_extensions.css'; +import TomSelect from "tom-select"; +import {marked} from "marked"; + +export default class extends Controller { + _tomSelect; + + connect() { + + let settings = { + allowEmptyOption: true, + plugins: ['dropdown_input', 'clear_button'], + searchField: ["name", "description", "category", "footprint"], + valueField: "id", + labelField: "name", + preload: "focus", + render: { + item: (data, escape) => { + return '' + (data.image ? "" : "") + escape(data.name) + ''; + }, + option: (data, escape) => { + if(data.text) { + return '' + escape(data.text) + ''; + } + + let tmp = '
' + + "
" + + (data.image ? "" : "") + + "
" + + "
" + + '
' + escape(data.name) + '
' + + (data.description ? '

' + marked.parseInline(data.description) + '

' : "") + + (data.category ? '

' + escape(data.category) : ""); + + return tmp + '

' + + '
'; + } + } + }; + + + if (this.element.dataset.autocomplete) { + const base_url = this.element.dataset.autocomplete; + settings.valueField = "id"; + settings.load = (query, callback) => { + const url = base_url.replace('__QUERY__', encodeURIComponent(query)); + + fetch(url) + .then(response => response.json()) + .then(json => {callback(json);}) + .catch(() => { + callback() + }); + }; + + + this._tomSelect = new TomSelect(this.element, settings); + //this._tomSelect.clearOptions(); + } + } + + disconnect() { + super.disconnect(); + //Destroy the TomSelect instance + this._tomSelect.destroy(); + } +} \ No newline at end of file diff --git a/assets/controllers/elements/part_select_controller.js b/assets/controllers/elements/part_select_controller.js index 0658f4b46..2b658d526 100644 --- a/assets/controllers/elements/part_select_controller.js +++ b/assets/controllers/elements/part_select_controller.js @@ -12,7 +12,7 @@ export default class extends Controller { let settings = { allowEmptyOption: true, - plugins: ['dropdown_input'], + plugins: ['dropdown_input', 'clear_button'], searchField: ["name", "description", "category", "footprint"], valueField: "id", labelField: "name", diff --git a/assets/controllers/pages/dont_check_quantity_checkbox_controller.js b/assets/controllers/pages/dont_check_quantity_checkbox_controller.js index 2abd3d77b..f3e8cb900 100644 --- a/assets/controllers/pages/dont_check_quantity_checkbox_controller.js +++ b/assets/controllers/pages/dont_check_quantity_checkbox_controller.js @@ -38,7 +38,7 @@ export default class extends Controller { connect() { //Add event listener to the checkbox - this.getCheckbox().addEventListener('change', this.toggleInputLimits.bind(this)); + this.getCheckbox()?.addEventListener('change', this.toggleInputLimits.bind(this)); } toggleInputLimits() { diff --git a/config/permissions.yaml b/config/permissions.yaml index 8cbd60c3f..8709fdb77 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -121,6 +121,10 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co <<: *PART_CONTAINING label: "perm.projects" + assemblies: + <<: *PART_CONTAINING + label: "perm.assemblies" + attachment_types: <<: *PART_CONTAINING label: "perm.part.attachment_types" diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php new file mode 100644 index 000000000..4e521ade9 --- /dev/null +++ b/migrations/Version20250304081039.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE assemblies (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, order_quantity INT NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts TINYINT(1) NOT NULL, description LONGTEXT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, INDEX IDX_5F3832C0727ACA70 (parent_id), INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('CREATE TABLE assembly_bom_entries (id INT AUTO_INCREMENT NOT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames LONGTEXT NOT NULL, name VARCHAR(255) DEFAULT NULL, comment LONGTEXT NOT NULL, price NUMERIC(11, 5) DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_assembly INT DEFAULT NULL, id_part INT DEFAULT NULL, price_currency_id INT DEFAULT NULL, INDEX IDX_8C74887E2F180363 (id_assembly), INDEX IDX_8C74887EC22F6CC4 (id_part), INDEX IDX_8C74887E3FFDCD60 (price_currency_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id)'); + $this->addSql('ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES `attachments` (id) ON DELETE SET NULL'); + $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E2F180363 FOREIGN KEY (id_assembly) REFERENCES assemblies (id)'); + $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES `parts` (id)'); + $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0727ACA70'); + $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0EA7100A1'); + $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E2F180363'); + $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EC22F6CC4'); + $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E3FFDCD60'); + $this->addSql('DROP TABLE assemblies'); + $this->addSql('DROP TABLE assembly_bom_entries'); + } +} diff --git a/migrations/Version20250304154507.php b/migrations/Version20250304154507.php new file mode 100644 index 000000000..62dcc43c0 --- /dev/null +++ b/migrations/Version20250304154507.php @@ -0,0 +1,25 @@ +addSql('ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON parts (built_assembly_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE `parts` DROP FOREIGN KEY FK_6940A7FECC660B3C'); + $this->addSql('DROP INDEX UNIQ_6940A7FECC660B3C ON `parts`'); + $this->addSql('ALTER TABLE `parts` DROP built_assembly_id'); + } +} diff --git a/migrations/Version20250310160354.php b/migrations/Version20250310160354.php new file mode 100644 index 000000000..542fcac28 --- /dev/null +++ b/migrations/Version20250310160354.php @@ -0,0 +1,27 @@ +addSql('ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e2f180363 TO IDX_8C74887E4AD2039E'); + $this->addSql('ALTER TABLE project_bom_entries ADD id_assembly INT DEFAULT NULL AFTER id_part'); + $this->addSql('ALTER TABLE project_bom_entries ADD CONSTRAINT FK_1AA2DD314AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id)'); + $this->addSql('CREATE INDEX IDX_1AA2DD314AD2039E ON project_bom_entries (id_assembly)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e4ad2039e TO IDX_8C74887E2F180363'); + $this->addSql('ALTER TABLE project_bom_entries DROP FOREIGN KEY FK_1AA2DD314AD2039E'); + $this->addSql('DROP INDEX IDX_1AA2DD314AD2039E ON project_bom_entries'); + $this->addSql('ALTER TABLE project_bom_entries DROP id_assembly'); + } +} diff --git a/src/Command/Migrations/ConvertBBCodeCommand.php b/src/Command/Migrations/ConvertBBCodeCommand.php index 201263ffd..b0c083921 100644 --- a/src/Command/Migrations/ConvertBBCodeCommand.php +++ b/src/Command/Migrations/ConvertBBCodeCommand.php @@ -22,6 +22,7 @@ namespace App\Command\Migrations; +use App\Entity\AssemblySystem\Assembly; use Symfony\Component\Console\Attribute\AsCommand; use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractNamedDBElement; @@ -88,6 +89,7 @@ protected function getTargetsLists(): array AttachmentType::class => ['comment'], StorageLocation::class => ['comment'], Project::class => ['comment'], + Assembly::class => ['comment'], Category::class => ['comment'], Manufacturer::class => ['comment'], MeasurementUnit::class => ['comment'], diff --git a/src/Controller/AdminPages/AssemblyAdminController.php b/src/Controller/AdminPages/AssemblyAdminController.php new file mode 100644 index 000000000..20f640923 --- /dev/null +++ b/src/Controller/AdminPages/AssemblyAdminController.php @@ -0,0 +1,80 @@ +. + */ + +declare(strict_types=1); + +namespace App\Controller\AdminPages; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\AssemblyAttachment; +use App\Entity\Parameters\AssemblyParameter; +use App\Form\AdminPages\AssemblyAdminForm; +use App\Services\ImportExportSystem\EntityExporter; +use App\Services\ImportExportSystem\EntityImporter; +use App\Services\Trees\StructuralElementRecursionHelper; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +#[Route(path: '/assembly')] +class AssemblyAdminController extends BaseAdminController +{ + protected string $entity_class = Assembly::class; + protected string $twig_template = 'admin/assembly_admin.html.twig'; + protected string $form_class = AssemblyAdminForm::class; + protected string $route_base = 'assembly'; + protected string $attachment_class = AssemblyAttachment::class; + protected ?string $parameter_class = AssemblyParameter::class; + + #[Route(path: '/{id}', name: 'assembly_delete', methods: ['DELETE'])] + public function delete(Request $request, Assembly $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse + { + return $this->_delete($request, $entity, $recursionHelper); + } + + #[Route(path: '/{id}/edit/{timestamp}', name: 'assembly_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}/edit', requirements: ['id' => '\d+'])] + public function edit(Assembly $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response + { + return $this->_edit($entity, $request, $em, $timestamp); + } + + #[Route(path: '/new', name: 'assembly_new')] + #[Route(path: '/{id}/clone', name: 'assembly_clone')] + #[Route(path: '/')] + public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Assembly $entity = null): Response + { + return $this->_new($request, $em, $importer, $entity); + } + + #[Route(path: '/export', name: 'assembly_export_all')] + public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response + { + return $this->_exportAll($em, $exporter, $request); + } + + #[Route(path: '/{id}/export', name: 'assembly_export')] + public function exportEntity(Assembly $entity, EntityExporter $exporter, Request $request): Response + { + return $this->_exportEntity($entity, $exporter, $request); + } +} diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php new file mode 100644 index 000000000..9710e9bec --- /dev/null +++ b/src/Controller/AssemblyController.php @@ -0,0 +1,302 @@ +. + */ +namespace App\Controller; + +use App\DataTables\AssemblyBomEntriesDataTable; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Parts\Part; +use App\Form\AssemblySystem\AssemblyAddPartsType; +use App\Form\AssemblySystem\AssemblyBuildType; +use App\Helpers\Assemblies\AssemblyBuildRequest; +use App\Repository\PartRepository; +use App\Services\ImportExportSystem\BOMImporter; +use App\Services\AssemblySystem\AssemblyBuildHelper; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\EntityManagerInterface; +use League\Csv\SyntaxError; +use Omines\DataTablesBundle\DataTableFactory; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +use Symfony\Contracts\Translation\TranslatorInterface; +use function Symfony\Component\Translation\t; + +#[Route(path: '/assembly')] +class AssemblyController extends AbstractController +{ + private PartRepository $partRepository; + + public function __construct( + private readonly DataTableFactory $dataTableFactory, + private readonly EntityManagerInterface $entityManager, + private readonly TranslatorInterface $translator, + ) { + $this->partRepository = $this->entityManager->getRepository(Part::class); + } + + #[Route(path: '/{id}/info', name: 'assembly_info', requirements: ['id' => '\d+'])] + public function info(Assembly $assembly, Request $request, AssemblyBuildHelper $buildHelper): Response + { + $this->denyAccessUnlessGranted('read', $assembly); + + $table = $this->dataTableFactory->createFromType(AssemblyBomEntriesDataTable::class, ['assembly' => $assembly]) + ->handleRequest($request); + + if ($table->isCallback()) { + return $table->getResponse(); + } + + return $this->render('assemblies/info/info.html.twig', [ + 'buildHelper' => $buildHelper, + 'datatable' => $table, + 'assembly' => $assembly, + ]); + } + + #[Route(path: '/{id}/build', name: 'assembly_build', requirements: ['id' => '\d+'])] + public function build(Assembly $assembly, Request $request, AssemblyBuildHelper $buildHelper, EntityManagerInterface $entityManager): Response + { + $this->denyAccessUnlessGranted('read', $assembly); + + //If no number of builds is given (or it is invalid), just assume 1 + $number_of_builds = $request->query->getInt('n', 1); + if ($number_of_builds < 1) { + $number_of_builds = 1; + } + + $assemblyBuildRequest = new AssemblyBuildRequest($assembly, $number_of_builds); + $form = $this->createForm(AssemblyBuildType::class, $assemblyBuildRequest); + + $form->handleRequest($request); + if ($form->isSubmitted()) { + if ($form->isValid()) { + //Ensure that the user can withdraw stock from all parts + $this->denyAccessUnlessGranted('@parts_stock.withdraw'); + + //We have to do a flush already here, so that the newly created partLot gets an ID and can be logged to DB later. + $entityManager->flush(); + $buildHelper->doBuild($assemblyBuildRequest); + $entityManager->flush(); + $this->addFlash('success', 'assembly.build.flash.success'); + + return $this->redirect( + $request->get('_redirect', + $this->generateUrl('assembly_info', ['id' => $assembly->getID()] + ))); + } + + $this->addFlash('error', 'assembly.build.flash.invalid_input'); + } + + return $this->render('assemblies/build/build.html.twig', [ + 'buildHelper' => $buildHelper, + 'assembly' => $assembly, + 'build_request' => $assemblyBuildRequest, + 'number_of_builds' => $number_of_builds, + 'form' => $form, + ]); + } + + #[Route(path: '/{id}/import_bom', name: 'assembly_import_bom', requirements: ['id' => '\d+'])] + public function importBOM(Request $request, EntityManagerInterface $entityManager, Assembly $assembly, + BOMImporter $BOMImporter, ValidatorInterface $validator): Response + { + $this->denyAccessUnlessGranted('edit', $assembly); + + $builder = $this->createFormBuilder(); + $builder->add('file', FileType::class, [ + 'label' => 'import.file', + 'required' => true, + 'attr' => [ + 'accept' => '.csv, .json' + ] + ]); + $builder->add('type', ChoiceType::class, [ + 'label' => 'assembly.bom_import.type', + 'required' => true, + 'choices' => [ + 'assembly.bom_import.type.json' => 'json', + 'assembly.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew', + ] + ]); + $builder->add('clear_existing_bom', CheckboxType::class, [ + 'label' => 'assembly.bom_import.clear_existing_bom', + 'required' => false, + 'data' => false, + 'help' => 'assembly.bom_import.clear_existing_bom.help', + ]); + $builder->add('submit', SubmitType::class, [ + 'label' => 'import.btn', + ]); + + $form = $builder->getForm(); + + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + + //Clear existing BOM entries if requested + if ($form->get('clear_existing_bom')->getData()) { + $assembly->getBomEntries()->clear(); + $entityManager->flush(); + } + + try { + $entries = $BOMImporter->importFileIntoAssembly($form->get('file')->getData(), $assembly, [ + 'type' => $form->get('type')->getData(), + ]); + + //Validate the assembly entries + $errors = $validator->validateProperty($assembly, 'bom_entries'); + + //If no validation errors occured, save the changes and redirect to edit page + if (count ($errors) === 0) { + foreach ($entries as $entry) { + if ($entry instanceof AssemblyBOMEntry && $entry->getPart() !== null) { + $part = $entry->getPart(); + if ($part->getID() === null) { + $this->partRepository->save($part); + } + } + } + + $this->addFlash('success', t('assembly.bom_import.flash.success', ['%count%' => count($entries)])); + $entityManager->flush(); + return $this->redirectToRoute('assembly_edit', ['id' => $assembly->getID()]); + } + + //When we get here, there were validation errors + $this->addFlash('error', t('assembly.bom_import.flash.invalid_entries')); + + } catch (\UnexpectedValueException|\RuntimeException|SyntaxError $e) { + $this->addFlash('error', t('assembly.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()])); + } + } + + $jsonTemplate = [ + [ + "quantity" => 1.0, + "name" => $this->translator->trans('assembly.bom_import.template.entry.name'), + "part" => [ + "id" => null, + "ipn" => $this->translator->trans('assembly.bom_import.template.entry.part.ipn'), + "mpnr" => $this->translator->trans('assembly.bom_import.template.entry.part.mpnr'), + "name" => $this->translator->trans('assembly.bom_import.template.entry.part.name'), + "description" => null, + "manufacturer" => [ + "id" => null, + "name" => $this->translator->trans('assembly.bom_import.template.entry.part.manufacturer.name') + ], + "category" => [ + "id" => null, + "name" => $this->translator->trans('assembly.bom_import.template.entry.part.category.name') + ] + ] + ] + ]; + + return $this->render('assemblies/import_bom.html.twig', [ + 'assembly' => $assembly, + 'jsonTemplate' => $jsonTemplate, + 'form' => $form, + 'errors' => $errors ?? null, + ]); + } + + #[Route(path: '/add_parts', name: 'assembly_add_parts_no_id')] + #[Route(path: '/{id}/add_parts', name: 'assembly_add_parts', requirements: ['id' => '\d+'])] + public function addPart(Request $request, EntityManagerInterface $entityManager, ?Assembly $assembly): Response + { + if($assembly instanceof Assembly) { + $this->denyAccessUnlessGranted('edit', $assembly); + } else { + $this->denyAccessUnlessGranted('@assemblies.edit'); + } + + $form = $this->createForm(AssemblyAddPartsType::class, null, [ + 'assembly' => $assembly, + ]); + + //Preset the BOM entries with the selected parts, when the form was not submitted yet + $preset_data = new ArrayCollection(); + foreach (explode(',', (string) $request->get('parts', '')) as $part_id) { + //Skip empty part IDs. Postgres seems to be especially sensitive to empty strings, as it does not allow them in integer columns + if ($part_id === '') { + continue; + } + + $part = $entityManager->getRepository(Part::class)->find($part_id); + if (null !== $part) { + //If there is already a BOM entry for this part, we use this one (we edit it then) + $bom_entry = $entityManager->getRepository(AssemblyBOMEntry::class)->findOneBy([ + 'assembly' => $assembly, + 'part' => $part + ]); + if ($bom_entry !== null) { + $preset_data->add($bom_entry); + } else { //Otherwise create an empty one + $entry = new AssemblyBOMEntry(); + $entry->setAssembly($assembly); + $entry->setPart($part); + $preset_data->add($entry); + } + } + } + $form['bom_entries']->setData($preset_data); + + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $target_assembly = $assembly ?? $form->get('assembly')->getData(); + + //Ensure that we really have acces to the selected assembly + $this->denyAccessUnlessGranted('edit', $target_assembly); + + $data = $form->getData(); + $bom_entries = $data['bom_entries']; + foreach ($bom_entries as $bom_entry){ + $target_assembly->addBOMEntry($bom_entry); + } + + $entityManager->flush(); + + //If a redirect query parameter is set, redirect to this page + if ($request->query->get('_redirect')) { + return $this->redirect($request->query->get('_redirect')); + } + //Otherwise just show the assembly info page + return $this->redirectToRoute('assembly_info', ['id' => $target_assembly->getID()]); + } + + return $this->render('assemblies/add_parts.html.twig', [ + 'assembly' => $assembly, + 'form' => $form, + ]); + } +} diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 6708ed4cd..9e7e61b03 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -23,6 +23,7 @@ namespace App\Controller; use App\DataTables\LogDataTable; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\AttachmentUpload; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -45,6 +46,7 @@ use App\Services\Parameters\ParameterExtractor; use App\Services\Parts\PartLotWithdrawAddHelper; use App\Services\Parts\PricedetailHelper; +use App\Services\AssemblySystem\AssemblyBuildPartHelper; use App\Services\ProjectSystem\ProjectBuildPartHelper; use App\Settings\BehaviorSettings\PartInfoSettings; use DateTime; @@ -158,11 +160,15 @@ public function delete(Request $request, Part $part): RedirectResponse #[Route(path: '/new', name: 'part_new')] #[Route(path: '/{id}/clone', name: 'part_clone')] - #[Route(path: '/new_build_part/{project_id}', name: 'part_new_build_part')] - public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator, - AttachmentSubmitHandler $attachmentSubmitHandler, ProjectBuildPartHelper $projectBuildPartHelper, - #[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null, - #[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null): Response + #[Route(path: '/new_build_part_project/{project_id}', name: 'part_new_build_part_project')] + #[Route(path: '/new_build_part_assembly/{assembly_id}', name: 'part_new_build_part_assembly')] + public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator, + AttachmentSubmitHandler $attachmentSubmitHandler, + ProjectBuildPartHelper $projectBuildPartHelper, + AssemblyBuildPartHelper $assemblyBuildPartHelper, + #[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null, + #[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null, + #[MapEntity(mapping: ['assembly_id' => 'id'])] ?Assembly $assembly = null): Response { if ($part instanceof Part) { @@ -176,6 +182,14 @@ public function new(Request $request, EntityManagerInterface $em, TranslatorInte return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]); } $new_part = $projectBuildPartHelper->getPartInitialization($project); + } elseif ($assembly instanceof Assembly) { + //Initialize a new part for a build part from the given assembly + //Ensure that the assembly has not already a build part + if ($project->getBuildPart() instanceof Part) { + $this->addFlash('error', 'part.new_build_part.error.build_part_already_exists'); + return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]); + } + $new_part = $assemblyBuildPartHelper->getPartInitialization($assembly); } else { //Create an empty part from scratch $new_part = new Part(); } diff --git a/src/Controller/TreeController.php b/src/Controller/TreeController.php index 71f8ba5c6..0ba3a1584 100644 --- a/src/Controller/TreeController.php +++ b/src/Controller/TreeController.php @@ -22,6 +22,7 @@ namespace App\Controller; +use App\Entity\AssemblySystem\Assembly; use Symfony\Component\HttpFoundation\Response; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; @@ -129,4 +130,17 @@ public function deviceTree(?Project $device = null): JsonResponse return new JsonResponse($tree); } + + #[Route(path: '/assembly/{id}', name: 'tree_assembly')] + #[Route(path: '/assemblies', name: 'tree_assembly_root')] + public function assemblyTree(?Assembly $assembly = null): JsonResponse + { + if ($this->isGranted('@assemblies.read')) { + $tree = $this->treeGenerator->getTreeView(Assembly::class, $assembly, 'assemblies'); + } else { + return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN); + } + + return new JsonResponse($tree); + } } diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 89eac7ff7..09792951a 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -22,7 +22,9 @@ namespace App\Controller; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Parameters\AbstractParameter; +use App\Services\Attachments\AssemblyPreviewGenerator; use Symfony\Component\HttpFoundation\Response; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Category; @@ -53,6 +55,8 @@ use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; +use Symfony\Contracts\Translation\TranslatorInterface; +use InvalidArgumentException; /** * In this controller the endpoints for the typeaheads are collected. @@ -60,8 +64,11 @@ #[Route(path: '/typeahead')] class TypeaheadController extends AbstractController { - public function __construct(protected AttachmentURLGenerator $urlGenerator, protected Packages $assets) - { + public function __construct( + protected AttachmentURLGenerator $urlGenerator, + protected Packages $assets, + protected TranslatorInterface $translator + ) { } #[Route(path: '/builtInResources/search', name: 'typeahead_builtInRessources')] @@ -109,19 +116,22 @@ private function typeToParameterClass(string $type): string 'group' => GroupParameter::class, 'measurement_unit' => MeasurementUnitParameter::class, 'currency' => Currency::class, - default => throw new \InvalidArgumentException('Invalid parameter type: '.$type), + default => throw new InvalidArgumentException('Invalid parameter type: '.$type), }; } #[Route(path: '/parts/search/{query}', name: 'typeahead_parts')] - public function parts(EntityManagerInterface $entityManager, PartPreviewGenerator $previewGenerator, - AttachmentURLGenerator $attachmentURLGenerator, string $query = ""): JsonResponse - { + public function parts( + EntityManagerInterface $entityManager, + PartPreviewGenerator $previewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" + ): JsonResponse { $this->denyAccessUnlessGranted('@parts.read'); - $repo = $entityManager->getRepository(Part::class); + $partRepository = $entityManager->getRepository(Part::class); - $parts = $repo->autocompleteSearch($query, 100); + $parts = $partRepository->autocompleteSearch($query, 100); $data = []; foreach ($parts as $part) { @@ -147,6 +157,44 @@ public function parts(EntityManagerInterface $entityManager, PartPreviewGenerato return new JsonResponse($data); } + #[Route(path: '/assemblies/search/{query}', name: 'typeahead_assemblies')] + public function assemblies( + EntityManagerInterface $entityManager, + AssemblyPreviewGenerator $assemblyPreviewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" + ): JsonResponse { + $this->denyAccessUnlessGranted('@assemblies.read'); + + $result = []; + + $assemblyRepository = $entityManager->getRepository(Assembly::class); + + $assemblies = $assemblyRepository->autocompleteSearch($query, 100); + + foreach ($assemblies as $assembly) { + $preview_attachment = $assemblyPreviewGenerator->getTablePreviewAttachment($assembly); + + if($preview_attachment instanceof Attachment) { + $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); + } else { + $preview_url = ''; + } + + /** @var Assembly $assembly */ + $result[] = [ + 'id' => $assembly->getID(), + 'name' => $this->translator->trans('typeahead.parts.assembly.name', ['%name%' => $assembly->getName()]), + 'category' => '', + 'footprint' => '', + 'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'), + 'image' => $preview_url, + ]; + } + + return new JsonResponse($result); + } + #[Route(path: '/parameters/{type}/search/{query}', name: 'typeahead_parameters', requirements: ['type' => '.+'])] public function parameters(string $type, EntityManagerInterface $entityManager, string $query = ""): JsonResponse { diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php new file mode 100644 index 000000000..7149ed5f5 --- /dev/null +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -0,0 +1,209 @@ +. + */ +namespace App\DataTables; + +use App\DataTables\Column\EntityColumn; +use App\DataTables\Column\LocaleDateTimeColumn; +use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Helpers\PartDataTableHelper; +use App\Entity\Attachments\Attachment; +use App\Entity\Parts\Part; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Services\EntityURLGenerator; +use App\Services\Formatters\AmountFormatter; +use Doctrine\ORM\QueryBuilder; +use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; +use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; +use Omines\DataTablesBundle\Column\TextColumn; +use Omines\DataTablesBundle\DataTable; +use Omines\DataTablesBundle\DataTableTypeInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +class AssemblyBomEntriesDataTable implements DataTableTypeInterface +{ + public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter) + { + } + + + public function configure(DataTable $dataTable, array $options): void + { + $dataTable + //->add('select', SelectColumn::class) + ->add('picture', TextColumn::class, [ + 'label' => '', + 'className' => 'no-colvis', + 'render' => function ($value, AssemblyBOMEntry $context) { + if(!$context->getPart() instanceof Part) { + return ''; + } + return $this->partDataTableHelper->renderPicture($context->getPart()); + }, + ]) + ->add('id', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.id'), + 'visible' => false, + ]) + ->add('quantity', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.bom.quantity'), + 'className' => 'text-center', + 'orderField' => 'bom_entry.quantity', + 'render' => function ($value, AssemblyBOMEntry $context): float|string { + //If we have a non-part entry, only show the rounded quantity + if (!$context->getPart() instanceof Part) { + return round($context->getQuantity()); + } + //Otherwise use the unit of the part to format the quantity + return htmlspecialchars($this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit())); + }, + ]) + ->add('name', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.name'), + 'orderField' => 'NATSORT(part.name)', + 'render' => function ($value, AssemblyBOMEntry $context) { + if(!$context->getPart() instanceof Part) { + return htmlspecialchars((string) $context->getName()); + } + + //Part exists if we reach this point + + $tmp = $this->partDataTableHelper->renderName($context->getPart()); + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
'.htmlspecialchars($context->getName()).''; + } + return $tmp; + }, + ]) + ->add('ipn', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.ipn'), + 'orderField' => 'NATSORT(part.ipn)', + 'visible' => false, + 'render' => function ($value, AssemblyBOMEntry $context) { + if($context->getPart() instanceof Part) { + return $context->getPart()->getIpn(); + } + } + ]) + ->add('description', MarkdownColumn::class, [ + 'label' => $this->translator->trans('part.table.description'), + 'data' => function (AssemblyBOMEntry $context) { + if($context->getPart() instanceof Part) { + return $context->getPart()->getDescription(); + } + //For non-part BOM entries show the comment field + return $context->getComment(); + }, + ]) + ->add('category', EntityColumn::class, [ + 'label' => $this->translator->trans('part.table.category'), + 'property' => 'part.category', + 'orderField' => 'NATSORT(category.name)', + ]) + ->add('footprint', EntityColumn::class, [ + 'property' => 'part.footprint', + 'label' => $this->translator->trans('part.table.footprint'), + 'orderField' => 'NATSORT(footprint.name)', + ]) + ->add('manufacturer', EntityColumn::class, [ + 'property' => 'part.manufacturer', + 'label' => $this->translator->trans('part.table.manufacturer'), + 'orderField' => 'NATSORT(manufacturer.name)', + ]) + ->add('mountnames', TextColumn::class, [ + 'label' => 'assembly.bom.mountnames', + 'render' => function ($value, AssemblyBOMEntry $context) { + $html = ''; + + foreach (explode(',', $context->getMountnames()) as $mountname) { + $html .= sprintf('%s ', htmlspecialchars($mountname)); + } + return $html; + }, + ]) + ->add('instockAmount', TextColumn::class, [ + 'label' => 'assembly.bom.instockAmount', + 'visible' => false, + 'render' => function ($value, AssemblyBOMEntry $context) { + if ($context->getPart() !== null) { + return $this->partDataTableHelper->renderAmount($context->getPart()); + } + + return ''; + } + ]) + ->add('storageLocations', TextColumn::class, [ + 'label' => 'part.table.storeLocations', + 'visible' => false, + 'render' => function ($value, AssemblyBOMEntry $context) { + if ($context->getPart() !== null) { + return $this->partDataTableHelper->renderStorageLocations($context->getPart()); + } + + return ''; + } + ]) + ->add('addedDate', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('part.table.addedDate'), + 'visible' => false, + ]) + ->add('lastModified', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('part.table.lastModified'), + 'visible' => false, + ]) + ; + + $dataTable->addOrderBy('name', DataTable::SORT_ASCENDING); + + $dataTable->createAdapter(ORMAdapter::class, [ + 'entity' => Attachment::class, + 'query' => function (QueryBuilder $builder) use ($options): void { + $this->getQuery($builder, $options); + }, + 'criteria' => [ + function (QueryBuilder $builder) use ($options): void { + $this->buildCriteria($builder, $options); + }, + new SearchCriteriaProvider(), + ], + ]); + } + + private function getQuery(QueryBuilder $builder, array $options): void + { + $builder->select('bom_entry') + ->addSelect('part') + ->from(AssemblyBOMEntry::class, 'bom_entry') + ->leftJoin('bom_entry.part', 'part') + ->leftJoin('part.category', 'category') + ->leftJoin('part.footprint', 'footprint') + ->leftJoin('part.manufacturer', 'manufacturer') + ->where('bom_entry.assembly = :assembly') + ->setParameter('assembly', $options['assembly']) + ; + } + + private function buildCriteria(QueryBuilder $builder, array $options): void + { + + } +} diff --git a/src/DataTables/Helpers/AssemblyDataTableHelper.php b/src/DataTables/Helpers/AssemblyDataTableHelper.php new file mode 100644 index 000000000..36f7836b7 --- /dev/null +++ b/src/DataTables/Helpers/AssemblyDataTableHelper.php @@ -0,0 +1,48 @@ +. + */ + +namespace App\DataTables\Helpers; + +use App\Entity\AssemblySystem\Assembly; +use App\Services\EntityURLGenerator; + +/** + * A helper service which contains common code to render columns for assembly related tables + */ +class AssemblyDataTableHelper +{ + public function __construct(private readonly EntityURLGenerator $entityURLGenerator) { + } + + public function renderName(Assembly $context): string + { + $icon = ''; + + return sprintf( + '%s%s', + $this->entityURLGenerator->infoURL($context), + $icon, + htmlspecialchars($context->getName()) + ); + } +} diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index fcb069844..1c7a09e49 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -25,7 +25,9 @@ use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Helpers\AssemblyDataTableHelper; use App\DataTables\Helpers\PartDataTableHelper; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\ProjectBOMEntry; @@ -41,11 +43,15 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface { - public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter) - { + public function __construct( + protected TranslatorInterface $translator, + protected PartDataTableHelper $partDataTableHelper, + protected AssemblyDataTableHelper $assemblyDataTableHelper, + protected EntityURLGenerator $entityURLGenerator, + protected AmountFormatter $amountFormatter + ) { } - public function configure(DataTable $dataTable, array $options): void { $dataTable @@ -84,16 +90,26 @@ public function configure(DataTable $dataTable, array $options): void 'label' => $this->translator->trans('part.table.name'), 'orderField' => 'NATSORT(part.name)', 'render' => function ($value, ProjectBOMEntry $context) { - if(!$context->getPart() instanceof Part) { + if(!$context->getPart() instanceof Part && !$context->getAssembly() instanceof Assembly) { return htmlspecialchars((string) $context->getName()); } - //Part exists if we reach this point - - $tmp = $this->partDataTableHelper->renderName($context->getPart()); - if($context->getName() !== null && $context->getName() !== '') { - $tmp .= '
'.htmlspecialchars($context->getName()).''; + if ($context->getPart() !== null) { + $tmp = $this->partDataTableHelper->renderName($context->getPart()); + $tmp = $this->translator->trans('part.table.name.value.for_part', ['%value%' => $tmp]); + + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
'.htmlspecialchars($context->getName()).''; + } + } elseif ($context->getAssembly() !== null) { + $tmp = $this->assemblyDataTableHelper->renderName($context->getAssembly()); + $tmp = $this->translator->trans('part.table.name.value.for_assembly', ['%value%' => $tmp]); + + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
'.htmlspecialchars($context->getName()).''; + } } + return $tmp; }, ]) diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php new file mode 100644 index 000000000..17a6868f0 --- /dev/null +++ b/src/Entity/AssemblySystem/Assembly.php @@ -0,0 +1,358 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\AssemblySystem; + +use App\Repository\AssemblyRepository; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Attachments\Attachment; +use App\Validator\Constraints\UniqueObjectCollection; +use Doctrine\DBAL\Types\Types; +use App\Entity\Attachments\AssemblyAttachment; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Parameters\AssemblyParameter; +use App\Entity\Parts\Part; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use InvalidArgumentException; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; + +/** + * This class represents a assembly in the database. + * + * @extends AbstractStructuralDBElement + */ +#[ORM\Entity(repositoryClass: AssemblyRepository::class)] +#[ORM\Table(name: 'assemblies')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@assemblies.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['assembly:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['assembly:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/assemblies/{id}/children.{_format}', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'Retrieves the children elements of a assembly.'), + security: 'is_granted("@assemblies.read")' + ) + ], + uriVariables: [ + 'id' => new Link(fromProperty: 'children', fromClass: Assembly::class) + ], + normalizationContext: ['groups' => ['assembly:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] +class Assembly extends AbstractStructuralDBElement +{ + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; + + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['assembly:read', 'assembly:write'])] + #[ApiProperty(readableLink: false, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; + + #[Groups(['assembly:read', 'assembly:write'])] + protected string $comment = ''; + + /** + * @var Collection + */ + #[Assert\Valid] + #[Groups(['extended', 'full', 'import'])] + #[ORM\OneToMany(mappedBy: 'assembly', targetEntity: AssemblyBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])] + #[UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name'])] + protected Collection $bom_entries; + + #[ORM\Column(type: Types::INTEGER)] + protected int $order_quantity = 0; + + /** + * @var string|null The current status of the assembly + */ + #[Assert\Choice(['draft', 'planning', 'in_production', 'finished', 'archived'])] + #[Groups(['extended', 'full', 'assembly:read', 'assembly:write', 'import'])] + #[ORM\Column(type: Types::STRING, length: 64, nullable: true)] + protected ?string $status = null; + + + /** + * @var Part|null The (optional) part that represents the builds of this assembly in the stock + */ + #[ORM\OneToOne(mappedBy: 'built_assembly', targetEntity: Part::class, cascade: ['persist'], orphanRemoval: true)] + #[Groups(['assembly:read', 'assembly:write'])] + protected ?Part $build_part = null; + + #[ORM\Column(type: Types::BOOLEAN)] + protected bool $order_only_missing_parts = false; + + #[Groups(['simple', 'extended', 'full', 'assembly:read', 'assembly:write'])] + #[ORM\Column(type: Types::TEXT)] + protected string $description = ''; + + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'element', targetEntity: AssemblyAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + #[Groups(['assembly:read', 'assembly:write'])] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: AssemblyAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['assembly:read', 'assembly:write'])] + protected ?Attachment $master_picture_attachment = null; + + /** @var Collection + */ + #[ORM\OneToMany(mappedBy: 'element', targetEntity: AssemblyParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] + #[Groups(['assembly:read', 'assembly:write'])] + protected Collection $parameters; + + #[Groups(['assembly:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['assembly:read'])] + protected ?\DateTimeImmutable $lastModified = null; + + + /******************************************************************************** + * + * Getters + * + *********************************************************************************/ + + public function __construct() + { + $this->attachments = new ArrayCollection(); + $this->parameters = new ArrayCollection(); + parent::__construct(); + $this->bom_entries = new ArrayCollection(); + $this->children = new ArrayCollection(); + } + + public function __clone() + { + //When cloning this assembly, we have to clone each bom entry too. + if ($this->id) { + $bom_entries = $this->bom_entries; + $this->bom_entries = new ArrayCollection(); + //Set master attachment is needed + foreach ($bom_entries as $bom_entry) { + $clone = clone $bom_entry; + $this->addBomEntry($clone); + } + } + + //Parent has to be last call, as it resets the ID + parent::__clone(); + } + + /** + * Get the order quantity of this assembly. + * + * @return int the order quantity + */ + public function getOrderQuantity(): int + { + return $this->order_quantity; + } + + /** + * Get the "order_only_missing_parts" attribute. + * + * @return bool the "order_only_missing_parts" attribute + */ + public function getOrderOnlyMissingParts(): bool + { + return $this->order_only_missing_parts; + } + + /******************************************************************************** + * + * Setters + * + *********************************************************************************/ + + /** + * Set the order quantity. + * + * @param int $new_order_quantity the new order quantity + * + * @return $this + */ + public function setOrderQuantity(int $new_order_quantity): self + { + if ($new_order_quantity < 0) { + throw new InvalidArgumentException('The new order quantity must not be negative!'); + } + $this->order_quantity = $new_order_quantity; + + return $this; + } + + /** + * Set the "order_only_missing_parts" attribute. + * + * @param bool $new_order_only_missing_parts the new "order_only_missing_parts" attribute + */ + public function setOrderOnlyMissingParts(bool $new_order_only_missing_parts): self + { + $this->order_only_missing_parts = $new_order_only_missing_parts; + + return $this; + } + + public function getBomEntries(): Collection + { + return $this->bom_entries; + } + + /** + * @return $this + */ + public function addBomEntry(AssemblyBOMEntry $entry): self + { + $entry->setAssembly($this); + $this->bom_entries->add($entry); + return $this; + } + + /** + * @return $this + */ + public function removeBomEntry(AssemblyBOMEntry $entry): self + { + $this->bom_entries->removeElement($entry); + return $this; + } + + public function getDescription(): string + { + return $this->description; + } + + public function setDescription(string $description): Assembly + { + $this->description = $description; + return $this; + } + + /** + * @return string + */ + public function getStatus(): ?string + { + return $this->status; + } + + /** + * @param string $status + */ + public function setStatus(?string $status): void + { + $this->status = $status; + } + + /** + * Checks if this assembly has an associated part representing the builds of this assembly in the stock. + */ + public function hasBuildPart(): bool + { + return $this->build_part instanceof Part; + } + + /** + * Gets the part representing the builds of this assembly in the stock, if it is existing + */ + public function getBuildPart(): ?Part + { + return $this->build_part; + } + + /** + * Sets the part representing the builds of this assembly in the stock. + */ + public function setBuildPart(?Part $build_part): void + { + $this->build_part = $build_part; + if ($build_part instanceof Part) { + $build_part->setBuiltAssembly($this); + } + } + + #[Assert\Callback] + public function validate(ExecutionContextInterface $context, $payload): void + { + //If this assembly has subassemblies, and these have builds part, they must be included in the BOM + foreach ($this->getChildren() as $child) { + if (!$child->getBuildPart() instanceof Part) { + continue; + } + //We have to search all bom entries for the build part + $found = false; + foreach ($this->getBomEntries() as $bom_entry) { + if ($bom_entry->getPart() === $child->getBuildPart()) { + $found = true; + break; + } + } + + //When the build part is not found, we have to add an error + if (!$found) { + $context->buildViolation('assembly.bom_has_to_include_all_subelement_parts') + ->atPath('bom_entries') + ->setParameter('%assembly_name%', $child->getName()) + ->setParameter('%part_name%', $child->getBuildPart()->getName()) + ->addViolation(); + } + } + } +} diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php new file mode 100644 index 000000000..375fef040 --- /dev/null +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -0,0 +1,302 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\AssemblySystem; + +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Doctrine\Orm\Filter\RangeFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Contracts\TimeStampableInterface; +use App\Entity\AssemblySystem\Assembly; +use App\Repository\DBElementRepository; +use App\Validator\UniqueValidatableInterface; +use Doctrine\DBAL\Types\Types; +use App\Entity\Base\AbstractDBElement; +use App\Entity\Base\TimestampTrait; +use App\Entity\Parts\Part; +use App\Entity\PriceInformations\Currency; +use App\Validator\Constraints\BigDecimal\BigDecimalPositive; +use App\Validator\Constraints\Selectable; +use Brick\Math\BigDecimal; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; + +/** + * The AssemblyBOMEntry class represents an entry in a assembly's BOM. + */ +#[ORM\HasLifecycleCallbacks] +#[ORM\Entity(repositoryClass: DBElementRepository::class)] +#[ORM\Table('assembly_bom_entries')] +#[ApiResource( + operations: [ + new Get(uriTemplate: '/assembly_bom_entries/{id}.{_format}', security: 'is_granted("read", object)',), + new GetCollection(uriTemplate: '/assembly_bom_entries.{_format}', security: 'is_granted("@assemblies.read")',), + new Post(uriTemplate: '/assembly_bom_entries.{_format}', securityPostDenormalize: 'is_granted("create", object)',), + new Patch(uriTemplate: '/assembly_bom_entries/{id}.{_format}', security: 'is_granted("edit", object)',), + new Delete(uriTemplate: '/assembly_bom_entries/{id}.{_format}', security: 'is_granted("delete", object)',), + ], + normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['bom_entry:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/assemblies/{id}/bom.{_format}', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'Retrieves the BOM entries of the given assembly.'), + security: 'is_granted("@assemblies.read")' + ) + ], + uriVariables: [ + 'id' => new Link(fromProperty: 'bom_entries', fromClass: Assembly::class) + ], + normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])] +#[ApiFilter(RangeFilter::class, properties: ['quantity'])] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified', 'quantity'])] +class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInterface, TimeStampableInterface +{ + use TimestampTrait; + + #[Assert\Positive] + #[ORM\Column(name: 'quantity', type: Types::FLOAT)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] + protected float $quantity = 1.0; + + /** + * @var string A comma separated list of the names, where this parts should be placed + */ + #[ORM\Column(name: 'mountnames', type: Types::TEXT)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] + protected string $mountnames = ''; + + /** + * @var string|null An optional name describing this BOM entry (useful for non-part entries) + */ + #[Assert\Expression('this.getPart() !== null or this.getName() !== null', message: 'validator.assembly.bom_entry.name_or_part_needed')] + #[ORM\Column(type: Types::STRING, nullable: true)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] + protected ?string $name = null; + + /** + * @var string An optional comment for this BOM entry + */ + #[ORM\Column(type: Types::TEXT)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full'])] + protected string $comment = ''; + + /** + * @var Assembly|null + */ + #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'bom_entries')] + #[ORM\JoinColumn(name: 'id_assembly', nullable: true)] + #[Groups(['bom_entry:read', 'bom_entry:write', ])] + protected ?Assembly $assembly = null; + + /** + * @var Part|null The part associated with this + */ + #[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'assembly_bom_entries')] + #[ORM\JoinColumn(name: 'id_part')] + #[Groups(['bom_entry:read', 'bom_entry:write', 'full'])] + protected ?Part $part = null; + + /** + * @var BigDecimal|null The price of this non-part BOM entry + */ + #[Assert\AtLeastOneOf([new BigDecimalPositive(), new Assert\IsNull()])] + #[ORM\Column(type: 'big_decimal', precision: 11, scale: 5, nullable: true)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full'])] + protected ?BigDecimal $price = null; + + /** + * @var ?Currency The currency for the price of this non-part BOM entry + */ + #[ORM\ManyToOne(targetEntity: Currency::class)] + #[ORM\JoinColumn] + #[Selectable] + protected ?Currency $price_currency = null; + + public function __construct() + { + } + + public function getQuantity(): float + { + return $this->quantity; + } + + public function setQuantity(float $quantity): AssemblyBOMEntry + { + $this->quantity = $quantity; + return $this; + } + + public function getMountnames(): string + { + return $this->mountnames; + } + + public function setMountnames(string $mountnames): AssemblyBOMEntry + { + $this->mountnames = $mountnames; + return $this; + } + + /** + * @return string + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName(?string $name): AssemblyBOMEntry + { + $this->name = $name; + return $this; + } + + public function getComment(): string + { + return $this->comment; + } + + public function setComment(string $comment): AssemblyBOMEntry + { + $this->comment = $comment; + return $this; + } + + public function getAssembly(): ?Assembly + { + return $this->assembly; + } + + public function setAssembly(?Assembly $assembly): AssemblyBOMEntry + { + $this->assembly = $assembly; + return $this; + } + + public function getPart(): ?Part + { + return $this->part; + } + + public function setPart(?Part $part): AssemblyBOMEntry + { + $this->part = $part; + return $this; + } + + /** + * Returns the price of this BOM entry, if existing. + * Prices are only valid on non-Part BOM entries. + */ + public function getPrice(): ?BigDecimal + { + return $this->price; + } + + /** + * Sets the price of this BOM entry. + * Prices are only valid on non-Part BOM entries. + */ + public function setPrice(?BigDecimal $price): void + { + $this->price = $price; + } + + public function getPriceCurrency(): ?Currency + { + return $this->price_currency; + } + + public function setPriceCurrency(?Currency $price_currency): void + { + $this->price_currency = $price_currency; + } + + /** + * Checks whether this BOM entry is a part associated BOM entry or not. + * @return bool True if this BOM entry is a part associated BOM entry, false otherwise. + */ + public function isPartBomEntry(): bool + { + return $this->part instanceof Part; + } + + #[Assert\Callback] + public function validate(ExecutionContextInterface $context, $payload): void + { + //Round quantity to whole numbers, if the part is not a decimal part + if ($this->part instanceof Part && (!$this->part->getPartUnit() || $this->part->getPartUnit()->isInteger())) { + $this->quantity = round($this->quantity); + } + //Non-Part BOM entries are rounded + if (!$this->part instanceof Part) { + $this->quantity = round($this->quantity); + } + + //Check that the part is not the build representation part of this assembly or one of its parents + if ($this->part && $this->part->getBuiltAssembly() instanceof Assembly) { + //Get the associated assembly + $associated_assembly = $this->part->getBuiltAssembly(); + //Check that it is not the same as the current assembly neither one of its parents + $current_assembly = $this->assembly; + while ($current_assembly) { + if ($associated_assembly === $current_assembly) { + $context->buildViolation('assembly.bom_entry.can_not_add_own_builds_part') + ->atPath('part') + ->addViolation(); + } + $current_assembly = $current_assembly->getParent(); + } + } + } + + + public function getComparableFields(): array + { + return [ + 'name' => $this->getName(), + 'part' => $this->getPart()?->getID(), + ]; + } +} diff --git a/src/Entity/Attachments/AssemblyAttachment.php b/src/Entity/Attachments/AssemblyAttachment.php new file mode 100644 index 000000000..bb9a11c8f --- /dev/null +++ b/src/Entity/Attachments/AssemblyAttachment.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Attachments; + +use App\Entity\AssemblySystem\Assembly; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; + +/** + * A attachment attached to a device element. + * @extends Attachment + */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] +class AssemblyAttachment extends Attachment +{ + final public const ALLOWED_ELEMENT_CLASS = Assembly::class; + /** + * @var Assembly|null the element this attachment is associated with + */ + #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AttachmentContainingDBElement $element = null; +} diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index 00cf581a8..808c60623 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -97,7 +97,7 @@ #[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)] abstract class Attachment extends AbstractNamedDBElement { - private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class, + private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class, 'Assembly' => AssemblyAttachment::class, 'AttachmentType' => AttachmentTypeAttachment::class, 'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class, 'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class, @@ -107,7 +107,8 @@ abstract class Attachment extends AbstractNamedDBElement /* * The discriminator map used for API platform. The key should be the same as the api platform short type (the @type JSONLD field). */ - private const API_DISCRIMINATOR_MAP = ["Part" => PartAttachment::class, "Project" => ProjectAttachment::class, "AttachmentType" => AttachmentTypeAttachment::class, + private const API_DISCRIMINATOR_MAP = ["Part" => PartAttachment::class, "Project" => ProjectAttachment::class, "Assembly" => AssemblyAttachment::class, + "AttachmentType" => AttachmentTypeAttachment::class, "Category" => CategoryAttachment::class, "Footprint" => FootprintAttachment::class, "Manufacturer" => ManufacturerAttachment::class, "Currency" => CurrencyAttachment::class, "Group" => GroupAttachment::class, "MeasurementUnit" => MeasurementUnitAttachment::class, "StorageLocation" => StorageLocationAttachment::class, "Supplier" => SupplierAttachment::class, "User" => UserAttachment::class, "LabelProfile" => LabelAttachment::class]; diff --git a/src/Entity/Base/AbstractDBElement.php b/src/Entity/Base/AbstractDBElement.php index 9fb5d6489..ad9c534b0 100644 --- a/src/Entity/Base/AbstractDBElement.php +++ b/src/Entity/Base/AbstractDBElement.php @@ -22,6 +22,9 @@ namespace App\Entity\Base; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentTypeAttachment; @@ -68,7 +71,7 @@ * Every database table which are managed with this class (or a subclass of it) * must have the table row "id"!! The ID is the unique key to identify the elements. */ -#[DiscriminatorMap(typeProperty: 'type', mapping: ['attachment_type' => AttachmentType::class, 'attachment' => Attachment::class, 'attachment_type_attachment' => AttachmentTypeAttachment::class, 'category_attachment' => CategoryAttachment::class, 'currency_attachment' => CurrencyAttachment::class, 'footprint_attachment' => FootprintAttachment::class, 'group_attachment' => GroupAttachment::class, 'label_attachment' => LabelAttachment::class, 'manufacturer_attachment' => ManufacturerAttachment::class, 'measurement_unit_attachment' => MeasurementUnitAttachment::class, 'part_attachment' => PartAttachment::class, 'project_attachment' => ProjectAttachment::class, 'storelocation_attachment' => StorageLocationAttachment::class, 'supplier_attachment' => SupplierAttachment::class, 'user_attachment' => UserAttachment::class, 'category' => Category::class, 'project' => Project::class, 'project_bom_entry' => ProjectBOMEntry::class, 'footprint' => Footprint::class, 'group' => Group::class, 'manufacturer' => Manufacturer::class, 'orderdetail' => Orderdetail::class, 'part' => Part::class, 'pricedetail' => Pricedetail::class, 'storelocation' => StorageLocation::class, 'part_lot' => PartLot::class, 'currency' => Currency::class, 'measurement_unit' => MeasurementUnit::class, 'parameter' => AbstractParameter::class, 'supplier' => Supplier::class, 'user' => User::class])] +#[DiscriminatorMap(typeProperty: 'type', mapping: ['attachment_type' => AttachmentType::class, 'attachment' => Attachment::class, 'attachment_type_attachment' => AttachmentTypeAttachment::class, 'category_attachment' => CategoryAttachment::class, 'currency_attachment' => CurrencyAttachment::class, 'footprint_attachment' => FootprintAttachment::class, 'group_attachment' => GroupAttachment::class, 'label_attachment' => LabelAttachment::class, 'manufacturer_attachment' => ManufacturerAttachment::class, 'measurement_unit_attachment' => MeasurementUnitAttachment::class, 'part_attachment' => PartAttachment::class, 'project_attachment' => ProjectAttachment::class, 'assembly_attachment' => AssemblyAttachment::class, 'storelocation_attachment' => StorageLocationAttachment::class, 'supplier_attachment' => SupplierAttachment::class, 'user_attachment' => UserAttachment::class, 'category' => Category::class, 'project' => Project::class, 'project_bom_entry' => ProjectBOMEntry::class, 'assembly' => Assembly::class, 'assembly_bom_entry' => AssemblyBOMEntry::class, 'footprint' => Footprint::class, 'group' => Group::class, 'manufacturer' => Manufacturer::class, 'orderdetail' => Orderdetail::class, 'part' => Part::class, 'pricedetail' => Pricedetail::class, 'storelocation' => StorageLocation::class, 'part_lot' => PartLot::class, 'currency' => Currency::class, 'measurement_unit' => MeasurementUnit::class, 'parameter' => AbstractParameter::class, 'supplier' => Supplier::class, 'user' => User::class])] #[ORM\MappedSuperclass(repositoryClass: DBElementRepository::class)] abstract class AbstractDBElement implements JsonSerializable { diff --git a/src/Entity/LogSystem/CollectionElementDeleted.php b/src/Entity/LogSystem/CollectionElementDeleted.php index 16bf33f59..be19bb0c8 100644 --- a/src/Entity/LogSystem/CollectionElementDeleted.php +++ b/src/Entity/LogSystem/CollectionElementDeleted.php @@ -41,6 +41,8 @@ namespace App\Entity\LogSystem; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentTypeAttachment; @@ -58,6 +60,7 @@ use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\LogWithEventUndoInterface; use App\Entity\Contracts\NamedElementInterface; +use App\Entity\Parameters\AssemblyParameter; use App\Entity\ProjectSystem\Project; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parameters\AttachmentTypeParameter; @@ -147,6 +150,7 @@ private function resolveAbstractClassToInstantiableClass(string $abstract_class) { if (is_a($abstract_class, AbstractParameter::class, true)) { return match ($this->getTargetClass()) { + Assembly::class => AssemblyParameter::class, AttachmentType::class => AttachmentTypeParameter::class, Category::class => CategoryParameter::class, Currency::class => CurrencyParameter::class, @@ -168,6 +172,7 @@ private function resolveAbstractClassToInstantiableClass(string $abstract_class) Category::class => CategoryAttachment::class, Currency::class => CurrencyAttachment::class, Project::class => ProjectAttachment::class, + Assembly::class => AssemblyAttachment::class, Footprint::class => FootprintAttachment::class, Group::class => GroupAttachment::class, Manufacturer::class => ManufacturerAttachment::class, diff --git a/src/Entity/LogSystem/LogTargetType.php b/src/Entity/LogSystem/LogTargetType.php index 1c6e4f8c0..2bccf14af 100644 --- a/src/Entity/LogSystem/LogTargetType.php +++ b/src/Entity/LogSystem/LogTargetType.php @@ -22,6 +22,8 @@ */ namespace App\Entity\LogSystem; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\LabelSystem\LabelProfile; @@ -67,6 +69,8 @@ enum LogTargetType: int case LABEL_PROFILE = 19; case PART_ASSOCIATION = 20; + case ASSEMBLY = 21; + case ASSEMBLY_BOM_ENTRY = 22; /** * Returns the class name of the target type or null if the target type is NONE. @@ -82,6 +86,8 @@ public function toClass(): ?string self::CATEGORY => Category::class, self::PROJECT => Project::class, self::BOM_ENTRY => ProjectBOMEntry::class, + self::ASSEMBLY => Assembly::class, + self::ASSEMBLY_BOM_ENTRY => AssemblyBOMEntry::class, self::FOOTPRINT => Footprint::class, self::GROUP => Group::class, self::MANUFACTURER => Manufacturer::class, diff --git a/src/Entity/Parameters/AbstractParameter.php b/src/Entity/Parameters/AbstractParameter.php index 39f333dad..b6ef0412b 100644 --- a/src/Entity/Parameters/AbstractParameter.php +++ b/src/Entity/Parameters/AbstractParameter.php @@ -73,7 +73,7 @@ #[ORM\DiscriminatorMap([0 => CategoryParameter::class, 1 => CurrencyParameter::class, 2 => ProjectParameter::class, 3 => FootprintParameter::class, 4 => GroupParameter::class, 5 => ManufacturerParameter::class, 6 => MeasurementUnitParameter::class, 7 => PartParameter::class, 8 => StorageLocationParameter::class, - 9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class])] + 9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class, 11 => AssemblyParameter::class])] #[ORM\Table('parameters')] #[ORM\Index(columns: ['name'], name: 'parameter_name_idx')] #[ORM\Index(columns: ['param_group'], name: 'parameter_group_idx')] @@ -103,7 +103,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu */ private const API_DISCRIMINATOR_MAP = ["Part" => PartParameter::class, "AttachmentType" => AttachmentTypeParameter::class, "Category" => CategoryParameter::class, "Currency" => CurrencyParameter::class, - "Project" => ProjectParameter::class, "Footprint" => FootprintParameter::class, "Group" => GroupParameter::class, + "Project" => ProjectParameter::class, "Assembly" => AssemblyParameter::class, "Footprint" => FootprintParameter::class, "Group" => GroupParameter::class, "Manufacturer" => ManufacturerParameter::class, "MeasurementUnit" => MeasurementUnitParameter::class, "StorageLocation" => StorageLocationParameter::class, "Supplier" => SupplierParameter::class]; diff --git a/src/Entity/Parameters/AssemblyParameter.php b/src/Entity/Parameters/AssemblyParameter.php new file mode 100644 index 000000000..349fa7906 --- /dev/null +++ b/src/Entity/Parameters/AssemblyParameter.php @@ -0,0 +1,65 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 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 . + */ + +namespace App\Entity\Parameters; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Base\AbstractDBElement; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; + +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] +class AssemblyParameter extends AbstractParameter +{ + final public const ALLOWED_ELEMENT_CLASS = Assembly::class; + + /** + * @var Assembly the element this para is associated with + */ + #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; +} diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index 14a7903fc..74bd3c3a4 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -23,6 +23,7 @@ namespace App\Entity\Parts; use App\ApiPlatform\Filter\TagFilter; +use App\Entity\Parts\PartTraits\AssemblyTrait; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; @@ -114,6 +115,7 @@ class Part extends AttachmentContainingDBElement use OrderTrait; use ParametersTrait; use ProjectTrait; + use AssemblyTrait; use AssociationTrait; use EDATrait; @@ -169,6 +171,7 @@ public function __construct() $this->orderdetails = new ArrayCollection(); $this->parameters = new ArrayCollection(); $this->project_bom_entries = new ArrayCollection(); + $this->assembly_bom_entries = new ArrayCollection(); $this->associated_parts_as_owner = new ArrayCollection(); $this->associated_parts_as_other = new ArrayCollection(); diff --git a/src/Entity/Parts/PartTraits/AssemblyTrait.php b/src/Entity/Parts/PartTraits/AssemblyTrait.php new file mode 100644 index 000000000..57f78d352 --- /dev/null +++ b/src/Entity/Parts/PartTraits/AssemblyTrait.php @@ -0,0 +1,83 @@ + $assembly_bom_entries + */ + #[ORM\OneToMany(mappedBy: 'part', targetEntity: AssemblyBOMEntry::class, cascade: ['remove'], orphanRemoval: true)] + protected Collection $assembly_bom_entries; + + /** + * @var Assembly|null If a assembly is set here, then this part is special and represents the builds of an assembly. + */ + #[ORM\OneToOne(inversedBy: 'build_part', targetEntity: Assembly::class)] + #[ORM\JoinColumn] + protected ?Assembly $built_assembly = null; + + /** + * Returns all AssemblyBOMEntry that use this part. + * + * @phpstan-return Collection + */ + public function getAssemblyBomEntries(): Collection + { + return $this->assembly_bom_entries; + } + + /** + * Checks whether this part represents the builds of a assembly + * @return bool True if it represents the builds, false if not + */ + #[Groups(['part:read'])] + public function isAssemblyBuildPart(): bool + { + return $this->built_assembly !== null; + } + + /** + * Returns the assembly that this part represents the builds of, or null if it doesn't + */ + public function getBuiltAssembly(): ?Assembly + { + return $this->built_assembly; + } + + + /** + * Sets the assembly that this part represents the builds of + * @param Assembly|null $built_assembly The assembly that this part represents the builds of, or null if it is not a build part + */ + public function setBuiltAssembly(?Assembly $built_assembly): self + { + $this->built_assembly = $built_assembly; + return $this; + } + + + /** + * Get all assemblies which uses this part. + * + * @return Assembly[] all assemblies which uses this part as a one-dimensional array of Assembly objects + */ + public function getAssemblies(): array + { + $assemblies = []; + + foreach($this->assembly_bom_entries as $entry) { + $assemblies[] = $entry->getAssembly(); + } + + return $assemblies; + } +} diff --git a/src/Entity/ProjectSystem/Project.php b/src/Entity/ProjectSystem/Project.php index a103d6946..36a96377b 100644 --- a/src/Entity/ProjectSystem/Project.php +++ b/src/Entity/ProjectSystem/Project.php @@ -108,6 +108,7 @@ class Project extends AbstractStructuralDBElement #[Groups(['extended', 'full', 'import'])] #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part'])] + #[UniqueObjectCollection(message: 'project.bom_entry.assembly_already_in_bom', fields: ['assembly'])] #[UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name'])] protected Collection $bom_entries; diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index 2a7862ec5..692407730 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -35,6 +35,7 @@ use ApiPlatform\OpenApi\Model\Operation; use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Contracts\TimeStampableInterface; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; @@ -103,7 +104,10 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte /** * @var string|null An optional name describing this BOM entry (useful for non-part entries) */ - #[Assert\Expression('this.getPart() !== null or this.getName() !== null', message: 'validator.project.bom_entry.name_or_part_needed')] + #[Assert\Expression( + 'this.getPart() !== null or this.getAssembly() !== null or (this.getName() !== null and this.getName() != "")', + message: 'validator.project.bom_entry.part_or_assembly_needed' + )] #[ORM\Column(type: Types::STRING, nullable: true)] #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] protected ?string $name = null; @@ -131,6 +135,18 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte #[Groups(['bom_entry:read', 'bom_entry:write', 'full'])] protected ?Part $part = null; + /** + * @var Assembly|null The associated assembly + */ + #[Assert\Expression( + '(this.getPart() === null or this.getAssembly() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))', + message: 'validator.project.bom_entry.only_part_or_assembly_allowed' + )] + #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'assembly_bom_entries')] + #[ORM\JoinColumn(name: 'id_assembly')] + #[Groups(['bom_entry:read', 'bom_entry:write', ])] + protected ?Assembly $assembly = null; + /** * @var BigDecimal|null The price of this non-part BOM entry */ @@ -212,8 +228,6 @@ public function setProject(?Project $project): ProjectBOMEntry return $this; } - - public function getPart(): ?Part { return $this->part; @@ -225,6 +239,16 @@ public function setPart(?Part $part): ProjectBOMEntry return $this; } + public function getAssembly(): ?Assembly + { + return $this->assembly; + } + + public function setAssembly(?Assembly $assembly): void + { + $this->assembly = $assembly; + } + /** * Returns the price of this BOM entry, if existing. * Prices are only valid on non-Part BOM entries. @@ -262,6 +286,15 @@ public function isPartBomEntry(): bool return $this->part instanceof Part; } + /** + * Checks whether this BOM entry is a assembly associated BOM entry or not. + * @return bool True if this BOM entry is a assembly associated BOM entry, false otherwise. + */ + public function isAssemblyBomEntry(): bool + { + return $this->assembly instanceof Assembly; + } + #[Assert\Callback] public function validate(ExecutionContextInterface $context, $payload): void { @@ -323,6 +356,7 @@ public function getComparableFields(): array return [ 'name' => $this->getName(), 'part' => $this->getPart()?->getID(), + 'assembly' => $this->getAssembly()?->getID(), ]; } } diff --git a/src/Form/AdminPages/AssemblyAdminForm.php b/src/Form/AdminPages/AssemblyAdminForm.php new file mode 100644 index 000000000..be1564d21 --- /dev/null +++ b/src/Form/AdminPages/AssemblyAdminForm.php @@ -0,0 +1,64 @@ +. + */ +namespace App\Form\AdminPages; + +use App\Entity\Base\AbstractNamedDBElement; +use App\Form\AssemblySystem\AssemblyBOMEntryCollectionType; +use App\Form\Type\RichTextEditorType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormBuilderInterface; + +class AssemblyAdminForm extends BaseEntityAdminForm +{ + protected function additionalFormElements(FormBuilderInterface $builder, array $options, AbstractNamedDBElement $entity): void + { + $builder->add('description', RichTextEditorType::class, [ + 'required' => false, + 'label' => 'part.edit.description', + 'mode' => 'markdown-single_line', + 'empty_data' => '', + 'attr' => [ + 'placeholder' => 'part.edit.description.placeholder', + 'rows' => 2, + ], + ]); + + $builder->add('bom_entries', AssemblyBOMEntryCollectionType::class); + + $builder->add('status', ChoiceType::class, [ + 'attr' => [ + 'class' => 'form-select', + ], + 'label' => 'assembly.edit.status', + 'required' => false, + 'empty_data' => '', + 'choices' => [ + 'assembly.status.draft' => 'draft', + 'assembly.status.planning' => 'planning', + 'assembly.status.in_production' => 'in_production', + 'assembly.status.finished' => 'finished', + 'assembly.status.archived' => 'archived', + ], + ]); + } +} diff --git a/src/Form/AdminPages/BaseEntityAdminForm.php b/src/Form/AdminPages/BaseEntityAdminForm.php index 5a4ef5bce..e5d69b35c 100644 --- a/src/Form/AdminPages/BaseEntityAdminForm.php +++ b/src/Form/AdminPages/BaseEntityAdminForm.php @@ -22,6 +22,7 @@ namespace App\Form\AdminPages; +use App\Entity\AssemblySystem\Assembly; use App\Entity\PriceInformations\Currency; use App\Entity\ProjectSystem\Project; use App\Entity\UserSystem\Group; @@ -114,7 +115,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ); } - if ($entity instanceof AbstractStructuralDBElement && !($entity instanceof Group || $entity instanceof Project || $entity instanceof Currency)) { + if ($entity instanceof AbstractStructuralDBElement && !($entity instanceof Group || $entity instanceof Project || $entity instanceof Assembly || $entity instanceof Currency)) { $builder->add('alternative_names', TextType::class, [ 'required' => false, 'label' => 'entity.edit.alternative_names.label', diff --git a/src/Form/AssemblySystem/AssemblyAddPartsType.php b/src/Form/AssemblySystem/AssemblyAddPartsType.php new file mode 100644 index 000000000..4d84881f0 --- /dev/null +++ b/src/Form/AssemblySystem/AssemblyAddPartsType.php @@ -0,0 +1,88 @@ +. + */ +namespace App\Form\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Form\Type\StructuralEntityType; +use App\Validator\Constraints\UniqueObjectCollection; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\NotNull; + +class AssemblyAddPartsType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('assembly', StructuralEntityType::class, [ + 'class' => Assembly::class, + 'required' => true, + 'disabled' => $options['assembly'] instanceof Assembly, //If a assembly is given, disable the field + 'data' => $options['assembly'], + 'constraints' => [ + new NotNull() + ] + ]); + $builder->add('bom_entries', AssemblyBOMEntryCollectionType::class, [ + 'entry_options' => [ + 'constraints' => [ + new UniqueEntity(fields: ['part', 'assembly'], message: 'assembly.bom_entry.part_already_in_bom', + entityClass: AssemblyBOMEntry::class), + new UniqueEntity(fields: ['name', 'assembly'], message: 'assembly.bom_entry.name_already_in_bom', + entityClass: AssemblyBOMEntry::class, ignoreNull: true), + ] + ], + 'constraints' => [ + new UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part']), + new UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name']), + ] + ]); + $builder->add('submit', SubmitType::class, ['label' => 'save']); + + //After submit set the assembly for all bom entries, so that it can be validated properly + $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) { + $form = $event->getForm(); + /** @var Assembly $assembly */ + $assembly = $form->get('assembly')->getData(); + $bom_entries = $form->get('bom_entries')->getData(); + + foreach ($bom_entries as $bom_entry) { + $bom_entry->setAssembly($assembly); + } + }); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'assembly' => null, + ]); + + $resolver->setAllowedTypes('assembly', ['null', Assembly::class]); + } +} diff --git a/src/Form/AssemblySystem/AssemblyBOMEntryCollectionType.php b/src/Form/AssemblySystem/AssemblyBOMEntryCollectionType.php new file mode 100644 index 000000000..04293f4e0 --- /dev/null +++ b/src/Form/AssemblySystem/AssemblyBOMEntryCollectionType.php @@ -0,0 +1,32 @@ +setDefaults([ + 'entry_type' => AssemblyBOMEntryType::class, + 'entry_options' => [ + 'label' => false, + ], + 'allow_add' => true, + 'allow_delete' => true, + 'by_reference' => false, + 'reindex_enable' => true, + 'label' => false, + ]); + } +} diff --git a/src/Form/AssemblySystem/AssemblyBOMEntryType.php b/src/Form/AssemblySystem/AssemblyBOMEntryType.php new file mode 100644 index 000000000..9addccb34 --- /dev/null +++ b/src/Form/AssemblySystem/AssemblyBOMEntryType.php @@ -0,0 +1,90 @@ +addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { + $form = $event->getForm(); + /** @var AssemblyBOMEntry $data */ + $data = $event->getData(); + + $form->add('quantity', SIUnitType::class, [ + 'label' => 'assembly.bom.quantity', + 'measurement_unit' => $data && $data->getPart() ? $data->getPart()->getPartUnit() : null, + ]); + }); + + $builder + + ->add('part', PartSelectType::class, [ + 'required' => false, + ]) + + ->add('name', TextType::class, [ + 'label' => 'assembly.bom.name', + 'required' => false, + ]) + ->add('mountnames', TextType::class, [ + 'required' => false, + 'label' => 'assembly.bom.mountnames', + 'empty_data' => '', + 'attr' => [ + 'class' => 'tagsinput', + 'data-controller' => 'elements--tagsinput', + ] + ]) + ->add('comment', RichTextEditorType::class, [ + 'required' => false, + 'label' => 'assembly.bom.comment', + 'empty_data' => '', + 'mode' => 'markdown-single_line', + 'attr' => [ + 'rows' => 2, + ], + ]) + ->add('price', BigDecimalNumberType::class, [ + 'label' => false, + 'required' => false, + 'scale' => 5, + 'html5' => true, + 'attr' => [ + 'min' => 0, + 'step' => 'any', + ], + ]) + ->add('priceCurrency', CurrencyEntityType::class, [ + 'required' => false, + 'label' => false, + 'short' => true, + ]) + + ; + + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => AssemblyBOMEntry::class, + ]); + } +} diff --git a/src/Form/AssemblySystem/AssemblyBuildType.php b/src/Form/AssemblySystem/AssemblyBuildType.php new file mode 100644 index 000000000..8838706d4 --- /dev/null +++ b/src/Form/AssemblySystem/AssemblyBuildType.php @@ -0,0 +1,183 @@ +. + */ +namespace App\Form\AssemblySystem; + +use App\Helpers\Assemblies\AssemblyBuildRequest; +use Symfony\Bundle\SecurityBundle\Security; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Form\Type\PartLotSelectType; +use App\Form\Type\SIUnitType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\Event\PreSetDataEvent; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class AssemblyBuildType extends AbstractType implements DataMapperInterface +{ + public function __construct(private readonly Security $security) + { + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => AssemblyBuildRequest::class + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->setDataMapper($this); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'assembly.build.btn_build', + 'disabled' => !$this->security->isGranted('@parts_stock.withdraw'), + ]); + + $builder->add('dontCheckQuantity', CheckboxType::class, [ + 'label' => 'assembly.build.dont_check_quantity', + 'help' => 'assembly.build.dont_check_quantity.help', + 'required' => false, + 'attr' => [ + 'data-controller' => 'pages--dont-check-quantity-checkbox' + ] + ]); + + $builder->add('comment', TextType::class, [ + 'label' => 'part.info.withdraw_modal.comment', + 'help' => 'part.info.withdraw_modal.comment.hint', + 'empty_data' => '', + 'required' => false, + ]); + + + //The form is initially empty, we have to define the fields after we know the data + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { + $form = $event->getForm(); + /** @var AssemblyBuildRequest $build_request */ + $build_request = $event->getData(); + + $form->add('addBuildsToBuildsPart', CheckboxType::class, [ + 'label' => 'assembly.build.add_builds_to_builds_part', + 'required' => false, + 'disabled' => !$build_request->getAssembly()->getBuildPart() instanceof Part, + ]); + + if ($build_request->getAssembly()->getBuildPart() instanceof Part) { + $form->add('buildsPartLot', PartLotSelectType::class, [ + 'label' => 'assembly.build.builds_part_lot', + 'required' => false, + 'part' => $build_request->getAssembly()->getBuildPart(), + 'placeholder' => 'assembly.build.buildsPartLot.new_lot' + ]); + } + + foreach ($build_request->getPartBomEntries() as $bomEntry) { + //Every part lot has a field to specify the number of parts to take from this lot + foreach ($build_request->getPartLotsForBOMEntry($bomEntry) as $lot) { + $form->add('lot_' . $lot->getID(), SIUnitType::class, [ + 'label' => false, + 'measurement_unit' => $bomEntry->getPart()->getPartUnit(), + 'max' => min($build_request->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()), + 'disabled' => !$this->security->isGranted('withdraw', $lot), + ]); + } + } + + }); + } + + public function mapDataToForms($data, \Traversable $forms): void + { + if (!$data instanceof AssemblyBuildRequest) { + throw new \RuntimeException('Data must be an instance of ' . AssemblyBuildRequest::class); + } + + /** @var FormInterface[] $forms */ + $forms = iterator_to_array($forms); + foreach ($forms as $key => $form) { + //Extract the lot id from the form name + $matches = []; + if (preg_match('/^lot_(\d+)$/', $key, $matches)) { + $lot_id = (int) $matches[1]; + $form->setData($data->getLotWithdrawAmount($lot_id)); + } + } + + $forms['comment']->setData($data->getComment()); + $forms['dontCheckQuantity']->setData($data->isDontCheckQuantity()); + $forms['addBuildsToBuildsPart']->setData($data->getAddBuildsToBuildsPart()); + if (isset($forms['buildsPartLot'])) { + $forms['buildsPartLot']->setData($data->getBuildsPartLot()); + } + + } + + public function mapFormsToData(\Traversable $forms, &$data): void + { + if (!$data instanceof AssemblyBuildRequest) { + throw new \RuntimeException('Data must be an instance of ' . AssemblyBuildRequest::class); + } + + /** @var FormInterface[] $forms */ + $forms = iterator_to_array($forms); + + foreach ($forms as $key => $form) { + //Extract the lot id from the form name + $matches = []; + if (preg_match('/^lot_(\d+)$/', $key, $matches)) { + $lot_id = (int) $matches[1]; + $data->setLotWithdrawAmount($lot_id, (float) $form->getData()); + } + } + + $data->setComment($forms['comment']->getData()); + $data->setDontCheckQuantity($forms['dontCheckQuantity']->getData()); + + if (isset($forms['buildsPartLot'])) { + $lot = $forms['buildsPartLot']->getData(); + if (!$lot) { //When the user selected "Create new lot", create a new lot + $lot = new PartLot(); + $description = 'Build ' . date('Y-m-d H:i:s'); + if ($data->getComment() !== '') { + $description .= ' (' . $data->getComment() . ')'; + } + $lot->setDescription($description); + + $data->getAssembly()->getBuildPart()->addPartLot($lot); + } + + $data->setBuildsPartLot($lot); + } + //This has to be set after the builds part lot, so that it can disable the option + $data->setAddBuildsToBuildsPart($forms['addBuildsToBuildsPart']->getData()); + } +} diff --git a/src/Form/Filters/AttachmentFilterType.php b/src/Form/Filters/AttachmentFilterType.php index ff80bd384..a44588955 100644 --- a/src/Form/Filters/AttachmentFilterType.php +++ b/src/Form/Filters/AttachmentFilterType.php @@ -23,6 +23,7 @@ namespace App\Form\Filters; use App\DataTables\Filters\AttachmentFilter; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Attachments\CategoryAttachment; @@ -80,6 +81,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'category.label' => CategoryAttachment::class, 'currency.label' => CurrencyAttachment::class, 'project.label' => ProjectAttachment::class, + 'assembly.label' => AssemblyAttachment::class, 'footprint.label' => FootprintAttachment::class, 'group.label' => GroupAttachment::class, 'label_profile.label' => LabelAttachment::class, diff --git a/src/Form/ProjectSystem/ProjectAddPartsType.php b/src/Form/ProjectSystem/ProjectAddPartsType.php index 61f72c41d..c5dbe99f3 100644 --- a/src/Form/ProjectSystem/ProjectAddPartsType.php +++ b/src/Form/ProjectSystem/ProjectAddPartsType.php @@ -59,6 +59,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ], 'constraints' => [ new UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part']), + new UniqueObjectCollection(message: 'project.bom_entry.assembly_already_in_bom', fields: ['assembly']), new UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name']), ] ]); diff --git a/src/Form/ProjectSystem/ProjectBOMEntryType.php b/src/Form/ProjectSystem/ProjectBOMEntryType.php index cac362fbb..de8eb789c 100644 --- a/src/Form/ProjectSystem/ProjectBOMEntryType.php +++ b/src/Form/ProjectSystem/ProjectBOMEntryType.php @@ -5,6 +5,7 @@ namespace App\Form\ProjectSystem; use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Form\Type\AssemblySelectType; use App\Form\Type\BigDecimalNumberType; use App\Form\Type\CurrencyEntityType; use App\Form\Type\PartSelectType; @@ -22,8 +23,6 @@ class ProjectBOMEntryType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { - - $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { $form = $event->getForm(); /** @var ProjectBOMEntry $data */ @@ -36,11 +35,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void }); $builder - ->add('part', PartSelectType::class, [ + 'label' => 'project.bom.part', + 'required' => false, + ]) + ->add('assembly', AssemblySelectType::class, [ + 'label' => 'project.bom.assembly', 'required' => false, ]) - ->add('name', TextType::class, [ 'label' => 'project.bom.name', 'required' => false, @@ -77,10 +79,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'label' => false, 'short' => true, - ]) - - ; - + ]); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Form/ProjectSystem/ProjectBuildType.php b/src/Form/ProjectSystem/ProjectBuildType.php index 2b7b52e28..d0d4e3433 100644 --- a/src/Form/ProjectSystem/ProjectBuildType.php +++ b/src/Form/ProjectSystem/ProjectBuildType.php @@ -22,6 +22,7 @@ */ namespace App\Form\ProjectSystem; +use App\Helpers\Assemblies\AssemblyBuildRequest; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; @@ -38,10 +39,11 @@ use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; class ProjectBuildType extends AbstractType implements DataMapperInterface { - public function __construct(private readonly Security $security) + public function __construct(private readonly Security $security, private readonly TranslatorInterface $translator) { } @@ -82,36 +84,54 @@ public function buildForm(FormBuilderInterface $builder, array $options): void //The form is initially empty, we have to define the fields after we know the data $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { $form = $event->getForm(); - /** @var ProjectBuildRequest $build_request */ - $build_request = $event->getData(); + /** @var ProjectBuildRequest $projectBuildRequest */ + $projectBuildRequest = $event->getData(); $form->add('addBuildsToBuildsPart', CheckboxType::class, [ 'label' => 'project.build.add_builds_to_builds_part', 'required' => false, - 'disabled' => !$build_request->getProject()->getBuildPart() instanceof Part, + 'disabled' => !$projectBuildRequest->getProject()->getBuildPart() instanceof Part, ]); - if ($build_request->getProject()->getBuildPart() instanceof Part) { + if ($projectBuildRequest->getProject()->getBuildPart() instanceof Part) { $form->add('buildsPartLot', PartLotSelectType::class, [ 'label' => 'project.build.builds_part_lot', 'required' => false, - 'part' => $build_request->getProject()->getBuildPart(), + 'part' => $projectBuildRequest->getProject()->getBuildPart(), 'placeholder' => 'project.build.buildsPartLot.new_lot' ]); } - foreach ($build_request->getPartBomEntries() as $bomEntry) { + foreach ($projectBuildRequest->getPartBomEntries() as $bomEntry) { //Every part lot has a field to specify the number of parts to take from this lot - foreach ($build_request->getPartLotsForBOMEntry($bomEntry) as $lot) { + foreach ($projectBuildRequest->getPartLotsForBOMEntry($bomEntry) as $lot) { $form->add('lot_' . $lot->getID(), SIUnitType::class, [ 'label' => false, 'measurement_unit' => $bomEntry->getPart()->getPartUnit(), - 'max' => min($build_request->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()), + 'max' => min($projectBuildRequest->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()), 'disabled' => !$this->security->isGranted('withdraw', $lot), ]); } } + foreach ($projectBuildRequest->getAssemblyBomEntries() as $bomEntry) { + $assemblyBuildRequest = new AssemblyBuildRequest($bomEntry->getAssembly(), $projectBuildRequest->getNumberOfBuilds()); + + //Add fields for assembly bom entries + foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) { + foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $lot) { + $form->add('lot_' . $lot->getID(), SIUnitType::class, [ + 'label' => $this->translator->trans('project.build.builds_part_lot_label', [ + '%name%' => $partBomEntry->getPart()->getName(), + '%quantity%' => $partBomEntry->getQuantity() * $projectBuildRequest->getNumberOfBuilds() + ]), + 'measurement_unit' => $partBomEntry->getPart()->getPartUnit(), + 'max' => min($assemblyBuildRequest->getNeededAmountForBOMEntry($partBomEntry), $lot->getAmount()), + 'disabled' => !$this->security->isGranted('withdraw', $lot), + ]); + } + } + } }); } diff --git a/src/Form/Type/AssemblySelectType.php b/src/Form/Type/AssemblySelectType.php new file mode 100644 index 000000000..ee6cf7c2a --- /dev/null +++ b/src/Form/Type/AssemblySelectType.php @@ -0,0 +1,125 @@ +addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { + $form = $event->getForm(); + $config = $form->getConfig()->getOptions(); + $data = $event->getData() ?? []; + + $config['compound'] = false; + $config['choices'] = is_iterable($data) ? $data : [$data]; + $config['error_bubbling'] = true; + + $form->add('autocomplete', EntityType::class, $config); + }); + + //After form submit, we have to add the selected element as choice, otherwise the form will not accept this element + $builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) { + $data = $event->getData(); + $form = $event->getForm(); + $options = $form->get('autocomplete')->getConfig()->getOptions(); + + + if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) { + $options['choices'] = []; + } else { + //Extract the ID from the submitted data + $id = $data['autocomplete']; + //Find the element in the database + $element = $this->em->find($options['class'], $id); + + //Add the element as choice + $options['choices'] = [$element]; + $options['error_bubbling'] = true; + $form->add('autocomplete', EntityType::class, $options); + } + }); + + $builder->setDataMapper($this); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'class' => Assembly::class, + 'choice_label' => 'name', + 'placeholder' => 'None', + 'compound' => true, + 'error_bubbling' => false, + ]); + + error_log($this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__'])); + + $resolver->setDefaults([ + 'attr' => [ + 'data-controller' => 'elements--assembly-select', + 'data-autocomplete' => $this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']), + 'autocomplete' => 'off', + ], + ]); + + $resolver->setDefaults([ + //Prefill the selected choice with the needed data, so the user can see it without an additional Ajax request + 'choice_attr' => ChoiceList::attr($this, function (?Assembly $assembly) { + if($assembly instanceof Assembly) { + //Determine the picture to show: + $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($assembly); + if ($preview_attachment instanceof Attachment) { + $preview_url = $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, + 'thumbnail_sm'); + } else { + $preview_url = ''; + } + } + + return $assembly instanceof Assembly ? [ + 'data-description' => $assembly->getDescription() ? mb_strimwidth($assembly->getDescription(), 0, 127, '...') : '', + 'data-category' => '', + 'data-footprint' => '', + 'data-image' => $preview_url, + ] : []; + }) + ]); + } + + public function mapDataToForms($data, \Traversable $forms): void + { + $form = current(iterator_to_array($forms, false)); + $form->setData($data); + } + + public function mapFormsToData(\Traversable $forms, &$data): void + { + $form = current(iterator_to_array($forms, false)); + $data = $form->getData(); + } + +} diff --git a/src/Form/Type/PartSelectType.php b/src/Form/Type/PartSelectType.php index 34b8fc7c4..c41d6b8f9 100644 --- a/src/Form/Type/PartSelectType.php +++ b/src/Form/Type/PartSelectType.php @@ -50,7 +50,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $options = $form->get('autocomplete')->getConfig()->getOptions(); - if (!isset($data['autocomplete']) || '' === $data['autocomplete']) { + if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) { $options['choices'] = []; } else { //Extract the ID from the submitted data @@ -84,7 +84,6 @@ public function configureOptions(OptionsResolver $resolver): void 'data-autocomplete' => $this->urlGenerator->generate('typeahead_parts', ['query' => '__QUERY__']), //Disable browser autocomplete 'autocomplete' => 'off', - ], ]); @@ -103,7 +102,7 @@ public function configureOptions(OptionsResolver $resolver): void } return $part instanceof Part ? [ - 'data-description' => mb_strimwidth($part->getDescription(), 0, 127, '...'), + 'data-description' => $part->getDescription() ? mb_strimwidth($part->getDescription(), 0, 127, '...') : '', 'data-category' => $part->getCategory() instanceof Category ? $part->getCategory()->getName() : '', 'data-footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '', 'data-image' => $preview_url, diff --git a/src/Helpers/Assemblies/AssemblyBuildRequest.php b/src/Helpers/Assemblies/AssemblyBuildRequest.php new file mode 100644 index 000000000..c33a6f612 --- /dev/null +++ b/src/Helpers/Assemblies/AssemblyBuildRequest.php @@ -0,0 +1,306 @@ +. + */ +namespace App\Helpers\Assemblies; + +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Validator\Constraints\AssemblySystem\ValidAssemblyBuildRequest; + +/** + * @see \App\Tests\Helpers\Assemblies\AssemblyBuildRequestTest + */ +#[ValidAssemblyBuildRequest] +final class AssemblyBuildRequest +{ + private readonly int $number_of_builds; + + /** + * @var array + */ + private array $withdraw_amounts = []; + + private string $comment = ''; + + private ?PartLot $builds_lot = null; + + private bool $add_build_to_builds_part = false; + + private bool $dont_check_quantity = false; + + /** + * @param Assembly $assembly The assembly that should be build + * @param int $number_of_builds The number of builds that should be created + */ + public function __construct(private readonly Assembly $assembly, int $number_of_builds) + { + if ($number_of_builds < 1) { + throw new \InvalidArgumentException('Number of builds must be at least 1!'); + } + $this->number_of_builds = $number_of_builds; + + $this->initializeArray(); + + //By default, use the first available lot of builds part if there is one. + if($assembly->getBuildPart() instanceof Part) { + $this->add_build_to_builds_part = true; + foreach( $assembly->getBuildPart()->getPartLots() as $lot) { + if (!$lot->isInstockUnknown()) { + $this->builds_lot = $lot; + break; + } + } + } + } + + private function initializeArray(): void + { + //Completely reset the array + $this->withdraw_amounts = []; + + //Now create an array for each BOM entry + foreach ($this->getPartBomEntries() as $bom_entry) { + $remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry); + foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) { + //If the lot has instock use it for the build + $this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount()); + $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]); + } + } + } + + /** + * Ensure that the assemblyBOMEntry belongs to the assembly, otherwise throw an exception. + */ + private function ensureBOMEntryValid(AssemblyBOMEntry $entry): void + { + if ($entry->getAssembly() !== $this->assembly) { + throw new \InvalidArgumentException('The given BOM entry does not belong to the assembly!'); + } + } + + /** + * Returns the partlot where the builds should be added to, or null if it should not be added to any lot. + */ + public function getBuildsPartLot(): ?PartLot + { + return $this->builds_lot; + } + + /** + * Return if the builds should be added to the builds part of this assembly as new stock + */ + public function getAddBuildsToBuildsPart(): bool + { + return $this->add_build_to_builds_part; + } + + /** + * Set if the builds should be added to the builds part of this assembly as new stock + * @return $this + */ + public function setAddBuildsToBuildsPart(bool $new_value): self + { + $this->add_build_to_builds_part = $new_value; + + if ($new_value === false) { + $this->builds_lot = null; + } + + return $this; + } + + /** + * Set the partlot where the builds should be added to, or null if it should not be added to any lot. + * The part lot must belong to the assembly build part, or an exception is thrown! + * @return $this + */ + public function setBuildsPartLot(?PartLot $new_part_lot): self + { + //Ensure that this new_part_lot belongs to the assembly + if (($new_part_lot instanceof PartLot && $new_part_lot->getPart() !== $this->assembly->getBuildPart()) || !$this->assembly->getBuildPart() instanceof Part) { + throw new \InvalidArgumentException('The given part lot does not belong to the assemblies build part!'); + } + + if ($new_part_lot instanceof PartLot) { + $this->setAddBuildsToBuildsPart(true); + } + + $this->builds_lot = $new_part_lot; + + return $this; + } + + /** + * Returns the comment where the user can write additional information about the build. + */ + public function getComment(): string + { + return $this->comment; + } + + /** + * Sets the comment where the user can write additional information about the build. + */ + public function setComment(string $comment): void + { + $this->comment = $comment; + } + + /** + * Returns the amount of parts that should be withdrawn from the given lot for the corresponding BOM entry. + * @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdrawal amount should be got + */ + public function getLotWithdrawAmount(PartLot|int $lot): float + { + $lot_id = $lot instanceof PartLot ? $lot->getID() : $lot; + + if (! array_key_exists($lot_id, $this->withdraw_amounts)) { + throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!'); + } + + return $this->withdraw_amounts[$lot_id]; + } + + /** + * Sets the amount of parts that should be withdrawn from the given lot for the corresponding BOM entry. + * @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdrawal amount should be got + * @return $this + */ + public function setLotWithdrawAmount(PartLot|int $lot, float $amount): self + { + if ($lot instanceof PartLot) { + $lot_id = $lot->getID(); + } elseif (is_int($lot)) { + $lot_id = $lot; + } else { + throw new \InvalidArgumentException('The given lot must be an instance of PartLot or an ID of a PartLot!'); + } + + $this->withdraw_amounts[$lot_id] = $amount; + + return $this; + } + + /** + * Returns the sum of all withdraw amounts for the given BOM entry. + */ + public function getWithdrawAmountSum(AssemblyBOMEntry $entry): float + { + $this->ensureBOMEntryValid($entry); + + $sum = 0; + foreach ($this->getPartLotsForBOMEntry($entry) as $lot) { + $sum += $this->getLotWithdrawAmount($lot); + } + + if ($entry->getPart() && !$entry->getPart()->useFloatAmount()) { + $sum = round($sum); + } + + return $sum; + } + + /** + * Returns the number of available lots to take stock from for the given BOM entry. + * @return PartLot[]|null Returns null if the entry is a non-part BOM entry + */ + public function getPartLotsForBOMEntry(AssemblyBOMEntry $assemblyBOMEntry): ?array + { + $this->ensureBOMEntryValid($assemblyBOMEntry); + + if (!$assemblyBOMEntry->getPart() instanceof Part) { + return null; + } + + //Filter out all lots which have unknown instock + return $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); + } + + /** + * Returns the needed amount of parts for the given BOM entry. + */ + public function getNeededAmountForBOMEntry(AssemblyBOMEntry $entry): float + { + $this->ensureBOMEntryValid($entry); + + return $entry->getQuantity() * $this->number_of_builds; + } + + /** + * Returns the list of all bom entries. + * @return AssemblyBOMEntry[] + */ + public function getBomEntries(): array + { + return $this->assembly->getBomEntries()->toArray(); + } + + /** + * Returns all part bom entries. + * @return AssemblyBOMEntry[] + */ + public function getPartBomEntries(): array + { + return $this->assembly->getBomEntries()->filter(fn(AssemblyBOMEntry $entry) => $entry->isPartBomEntry())->toArray(); + } + + /** + * Returns which assembly should be build + */ + public function getAssembly(): Assembly + { + return $this->assembly; + } + + /** + * Returns the number of builds that should be created. + */ + public function getNumberOfBuilds(): int + { + return $this->number_of_builds; + } + + /** + * If Set to true, the given withdraw amounts are used without any checks for requirements. + * @return bool + */ + public function isDontCheckQuantity(): bool + { + return $this->dont_check_quantity; + } + + /** + * Set to true, the given withdraw amounts are used without any checks for requirements. + * @param bool $dont_check_quantity + * @return $this + */ + public function setDontCheckQuantity(bool $dont_check_quantity): AssemblyBuildRequest + { + $this->dont_check_quantity = $dont_check_quantity; + return $this; + } + + +} diff --git a/src/Helpers/Projects/ProjectBuildRequest.php b/src/Helpers/Projects/ProjectBuildRequest.php index 430d37b56..3254565a5 100644 --- a/src/Helpers/Projects/ProjectBuildRequest.php +++ b/src/Helpers/Projects/ProjectBuildRequest.php @@ -22,10 +22,13 @@ */ namespace App\Helpers\Projects; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Helpers\Assemblies\AssemblyBuildRequest; use App\Validator\Constraints\ProjectSystem\ValidProjectBuildRequest; /** @@ -79,7 +82,7 @@ private function initializeArray(): void //Completely reset the array $this->withdraw_amounts = []; - //Now create an array for each BOM entry + //Now create an array for each part BOM entry foreach ($this->getPartBomEntries() as $bom_entry) { $remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry); foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) { @@ -88,6 +91,21 @@ private function initializeArray(): void $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]); } } + + //Now create an array for each assembly BOM entry + foreach ($this->getAssemblyBomEntries() as $assemblyBomEntry) { + $assemblyBuildRequest = new AssemblyBuildRequest($assemblyBomEntry->getAssembly(), $this->number_of_builds); + + //Add fields for assembly bom entries + foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) { + $remaining_amount = $assemblyBuildRequest->getNeededAmountForBOMEntry($partBomEntry) * $assemblyBomEntry->getQuantity(); + + foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $lot) { + $this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount()); + $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]); + } + } + } } /** @@ -230,12 +248,77 @@ public function getPartLotsForBOMEntry(ProjectBOMEntry $projectBOMEntry): ?array { $this->ensureBOMEntryValid($projectBOMEntry); - if (!$projectBOMEntry->getPart() instanceof Part) { + if (!$projectBOMEntry->getPart() instanceof Part && !$projectBOMEntry->getAssembly() instanceof Assembly) { return null; } //Filter out all lots which have unknown instock - return $projectBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); + if ($projectBOMEntry->getPart() instanceof Part) { + return $projectBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); + } elseif ($projectBOMEntry->getAssembly() instanceof Assembly) { + $assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds); + + //Add fields for assembly bom entries + $result = []; + foreach ($assemblyBuildRequest->getPartBomEntries() as $assemblyBOMEntry) { + $tmp = $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); + $result = array_merge($result, $tmp); + } + + return $result; + } + + return null; + } + + /** + * Returns all available assembly BOM-entries with no part assigned. + * @return AssemblyBOMEntry[]|null Returns null if no entries found + */ + public function getAssemblyBomEntriesWithoutPart(ProjectBOMEntry $projectBOMEntry): ?array + { + $this->ensureBOMEntryValid($projectBOMEntry); + + if (!$projectBOMEntry->getAssembly() instanceof Assembly) { + return null; + } + + $assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds); + + $result = []; + + foreach ($assemblyBuildRequest->getBomEntries() as $assemblyBOMEntry) { + if ($assemblyBOMEntry->getPart() === null) { + $result[] = $assemblyBOMEntry; + } + } + + return count($result) > 0 ? $result : null; + } + + /** + * Returns all available assembly BOM-entries with no part assigned. + * @return AssemblyBOMEntry[]|null Returns null if no entries found + */ + public function getAssemblyBomEntriesWithPartNoStock(ProjectBOMEntry $projectBOMEntry): ?array + { + $this->ensureBOMEntryValid($projectBOMEntry); + + if (!$projectBOMEntry->getAssembly() instanceof Assembly) { + return null; + } + + $assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds); + + $result = []; + + foreach ($assemblyBuildRequest->getBomEntries() as $assemblyBOMEntry) { + if ($assemblyBOMEntry->getPart() instanceof Part && $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->count() === 0) { + $result[] = $assemblyBOMEntry; + } + } + + return count($result) > 0 ? $result : null; } /** @@ -266,6 +349,15 @@ public function getPartBomEntries(): array return $this->project->getBomEntries()->filter(fn(ProjectBOMEntry $entry) => $entry->isPartBomEntry())->toArray(); } + /** + * Returns the all assembly bom entries that have to be built. + * @return ProjectBOMEntry[] + */ + public function getAssemblyBomEntries(): array + { + return $this->project->getBomEntries()->filter(fn(ProjectBOMEntry $entry) => $entry->isAssemblyBomEntry())->toArray(); + } + /** * Returns which project should be build */ @@ -301,6 +393,4 @@ public function setDontCheckQuantity(bool $dont_check_quantity): ProjectBuildReq $this->dont_check_quantity = $dont_check_quantity; return $this; } - - -} +} \ No newline at end of file diff --git a/src/Repository/AssemblyRepository.php b/src/Repository/AssemblyRepository.php new file mode 100644 index 000000000..031e6e82b --- /dev/null +++ b/src/Repository/AssemblyRepository.php @@ -0,0 +1,69 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 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 . + */ + +namespace App\Repository; + +use App\Entity\AssemblySystem\Assembly; + +/** + * @template TEntityClass of Assembly + * @extends DBElementRepository + */ +class AssemblyRepository extends StructuralDBElementRepository +{ + /** + * @return Assembly[] + */ + public function autocompleteSearch(string $query, int $max_limits = 50): array + { + $qb = $this->createQueryBuilder('assembly'); + $qb->select('assembly') + ->where('ILIKE(assembly.name, :query) = TRUE') + ->orWhere('ILIKE(assembly.description, :query) = TRUE'); + + $qb->setParameter('query', '%'.$query.'%'); + + $qb->setMaxResults($max_limits); + $qb->orderBy('NATSORT(assembly.name)', 'ASC'); + + return $qb->getQuery()->getResult(); + } +} \ No newline at end of file diff --git a/src/Repository/DBElementRepository.php b/src/Repository/DBElementRepository.php index 2437e8488..23ad296aa 100644 --- a/src/Repository/DBElementRepository.php +++ b/src/Repository/DBElementRepository.php @@ -154,4 +154,14 @@ protected function setField(AbstractDBElement $element, string $field, int $new_ $property->setAccessible(true); $property->setValue($element, $new_value); } + + protected function save(AbstractDBElement $entity, bool $flush = true): void + { + $manager = $this->getEntityManager(); + $manager->persist($entity); + + if ($flush) { + $manager->flush(); + } + } } diff --git a/src/Security/Voter/AttachmentVoter.php b/src/Security/Voter/AttachmentVoter.php index bd7ae4df8..766aaeac4 100644 --- a/src/Security/Voter/AttachmentVoter.php +++ b/src/Security/Voter/AttachmentVoter.php @@ -22,6 +22,7 @@ namespace App\Security\Voter; +use App\Entity\Attachments\AssemblyAttachment; use App\Services\UserSystem\VoterHelper; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Attachments\AttachmentContainingDBElement; @@ -89,6 +90,8 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $ $param = 'currencies'; } elseif (is_a($subject, ProjectAttachment::class, true)) { $param = 'projects'; + } elseif (is_a($subject, AssemblyAttachment::class, true)) { + $param = 'assemblies'; } elseif (is_a($subject, FootprintAttachment::class, true)) { $param = 'footprints'; } elseif (is_a($subject, GroupAttachment::class, true)) { diff --git a/src/Security/Voter/StructureVoter.php b/src/Security/Voter/StructureVoter.php index ad0299a79..8079757cd 100644 --- a/src/Security/Voter/StructureVoter.php +++ b/src/Security/Voter/StructureVoter.php @@ -22,6 +22,7 @@ namespace App\Security\Voter; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\AttachmentType; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; @@ -47,6 +48,7 @@ final class StructureVoter extends Voter AttachmentType::class => 'attachment_types', Category::class => 'categories', Project::class => 'projects', + Assembly::class => 'assemblies', Footprint::class => 'footprints', Manufacturer::class => 'manufacturers', StorageLocation::class => 'storelocations', diff --git a/src/Services/AssemblySystem/AssemblyBuildHelper.php b/src/Services/AssemblySystem/AssemblyBuildHelper.php new file mode 100644 index 000000000..8c95a4b66 --- /dev/null +++ b/src/Services/AssemblySystem/AssemblyBuildHelper.php @@ -0,0 +1,154 @@ +. + */ +namespace App\Services\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Parts\Part; +use App\Helpers\Assemblies\AssemblyBuildRequest; +use App\Services\Parts\PartLotWithdrawAddHelper; + +/** + * @see \App\Tests\Services\AssemblySystem\AssemblyBuildHelperTest + */ +class AssemblyBuildHelper +{ + public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper) + { + } + + /** + * Returns the maximum buildable amount of the given BOM entry based on the stock of the used parts. + * This function only works for BOM entries that are associated with a part. + */ + public function getMaximumBuildableCountForBOMEntry(AssemblyBOMEntry $assemblyBOMEntry): int + { + $part = $assemblyBOMEntry->getPart(); + + if (!$part instanceof Part) { + throw new \InvalidArgumentException('This function cannot determine the maximum buildable count for a BOM entry without a part!'); + } + + if ($assemblyBOMEntry->getQuantity() <= 0) { + throw new \RuntimeException('The quantity of the BOM entry must be greater than 0!'); + } + + $amount_sum = $part->getAmountSum(); + + return (int) floor($amount_sum / $assemblyBOMEntry->getQuantity()); + } + + /** + * Returns the maximum buildable amount of the given assembly, based on the stock of the used parts in the BOM. + */ + public function getMaximumBuildableCount(Assembly $assembly): int + { + $maximum_buildable_count = PHP_INT_MAX; + foreach ($assembly->getBomEntries() as $bom_entry) { + //Skip BOM entries without a part (as we can not determine that) + if (!$bom_entry->isPartBomEntry()) { + continue; + } + + //The maximum buildable count for the whole assembly is the minimum of all BOM entries + $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); + } + + return $maximum_buildable_count; + } + + /** + * Checks if the given assembly can be built with the current stock. + * This means that the maximum buildable count is greater or equal than the requested $number_of_assemblies + * @param int $number_of_builds + */ + public function isAssemblyBuildable(Assembly $assembly, int $number_of_builds = 1): bool + { + return $this->getMaximumBuildableCount($assembly) >= $number_of_builds; + } + + /** + * Check if the given BOM entry can be built with the current stock. + * This means that the maximum buildable count is greater or equal than the requested $number_of_assemblies + */ + public function isBOMEntryBuildable(AssemblyBOMEntry $bom_entry, int $number_of_builds = 1): bool + { + return $this->getMaximumBuildableCountForBOMEntry($bom_entry) >= $number_of_builds; + } + + /** + * Returns the assembly BOM entries for which parts are missing in the stock for the given number of builds + * @param Assembly $assembly The assembly for which the BOM entries should be checked + * @param int $number_of_builds How often should the assembly be build? + * @return AssemblyBOMEntry[] + */ + public function getNonBuildableAssemblyBomEntries(Assembly $assembly, int $number_of_builds = 1): array + { + if ($number_of_builds < 1) { + throw new \InvalidArgumentException('The number of builds must be greater than 0!'); + } + + $non_buildable_entries = []; + + foreach ($assembly->getBomEntries() as $bomEntry) { + $part = $bomEntry->getPart(); + + //Skip BOM entries without a part (as we can not determine that) + if (!$part instanceof Part) { + continue; + } + + $amount_sum = $part->getAmountSum(); + + if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { + $non_buildable_entries[] = $bomEntry; + } + } + + return $non_buildable_entries; + } + + /** + * Withdraw the parts from the stock using the given AssemblyBuildRequest and create the build parts entries, if needed. + * The AssemblyBuildRequest has to be validated before!! + * You have to flush changes to DB afterward + */ + public function doBuild(AssemblyBuildRequest $buildRequest): void + { + $message = $buildRequest->getComment(); + $message .= ' (Assembly build: '.$buildRequest->getAssembly()->getName().')'; + + foreach ($buildRequest->getPartBomEntries() as $bom_entry) { + foreach ($buildRequest->getPartLotsForBOMEntry($bom_entry) as $part_lot) { + $amount = $buildRequest->getLotWithdrawAmount($part_lot); + if ($amount > 0) { + $this->withdraw_add_helper->withdraw($part_lot, $amount, $message); + } + } + } + + if ($buildRequest->getAddBuildsToBuildsPart()) { + $this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message); + } + } +} diff --git a/src/Services/AssemblySystem/AssemblyBuildPartHelper.php b/src/Services/AssemblySystem/AssemblyBuildPartHelper.php new file mode 100644 index 000000000..9a5503505 --- /dev/null +++ b/src/Services/AssemblySystem/AssemblyBuildPartHelper.php @@ -0,0 +1,40 @@ +setBuiltAssembly($assembly); + + //Set the name of the part to the name of the assembly + $part->setName($assembly->getName()); + + //Set the description of the part to the description of the assembly + $part->setDescription($assembly->getDescription()); + + //Add a tag to the part that indicates that it is a build part + $part->setTags('assembly-build'); + + //Associate the part with the assembly + $assembly->setBuildPart($part); + + return $part; + } +} diff --git a/src/Services/Attachments/AssemblyPreviewGenerator.php b/src/Services/Attachments/AssemblyPreviewGenerator.php new file mode 100644 index 000000000..9ecbbd070 --- /dev/null +++ b/src/Services/Attachments/AssemblyPreviewGenerator.php @@ -0,0 +1,93 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\Attachments; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\Attachment; + +class AssemblyPreviewGenerator +{ + public function __construct(protected AttachmentManager $attachmentHelper) + { + } + + /** + * Returns a list of attachments that can be used for previewing the assembly ordered by priority. + * + * @param Assembly $assembly the assembly for which the attachments should be determined + * + * @return (Attachment|null)[] + * + * @psalm-return list + */ + public function getPreviewAttachments(Assembly $assembly): array + { + $list = []; + + //Master attachment has top priority + $attachment = $assembly->getMasterPictureAttachment(); + if ($this->isAttachmentValidPicture($attachment)) { + $list[] = $attachment; + } + + //Then comes the other images of the assembly + foreach ($assembly->getAttachments() as $attachment) { + //Dont show the master attachment twice + if ($this->isAttachmentValidPicture($attachment) && $attachment !== $assembly->getMasterPictureAttachment()) { + $list[] = $attachment; + } + } + + return $list; + } + + /** + * Determines what attachment should be used for previewing a assembly (especially in assembly table). + * The returned attachment is guaranteed to be existing and be a picture. + * + * @param Assembly $assembly The assembly for which the attachment should be determined + */ + public function getTablePreviewAttachment(Assembly $assembly): ?Attachment + { + $attachment = $assembly->getMasterPictureAttachment(); + if ($this->isAttachmentValidPicture($attachment)) { + return $attachment; + } + + return null; + } + + /** + * Checks if a attachment is exising and a valid picture. + * + * @param Attachment|null $attachment the attachment that should be checked + * + * @return bool true if the attachment is valid + */ + protected function isAttachmentValidPicture(?Attachment $attachment): bool + { + return $attachment instanceof Attachment + && $attachment->isPicture() + && $this->attachmentHelper->isFileExisting($attachment); + } +} diff --git a/src/Services/Attachments/AttachmentSubmitHandler.php b/src/Services/Attachments/AttachmentSubmitHandler.php index a30163ae1..81b998bec 100644 --- a/src/Services/Attachments/AttachmentSubmitHandler.php +++ b/src/Services/Attachments/AttachmentSubmitHandler.php @@ -22,6 +22,7 @@ namespace App\Services\Attachments; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentType; @@ -81,6 +82,7 @@ public function __construct( CategoryAttachment::class => 'category', CurrencyAttachment::class => 'currency', ProjectAttachment::class => 'project', + AssemblyAttachment::class => 'assembly', FootprintAttachment::class => 'footprint', GroupAttachment::class => 'group', ManufacturerAttachment::class => 'manufacturer', diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php index 142471457..db495cc7e 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -22,6 +22,8 @@ namespace App\Services; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; @@ -64,6 +66,8 @@ public function __construct(protected TranslatorInterface $translator, private r AttachmentType::class => $this->translator->trans('attachment_type.label'), Project::class => $this->translator->trans('project.label'), ProjectBOMEntry::class => $this->translator->trans('project_bom_entry.label'), + Assembly::class => $this->translator->trans('assembly.label'), + AssemblyBOMEntry::class => $this->translator->trans('assembly_bom_entry.label'), Footprint::class => $this->translator->trans('footprint.label'), Manufacturer::class => $this->translator->trans('manufacturer.label'), MeasurementUnit::class => $this->translator->trans('measurement_unit.label'), @@ -178,6 +182,8 @@ public function formatLabelHTMLForEntity(AbstractDBElement $entity, bool $includ $on = $entity->getOrderdetail()->getPart(); } elseif ($entity instanceof ProjectBOMEntry && $entity->getProject() instanceof Project) { $on = $entity->getProject(); + } elseif ($entity instanceof AssemblyBOMEntry && $entity->getAssembly() instanceof Assembly) { + $on = $entity->getAssembly(); } if (isset($on) && $on instanceof NamedElementInterface) { diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php index 78db06f07..8e1704b46 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -22,6 +22,7 @@ namespace App\Services; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\PartAttachment; @@ -98,6 +99,7 @@ public function timeTravelURL(AbstractDBElement $entity, \DateTimeInterface $dat AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', Project::class => 'project_edit', + Assembly::class => 'assembly_edit', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', StorageLocation::class => 'store_location_edit', @@ -204,6 +206,7 @@ public function infoURL(AbstractDBElement $entity): string AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', Project::class => 'project_info', + Assembly::class => 'assembly_info', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', StorageLocation::class => 'store_location_edit', @@ -234,6 +237,7 @@ public function editURL(AbstractDBElement $entity): string AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', Project::class => 'project_edit', + Assembly::class => 'assembly_edit', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', StorageLocation::class => 'store_location_edit', @@ -265,6 +269,7 @@ public function createURL(AbstractDBElement|string $entity): string AttachmentType::class => 'attachment_type_new', Category::class => 'category_new', Project::class => 'project_new', + Assembly::class => 'assembly_new', Supplier::class => 'supplier_new', Manufacturer::class => 'manufacturer_new', StorageLocation::class => 'store_location_new', @@ -296,6 +301,7 @@ public function cloneURL(AbstractDBElement $entity): string AttachmentType::class => 'attachment_type_clone', Category::class => 'category_clone', Project::class => 'device_clone', + Assembly::class => 'assembly_clone', Supplier::class => 'supplier_clone', Manufacturer::class => 'manufacturer_clone', StorageLocation::class => 'store_location_clone', @@ -323,6 +329,7 @@ public function listPartsURL(AbstractDBElement $entity): string { $map = [ Project::class => 'project_info', + Assembly::class => 'assembly_info', Category::class => 'part_list_category', Footprint::class => 'part_list_footprint', @@ -341,6 +348,7 @@ public function deleteURL(AbstractDBElement $entity): string AttachmentType::class => 'attachment_type_delete', Category::class => 'category_delete', Project::class => 'project_delete', + Assembly::class => 'assembly_delete', Supplier::class => 'supplier_delete', Manufacturer::class => 'manufacturer_delete', StorageLocation::class => 'store_location_delete', diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 862fa463f..fd32f0657 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -22,15 +22,25 @@ */ namespace App\Services\ImportExportSystem; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Parts\Category; +use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Repository\DBElementRepository; +use App\Repository\PartRepository; +use App\Repository\Parts\CategoryRepository; +use App\Repository\Parts\ManufacturerRepository; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use League\Csv\Reader; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\OptionsResolver\OptionsResolver; +use RuntimeException; +use UnexpectedValueException; /** * @see \App\Tests\Services\ImportExportSystem\BOMImporterTest @@ -50,14 +60,18 @@ class BOMImporter public function __construct( private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, - private readonly BOMValidationService $validationService + private readonly BOMValidationService $validationService, + private readonly PartRepository $partRepository, + private readonly ManufacturerRepository $manufacturerRepository, + private readonly CategoryRepository $categoryRepository, + private readonly DBElementRepository $assemblyBOMEntryRepository ) { } protected function configureOptions(OptionsResolver $resolver): OptionsResolver { $resolver->setRequired('type'); - $resolver->setAllowedValues('type', ['kicad_pcbnew', 'kicad_schematic']); + $resolver->setAllowedValues('type', ['kicad_pcbnew', 'kicad_schematic', 'json']); // For flexible schematic import with field mapping $resolver->setDefined(['field_mapping', 'field_priorities', 'delimiter']); @@ -87,13 +101,30 @@ public function importFileIntoProject(File $file, Project $project, array $optio return $bom_entries; } + /** + * Converts the given file into an array of BOM entries using the given options and save them into the given assembly. + * The changes are not saved into the database yet. + * @return AssemblyBOMEntry[] + */ + public function importFileIntoAssembly(File $file, Assembly $assembly, array $options): array + { + $bomEntries = $this->fileToBOMEntries($file, $options, AssemblyBOMEntry::class); + + //Assign the bom_entries to the assembly + foreach ($bomEntries as $bom_entry) { + $assembly->addBomEntry($bom_entry); + } + + return $bomEntries; + } + /** * Converts the given file into an array of BOM entries using the given options. - * @return ProjectBOMEntry[] + * @return ProjectBOMEntry[]|AssemblyBOMEntry[] */ - public function fileToBOMEntries(File $file, array $options): array + public function fileToBOMEntries(File $file, array $options, string $objectType = ProjectBOMEntry::class): array { - return $this->stringToBOMEntries($file->getContent(), $options); + return $this->stringToBOMEntries($file->getContent(), $options, $objectType); } /** @@ -117,22 +148,22 @@ public function validateBOMData(string $data, array $options): array * Import string data into an array of BOM entries, which are not yet assigned to a project. * @param string $data The data to import * @param array $options An array of options - * @return ProjectBOMEntry[] An array of imported entries + * @return ProjectBOMEntry[]|AssemblyBOMEntry[] An array of imported entries */ - public function stringToBOMEntries(string $data, array $options): array + public function stringToBOMEntries(string $data, array $options, string $objectType = ProjectBOMEntry::class): array { $resolver = new OptionsResolver(); $resolver = $this->configureOptions($resolver); $options = $resolver->resolve($options); return match ($options['type']) { - 'kicad_pcbnew' => $this->parseKiCADPCB($data), + 'kicad_pcbnew' => $this->parseKiCADPCB($data, $objectType), 'kicad_schematic' => $this->parseKiCADSchematic($data, $options), default => throw new InvalidArgumentException('Invalid import type!'), }; } - private function parseKiCADPCB(string $data): array + private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntry::class): array { $csv = Reader::createFromString($data); $csv->setDelimiter(';'); @@ -158,8 +189,13 @@ private function parseKiCADPCB(string $data): array throw new \UnexpectedValueException('Quantity missing at line ' . ($offset + 1) . '!'); } - $bom_entry = new ProjectBOMEntry(); - $bom_entry->setName($entry['Designation'] . ' (' . $entry['Package'] . ')'); + $bom_entry = $objectType === ProjectBOMEntry::class ? new ProjectBOMEntry() : new AssemblyBOMEntry(); + if ($objectType === ProjectBOMEntry::class) { + $bom_entry->setName($entry['Designation'] . ' (' . $entry['Package'] . ')'); + } else { + $bom_entry->setName($entry['Designation']); + } + $bom_entry->setMountnames($entry['Designator'] ?? ''); $bom_entry->setComment($entry['Supplier and ref'] ?? ''); $bom_entry->setQuantity((float) ($entry['Quantity'] ?? 1)); @@ -227,6 +263,174 @@ private function validateKiCADSchematicData(string $data, array $options): array return $this->validationService->validateBOMEntries($mapped_entries, $options); } + private function parseJson(string $data, array $options = [], string $objectType = ProjectBOMEntry::class): array + { + $result = []; + + $data = json_decode($data, true); + + foreach ($data as $entry) { + // Check quantity + if (!isset($entry['quantity'])) { + throw new UnexpectedValueException('quantity missing'); + } + if (!is_float($entry['quantity']) || $entry['quantity'] <= 0) { + throw new UnexpectedValueException('quantity expected as float greater than 0.0'); + } + + // Check name + if (isset($entry['name']) && !is_string($entry['name'])) { + throw new UnexpectedValueException('name of part list entry expected as string'); + } + + // Check if part is assigned with relevant information + if (isset($entry['part'])) { + if (!is_array($entry['part'])) { + throw new UnexpectedValueException('The property "part" should be an array'); + } + + $partIdValid = isset($entry['part']['id']) && is_int($entry['part']['id']) && $entry['part']['id'] > 0; + $partNameValid = isset($entry['part']['name']) && is_string($entry['part']['name']) && trim($entry['part']['name']) !== ''; + $partMpnrValid = isset($entry['part']['mpnr']) && is_string($entry['part']['mpnr']) && trim($entry['part']['mpnr']) !== ''; + $partIpnValid = isset($entry['part']['ipn']) && is_string($entry['part']['ipn']) && trim($entry['part']['ipn']) !== ''; + + if (!$partIdValid && !$partNameValid && !$partMpnrValid && !$partIpnValid) { + throw new UnexpectedValueException( + 'The property "part" must have either assigned: "id" as integer greater than 0, "name", "mpnr", or "ipn" as non-empty string' + ); + } + + $part = $partIdValid ? $this->partRepository->findOneBy(['id' => $entry['part']['id']]) : null; + $part = $part ?? ($partMpnrValid ? $this->partRepository->findOneBy(['manufacturer_product_number' => trim($entry['part']['mpnr'])]) : null); + $part = $part ?? ($partIpnValid ? $this->partRepository->findOneBy(['ipn' => trim($entry['part']['ipn'])]) : null); + $part = $part ?? ($partNameValid ? $this->partRepository->findOneBy(['name' => trim($entry['part']['name'])]) : null); + + if ($part === null) { + $part = new Part(); + $part->setName($entry['part']['name']); + } + + if ($partNameValid && $part->getName() !== trim($entry['part']['name'])) { + throw new RuntimeException(sprintf('Part name does not match exact the given name. Given for import: %s, found part: %s', $entry['part']['name'], $part->getName())); + } + + if ($partIpnValid && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) { + throw new RuntimeException(sprintf('Part mpnr does not match exact the given mpnr. Given for import: %s, found part: %s', $entry['part']['mpnr'], $part->getManufacturerProductNumber())); + } + + if ($partIpnValid && $part->getIpn() !== trim($entry['part']['ipn'])) { + throw new RuntimeException(sprintf('Part ipn does not match exact the given ipn. Given for import: %s, found part: %s', $entry['part']['ipn'], $part->getIpn())); + } + + // Part: Description check + if (isset($entry['part']['description']) && !is_null($entry['part']['description'])) { + if (!is_string($entry['part']['description']) || trim($entry['part']['description']) === '') { + throw new UnexpectedValueException('The property path "part.description" must be a non-empty string if not null'); + } + } + $partDescription = $entry['part']['description'] ?? ''; + + // Part: Manufacturer check + $manufacturerIdValid = false; + $manufacturerNameValid = false; + if (array_key_exists('manufacturer', $entry['part'])) { + if (!is_array($entry['part']['manufacturer'])) { + throw new UnexpectedValueException('The property path "part.manufacturer" must be an array'); + } + + $manufacturerIdValid = isset($entry['part']['manufacturer']['id']) && is_int($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] > 0; + $manufacturerNameValid = isset($entry['part']['manufacturer']['name']) && is_string($entry['part']['manufacturer']['name']) && trim($entry['part']['manufacturer']['name']) !== ''; + + // Stellen sicher, dass mindestens eine Bedingung für manufacturer erfüllt sein muss + if (!$manufacturerIdValid && !$manufacturerNameValid) { + throw new UnexpectedValueException( + 'The property "manufacturer" must have either assigned: "id" as integer greater than 0, or "name" as non-empty string' + ); + } + } + + $manufacturer = $manufacturerIdValid ? $this->manufacturerRepository->findOneBy(['id' => $entry['part']['manufacturer']['id']]) : null; + $manufacturer = $manufacturer ?? ($manufacturerNameValid ? $this->manufacturerRepository->findOneBy(['name' => trim($entry['part']['manufacturer']['name'])]) : null); + + if ($manufacturer === null) { + throw new RuntimeException( + 'Manufacturer not found' + ); + } + + if ($manufacturerNameValid && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) { + throw new RuntimeException(sprintf('Manufacturer name does not match exact the given name. Given for import: %s, found manufacturer: %s', $entry['manufacturer']['name'], $manufacturer->getName())); + } + + // Part: Category check + $categoryIdValid = false; + $categoryNameValid = false; + if (array_key_exists('category', $entry['part'])) { + if (!is_array($entry['part']['category'])) { + throw new UnexpectedValueException('part.category must be an array'); + } + + $categoryIdValid = isset($entry['part']['category']['id']) && is_int($entry['part']['category']['id']) && $entry['part']['category']['id'] > 0; + $categoryNameValid = isset($entry['part']['category']['name']) && is_string($entry['part']['category']['name']) && trim($entry['part']['category']['name']) !== ''; + + if (!$categoryIdValid && !$categoryNameValid) { + throw new UnexpectedValueException( + 'The property "category" must have either assigned: "id" as integer greater than 0, or "name" as non-empty string' + ); + } + } + + $category = $categoryIdValid ? $this->categoryRepository->findOneBy(['id' => $entry['part']['category']['id']]) : null; + $category = $category ?? ($categoryNameValid ? $this->categoryRepository->findOneBy(['name' => trim($entry['part']['category']['name'])]) : null); + + if ($category === null) { + throw new RuntimeException( + 'Category not found' + ); + } + + if ($categoryNameValid && $category->getName() !== trim($entry['part']['category']['name'])) { + throw new RuntimeException(sprintf('Category name does not match exact the given name. Given for import: %s, found category: %s', $entry['category']['name'], $category->getName())); + } + + $part->setDescription($partDescription); + $part->setManufacturer($manufacturer); + $part->setCategory($category); + + if ($partMpnrValid) { + $part->setManufacturerProductNumber($entry['part']['mpnr'] ?? ''); + } + if ($partIpnValid) { + $part->setIpn($entry['part']['ipn'] ?? ''); + } + + if ($objectType === AssemblyBOMEntry::class) { + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['part' => $part]); + + if ($bomEntry === null) { + $name = isset($entry['name']) && $entry['name'] !== null ? trim($entry['name']) : ''; + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $name]); + + if ($bomEntry === null) { + $bomEntry = new AssemblyBOMEntry(); + } + } + } else { + $bomEntry = new ProjectBOMEntry(); + } + + $bomEntry->setQuantity($entry['quantity']); + $bomEntry->setName($entry['name'] ?? ''); + + $bomEntry->setPart($part); + } + + $result[] = $bomEntry; + } + + return $result; + } + /** * This function uses the order of the fields in the CSV files to make them locale independent. * @param array $entry @@ -243,7 +447,7 @@ private function normalizeColumnNames(array $entry): array } //@phpstan-ignore-next-line We want to keep this check just to be safe when something changes - $new_index = self::MAP_KICAD_PCB_FIELDS[$index] ?? throw new \UnexpectedValueException('Invalid field index!'); + $new_index = self::MAP_KICAD_PCB_FIELDS[$index] ?? throw new UnexpectedValueException('Invalid field index!'); $out[$new_index] = $field; } diff --git a/src/Services/ProjectSystem/ProjectBuildHelper.php b/src/Services/ProjectSystem/ProjectBuildHelper.php index 269c7e4c2..d7ba9e6c9 100644 --- a/src/Services/ProjectSystem/ProjectBuildHelper.php +++ b/src/Services/ProjectSystem/ProjectBuildHelper.php @@ -22,10 +22,13 @@ */ namespace App\Services\ProjectSystem; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Helpers\Assemblies\AssemblyBuildRequest; use App\Helpers\Projects\ProjectBuildRequest; +use App\Services\AssemblySystem\AssemblyBuildHelper; use App\Services\Parts\PartLotWithdrawAddHelper; /** @@ -33,8 +36,10 @@ */ class ProjectBuildHelper { - public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper) - { + public function __construct( + private readonly PartLotWithdrawAddHelper $withdrawAddHelper, + private readonly AssemblyBuildHelper $assemblyBuildHelper + ) { } /** @@ -66,12 +71,16 @@ public function getMaximumBuildableCount(Project $project): int $maximum_buildable_count = PHP_INT_MAX; foreach ($project->getBomEntries() as $bom_entry) { //Skip BOM entries without a part (as we can not determine that) - if (!$bom_entry->isPartBomEntry()) { + if (!$bom_entry->isPartBomEntry() && $bom_entry->getAssembly() === null) { continue; } //The maximum buildable count for the whole project is the minimum of all BOM entries - $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); + if ($bom_entry->getPart() !== null) { + $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); + } elseif ($bom_entry->getAssembly() !== null) { + $maximum_buildable_count = min($maximum_buildable_count, $this->assemblyBuildHelper->getMaximumBuildableCount($bom_entry->getAssembly())); + } } return $maximum_buildable_count; @@ -97,10 +106,10 @@ public function isBOMEntryBuildable(ProjectBOMEntry $bom_entry, int $number_of_b } /** - * Returns the project BOM entries for which parts are missing in the stock for the given number of builds + * Returns the project or assembly BOM entries for which parts are missing in the stock for the given number of builds * @param Project $project The project for which the BOM entries should be checked * @param int $number_of_builds How often should the project be build? - * @return ProjectBOMEntry[] + * @return ProjectBOMEntry[]|AssemblyBOMEntry[] */ public function getNonBuildableProjectBomEntries(Project $project, int $number_of_builds = 1): array { @@ -108,24 +117,29 @@ public function getNonBuildableProjectBomEntries(Project $project, int $number_o throw new \InvalidArgumentException('The number of builds must be greater than 0!'); } - $non_buildable_entries = []; + $nonBuildableEntries = []; foreach ($project->getBomEntries() as $bomEntry) { $part = $bomEntry->getPart(); //Skip BOM entries without a part (as we can not determine that) - if (!$part instanceof Part) { + if (!$part instanceof Part && $bomEntry->getAssembly() === null) { continue; } - $amount_sum = $part->getAmountSum(); + if ($bomEntry->getPart() !== null) { + $amount_sum = $part->getAmountSum(); - if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { - $non_buildable_entries[] = $bomEntry; + if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { + $nonBuildableEntries[] = $bomEntry; + } + } elseif ($bomEntry->getAssembly() !== null) { + $nonBuildableAssemblyEntries = $this->assemblyBuildHelper->getNonBuildableAssemblyBomEntries($bomEntry->getAssembly(), $number_of_builds); + $nonBuildableEntries = array_merge($nonBuildableEntries, $nonBuildableAssemblyEntries); } } - return $non_buildable_entries; + return $nonBuildableEntries; } /** @@ -133,22 +147,37 @@ public function getNonBuildableProjectBomEntries(Project $project, int $number_o * The ProjectBuildRequest has to be validated before!! * You have to flush changes to DB afterward */ - public function doBuild(ProjectBuildRequest $buildRequest): void + public function doBuild(ProjectBuildRequest $projectBuildRequest): void { - $message = $buildRequest->getComment(); - $message .= ' (Project build: '.$buildRequest->getProject()->getName().')'; + $message = $projectBuildRequest->getComment(); + $message .= ' (Project build: '.$projectBuildRequest->getProject()->getName().')'; - foreach ($buildRequest->getPartBomEntries() as $bom_entry) { - foreach ($buildRequest->getPartLotsForBOMEntry($bom_entry) as $part_lot) { - $amount = $buildRequest->getLotWithdrawAmount($part_lot); + foreach ($projectBuildRequest->getPartBomEntries() as $bomEntry) { + foreach ($projectBuildRequest->getPartLotsForBOMEntry($bomEntry) as $partLot) { + $amount = $projectBuildRequest->getLotWithdrawAmount($partLot); if ($amount > 0) { - $this->withdraw_add_helper->withdraw($part_lot, $amount, $message); + $this->withdrawAddHelper->withdraw($partLot, $amount, $message); + } + } + } + + foreach ($projectBuildRequest->getAssemblyBomEntries() as $bomEntry) { + $assemblyBuildRequest = new AssemblyBuildRequest($bomEntry->getAssembly(), $projectBuildRequest->getNumberOfBuilds()); + + //Add fields for assembly bom entries + foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) { + foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $partLot) { + //Read amount from build configuration of the projectBuildRequest + $amount = $projectBuildRequest->getLotWithdrawAmount($partLot); + if ($amount > 0) { + $this->withdrawAddHelper->withdraw($partLot, $amount, $message); + } } } } - if ($buildRequest->getAddBuildsToBuildsPart()) { - $this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message); + if ($projectBuildRequest->getAddBuildsToBuildsPart()) { + $this->withdrawAddHelper->add($projectBuildRequest->getBuildsPartLot(), $projectBuildRequest->getNumberOfBuilds(), $message); } } } diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index f7a9d1c40..5f08b8183 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -22,6 +22,7 @@ namespace App\Services\Trees; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\AttachmentType; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; @@ -170,6 +171,12 @@ protected function getEditNodes(): array $this->urlGenerator->generate('project_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-archive'); } + if ($this->security->isGranted('read', new Assembly())) { + $nodes[] = (new TreeViewNode( + $this->translator->trans('tree.tools.edit.assemblies'), + $this->urlGenerator->generate('assembly_new') + ))->setIcon('fa-fw fa-treeview fa-solid fa-list'); + } if ($this->security->isGranted('read', new Supplier())) { $nodes[] = (new TreeViewNode( $this->translator->trans('tree.tools.edit.suppliers'), diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index 73ffa5baf..fa9935c8f 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -22,6 +22,7 @@ namespace App\Services\Trees; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; @@ -154,6 +155,10 @@ private function getTreeViewUncached( $href_type = 'list_parts'; } + if ($mode === 'assemblies') { + $href_type = 'list_parts'; + } + $generic = $this->getGenericTree($class, $parent); $treeIterator = new TreeViewNodeIterator($generic); $recursiveIterator = new RecursiveIteratorIterator($treeIterator, RecursiveIteratorIterator::SELF_FIRST); @@ -219,6 +224,7 @@ protected function entityClassToRootNodeString(string $class): string Manufacturer::class => $this->translator->trans('manufacturer.labelp'), Supplier::class => $this->translator->trans('supplier.labelp'), Project::class => $this->translator->trans('project.labelp'), + Assembly::class => $this->translator->trans('assembly.labelp'), default => $this->translator->trans('tree.root_node.text'), }; } @@ -233,6 +239,7 @@ protected function entityClassToRootNodeIcon(string $class): ?string Manufacturer::class => $icon.'fa-industry', Supplier::class => $icon.'fa-truck', Project::class => $icon.'fa-archive', + Assembly::class => $icon.'fa-list', default => null, }; } diff --git a/src/Twig/EntityExtension.php b/src/Twig/EntityExtension.php index 762ebb094..086b21c5a 100644 --- a/src/Twig/EntityExtension.php +++ b/src/Twig/EntityExtension.php @@ -22,6 +22,7 @@ */ namespace App\Twig; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Base\AbstractDBElement; use App\Entity\ProjectSystem\Project; @@ -108,6 +109,7 @@ public function getEntityType(object $entity): ?string Manufacturer::class => 'manufacturer', Category::class => 'category', Project::class => 'device', + Assembly::class => 'assembly', Attachment::class => 'attachment', Supplier::class => 'supplier', User::class => 'user', diff --git a/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequest.php b/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequest.php new file mode 100644 index 000000000..dd3bc19ee --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequest.php @@ -0,0 +1,37 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use Symfony\Component\Validator\Constraint; + +/** + * This constraint checks that the given ValidAssemblyBuildRequest is valid. + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class ValidAssemblyBuildRequest extends Constraint +{ + public function getTargets(): string + { + return self::CLASS_CONSTRAINT; + } +} diff --git a/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequestValidator.php b/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequestValidator.php new file mode 100644 index 000000000..9d8c2e56a --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequestValidator.php @@ -0,0 +1,84 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use App\Entity\Parts\PartLot; +use App\Helpers\Assemblies\AssemblyBuildRequest; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; + +class ValidAssemblyBuildRequestValidator extends ConstraintValidator +{ + private function buildViolationForLot(PartLot $partLot, string $message): ConstraintViolationBuilderInterface + { + return $this->context->buildViolation($message) + ->atPath('lot_' . $partLot->getID()) + ->setParameter('{{ lot }}', $partLot->getName()); + } + + public function validate($value, Constraint $constraint): void + { + if (!$constraint instanceof ValidAssemblyBuildRequest) { + throw new UnexpectedTypeException($constraint, ValidAssemblyBuildRequest::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!$value instanceof AssemblyBuildRequest) { + throw new UnexpectedTypeException($value, AssemblyBuildRequest::class); + } + + foreach ($value->getPartBomEntries() as $bom_entry) { + $withdraw_sum = $value->getWithdrawAmountSum($bom_entry); + $needed_amount = $value->getNeededAmountForBOMEntry($bom_entry); + + foreach ($value->getPartLotsForBOMEntry($bom_entry) as $lot) { + $withdraw_amount = $value->getLotWithdrawAmount($lot); + + if ($withdraw_amount < 0) { + $this->buildViolationForLot($lot, 'validator.assembly_build.lot_must_not_smaller_0') + ->addViolation(); + } + + if ($withdraw_amount > $lot->getAmount()) { + $this->buildViolationForLot($lot, 'validator.assembly_build.lot_must_not_bigger_than_stock') + ->addViolation(); + } + + if ($withdraw_sum > $needed_amount && $value->isDontCheckQuantity() === false) { + $this->buildViolationForLot($lot, 'validator.assembly_build.lot_bigger_than_needed') + ->addViolation(); + } + + if ($withdraw_sum < $needed_amount && $value->isDontCheckQuantity() === false) { + $this->buildViolationForLot($lot, 'validator.assembly_build.lot_smaller_than_needed') + ->addViolation(); + } + } + } + } +} diff --git a/templates/admin/assembly_admin.html.twig b/templates/admin/assembly_admin.html.twig new file mode 100644 index 000000000..d8b3ab257 --- /dev/null +++ b/templates/admin/assembly_admin.html.twig @@ -0,0 +1,62 @@ +{% extends "admin/base_admin.html.twig" %} + +{# @var entity App\Entity\AssemblySystem\Assembly #} + +{% block card_title %} + {% trans %}assembly.caption{% endtrans %} +{% endblock %} + +{% block edit_title %} + {% trans %}assembly.edit{% endtrans %}: {{ entity.name }} +{% endblock %} + +{% block new_title %} + {% trans %}assembly.new{% endtrans %} +{% endblock %} + +{% block additional_pills %} + +{% endblock %} + +{% block quick_links %} +
+
+ +
+
+{% endblock %} + +{% block additional_controls %} + {{ form_row(form.description) }} + {{ form_row(form.status) }} + {% if entity.id %} +
+ +
+ {% if entity.buildPart %} + {{ entity.buildPart.name }} + {% else %} + {% trans %}assembly.edit.associated_build_part.add{% endtrans %} + {% endif %} +

{% trans %}assembly.edit.associated_build.hint{% endtrans %}

+
+
+ {% endif %} + +{% endblock %} + +{% block additional_panes %} +
+ {% form_theme form.bom_entries with ['form/collection_types_layout_assembly.html.twig'] %} + {{ form_errors(form.bom_entries) }} + {{ form_widget(form.bom_entries) }} + {% if entity.id %} + + + {% trans %}assembly.edit.bom.import_bom{% endtrans %} + + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/admin/project_admin.html.twig b/templates/admin/project_admin.html.twig index 1a9950691..dcf8c64cf 100644 --- a/templates/admin/project_admin.html.twig +++ b/templates/admin/project_admin.html.twig @@ -36,7 +36,7 @@ {% if entity.buildPart %} {{ entity.buildPart.name }} {% else %} - {% trans %}project.edit.associated_build_part.add{% endtrans %} {% endif %}

{% trans %}project.edit.associated_build.hint{% endtrans %}

diff --git a/templates/assemblies/add_parts.html.twig b/templates/assemblies/add_parts.html.twig new file mode 100644 index 000000000..d8d8e657f --- /dev/null +++ b/templates/assemblies/add_parts.html.twig @@ -0,0 +1,22 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{% trans %}assembly.add_parts_to_assembly{% endtrans %}{% endblock %} + +{% block card_title %} + + {% trans %}assembly.add_parts_to_assembly{% endtrans %}{% if assembly %}: {{ assembly.name }}{% endif %} +{% endblock %} + +{% block card_content %} + + {{ form_start(form) }} + + {{ form_row(form.assembly) }} + {% form_theme form.bom_entries with ['form/collection_types_layout_assembly.html.twig'] %} + {{ form_widget(form.bom_entries) }} + + {{ form_row(form.submit) }} + + {{ form_end(form) }} + +{% endblock %} \ No newline at end of file diff --git a/templates/assemblies/build/_form.html.twig b/templates/assemblies/build/_form.html.twig new file mode 100644 index 000000000..0123ab010 --- /dev/null +++ b/templates/assemblies/build/_form.html.twig @@ -0,0 +1,88 @@ +{% import "helper.twig" as helper %} + +{{ form_start(form) }} + + + + + + + + + + + + {% for bom_entry in build_request.bomEntries %} + {# 1st row basic infos about the BOM entry #} + + + + + + + + + + {% endfor %} + +
+
+ +
+
{% trans %}part.table.name{% endtrans %}{% trans %}assembly.bom.mountnames{% endtrans %}{% trans %}assembly.build.required_qty{% endtrans %}
+
+ + {#
+
+ {% if bom_entry.part %} + {{ bom_entry.part.name }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} + {% else %} + {{ bom_entry.name }} + {% endif %} + + {% for tag in bom_entry.mountnames|split(',') %} + {{ tag | trim }} + {% endfor %} + + {{ build_request.neededAmountForBOMEntry(bom_entry) | format_amount(bom_entry.part.partUnit ?? null) }} {% trans %}assembly.builds.needed{% endtrans %} + (= {{ number_of_builds }} x {{ bom_entry.quantity | format_amount(bom_entry.part.partUnit ?? null) }}) +
+ {% set lots = build_request.partLotsForBOMEntry(bom_entry) %} + {% if lots is not null %} + {% for lot in lots %} + {# @var lot \App\Entity\Parts\PartLot #} +
+ +
+ {{ form_errors(form["lot_"~lot.id]) }} + {{ form_widget(form["lot_"~lot.id]) }} +
+
+ / {{ lot.amount | format_amount(lot.part.partUnit) }} {% trans %}assembly.builds.stocked{% endtrans %} +
+
+ {% endfor %} + {% endif %} +
+ +{{ form_row(form.comment) }} +
+{{ form_row(form.dontCheckQuantity) }} +
+ +{{ form_row(form.addBuildsToBuildsPart) }} +{% if form.buildsPartLot is defined %} + {{ form_row(form.buildsPartLot) }} +{% endif %} + +{{ form_row(form.submit) }} + +{{ form_end(form) }} \ No newline at end of file diff --git a/templates/assemblies/build/build.html.twig b/templates/assemblies/build/build.html.twig new file mode 100644 index 000000000..8f01607cb --- /dev/null +++ b/templates/assemblies/build/build.html.twig @@ -0,0 +1,40 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{% trans %}assembly.info.builds.label{% endtrans %}: {{ number_of_builds }}x {{ assembly.name }}{% endblock %} + +{% block card_title %} + + {% trans %}assembly.info.builds.label{% endtrans %}: {{ number_of_builds }}x {{ assembly.name }} +{% endblock %} + +{% block card_content %} + {% set can_build = buildHelper.assemblyBuildable(assembly, number_of_builds) %} + {% import "components/assemblies.macro.html.twig" as assembly_macros %} + + {% if assembly.status is not empty and assembly.status != "in_production" %} + + {% endif %} + + + +

{% trans %}assembly.build.help{% endtrans %}

+ + {% include 'assemblies/build/_form.html.twig' %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/assemblies/import_bom.html.twig b/templates/assemblies/import_bom.html.twig new file mode 100644 index 000000000..53168b438 --- /dev/null +++ b/templates/assemblies/import_bom.html.twig @@ -0,0 +1,60 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{% trans %}assembly.import_bom{% endtrans %}{% endblock %} + +{% block before_card %} + {% if errors %} +
+

{% trans %}parts.import.errors.title{% endtrans %}

+
    + {% for violation in errors %} +
  • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators') }} +
  • + {% endfor %} +
+
+ {% endif %} +{% endblock %} + + +{% block card_title %} + + {% trans %}assembly.import_bom{% endtrans %}{% if assembly %}: {{ assembly.name }}{% endif %} +{% endblock %} + +{% block card_content %} + {{ form(form) }} +{% endblock %} + +{% block additional_content %} +
+
+
+
+ {% trans %}assembly.import_bom.template.header.json{% endtrans %} +
+
+
{{ jsonTemplate|json_encode(constant('JSON_PRETTY_PRINT') b-or constant('JSON_UNESCAPED_UNICODE')) }}
+ + {{ 'assembly.bom_import.template.json.table'|trans|raw }} +
+
+
+
+
+
+ {% trans %}assembly.import_bom.template.header.kicad_pcbnew{% endtrans %} +
+
+ {{ 'assembly.bom_import.template.kicad_pcbnew.exptected_columns'|trans }} +
Id;Designator;Package;Quantity;Designation;Supplier and ref
+ {{ 'assembly.bom_import.template.kicad_pcbnew.exptected_columns.note'|trans|raw }} + + {{ 'assembly.bom_import.template.json.table'|trans|raw }} +
+
+
+
+{% endblock %} diff --git a/templates/assemblies/info/_bom.html.twig b/templates/assemblies/info/_bom.html.twig new file mode 100644 index 000000000..6a2ca3e03 --- /dev/null +++ b/templates/assemblies/info/_bom.html.twig @@ -0,0 +1,22 @@ +{% import "components/datatables.macro.html.twig" as datatables %} + + + +{{ datatables.datatable(datatable, 'elements/datatables/datatables', 'assemblies') }} \ No newline at end of file diff --git a/templates/assemblies/info/_builds.html.twig b/templates/assemblies/info/_builds.html.twig new file mode 100644 index 000000000..780c8c609 --- /dev/null +++ b/templates/assemblies/info/_builds.html.twig @@ -0,0 +1,40 @@ +{% set can_build = buildHelper.assemblyBuildable(assembly) %} + +{% import "components/assemblies.macro.html.twig" as assembly_macros %} + +{% if assembly.status is not empty and assembly.status != "in_production" %} + +{% endif %} + + + +
+
+
+
+ + + +
+
+
+
+ +{% if assembly.buildPart %} +

{% trans %}assembly.builds.no_stocked_builds{% endtrans %}: {{ assembly.buildPart.amountSum }}

+{% endif %} \ No newline at end of file diff --git a/templates/assemblies/info/_info.html.twig b/templates/assemblies/info/_info.html.twig new file mode 100644 index 000000000..495072b8a --- /dev/null +++ b/templates/assemblies/info/_info.html.twig @@ -0,0 +1,77 @@ +{% import "helper.twig" as helper %} + +
+
+
+
+ {% if assembly.masterPictureAttachment %} + + + + {% else %} + Part main image + {% endif %} +
+
+

{{ assembly.name }} + {# You need edit permission to use the edit button #} + {% if is_granted('edit', assembly) %} + + {% endif %} +

+
{{ assembly.description|format_markdown(true) }}
+ {% if assembly.buildPart %} +
{% trans %}assembly.edit.associated_build_part{% endtrans %}:
+ {{ assembly.buildPart.name }} + {% endif %} + +
+
+
+ + +
{# Sidebar panel with infos about last creation date, etc. #} +
+ + {{ helper.date_user_combination(assembly, true) }} + +
+ + {{ helper.date_user_combination(assembly, false) }} + +
+ +
+
+ {{ helper.assemblies_status_to_badge(assembly.status) }} +
+
+
+
+ + + {{ assembly.bomEntries | length }} + {% trans %}assembly.info.bom_entries_count{% endtrans %} + +
+
+ {% if assembly.children is not empty %} +
+
+ + + {{ assembly.children | length }} + {% trans %}assembly.info.sub_assemblies_count{% endtrans %} + +
+
+ {% endif %} +
+ + {% if assembly.comment is not empty %} +

+

{% trans %}comment.label{% endtrans %}:
+ {{ assembly.comment|format_markdown }} +

+ {% endif %} +
\ No newline at end of file diff --git a/templates/assemblies/info/_info_card.html.twig b/templates/assemblies/info/_info_card.html.twig new file mode 100644 index 000000000..508b2b069 --- /dev/null +++ b/templates/assemblies/info/_info_card.html.twig @@ -0,0 +1,133 @@ +{% import "helper.twig" as helper %} +{% import "label_system/dropdown_macro.html.twig" as dropdown %} + +{{ helper.breadcrumb_entity_link(assembly) }} + +
+
+
+ +
+
+
+ {% if assembly.description is not empty %} + {{ assembly.description|format_markdown }} + {% endif %} +
+ +
+
+
+
+
+
+ + {{ assembly.name }} +
+
+ + + {% if assembly.parent %} + {{ assembly.parent.fullPath }} + {% else %} + - + {% endif %} + +
+
+
+ {% block quick_links %}{% endblock %} + + + {% trans %}entity.edit.btn{% endtrans %} + +
+ + {{ assembly.lastModified | format_datetime("short") }} + +
+ + {{ assembly.addedDate | format_datetime("short") }} + +
+
+
+
+
+
+
+ + {{ assembly.children | length }} +
+
+ + {{ assembly.bomEntries | length }} +
+
+
+ + {% if assembly.attachments is not empty %} +
+ {% include "parts/info/_attachments_info.html.twig" with {"part": assembly} %} +
+ {% endif %} + + {% if assembly.parameters is not empty %} +
+ {% for name, parameters in assembly.groupedParameters %} + {% if name is not empty %}
{{ name }}
{% endif %} + {{ helper.parameters_table(assembly) }} + {% endfor %} +
+ {% endif %} + + {% if assembly.comment is not empty %} +
+
+ {{ assembly.comment|format_markdown }} +
+
+ {% endif %} +
+
+
+
+
+
+
\ No newline at end of file diff --git a/templates/assemblies/info/_subassemblies.html.twig b/templates/assemblies/info/_subassemblies.html.twig new file mode 100644 index 000000000..8c92c5e91 --- /dev/null +++ b/templates/assemblies/info/_subassemblies.html.twig @@ -0,0 +1,28 @@ + + + + + + + + + + + {% for subassembly in assembly.children %} + + + + + + + {% endfor %} + +
{% trans %}name.label{% endtrans %}{% trans %}description.label{% endtrans %}# {% trans %}assembly.info.bom_entries_count{% endtrans %}# {% trans %}assembly.info.sub_assemblies_count{% endtrans %}
{# Name #} + {{ subassembly.name }} + {# Description #} + {{ subassembly.description | format_markdown }} + + {{ subassembly.bomEntries | length }} + + {{ subassembly.children | length }} +
\ No newline at end of file diff --git a/templates/assemblies/info/info.html.twig b/templates/assemblies/info/info.html.twig new file mode 100644 index 000000000..f5dac1e63 --- /dev/null +++ b/templates/assemblies/info/info.html.twig @@ -0,0 +1,105 @@ +{% extends "main_card.html.twig" %} +{% import "helper.twig" as helper %} + +{% block title %} + {% trans %}assembly.info.title{% endtrans %}: {{ assembly.name }} +{% endblock %} + +{% block content %} + + {{ helper.breadcrumb_entity_link(assembly) }} + {{ parent() }} +{% endblock %} + +{% block card_title %} + {% if assembly.masterPictureAttachment is not null and attachment_manager.isFileExisting(assembly.masterPictureAttachment) %} + + {% else %} + {{ helper.entity_icon(assembly, "me-1") }} + {% endif %} + {% trans %}assembly.info.title{% endtrans %}: {{ assembly.name }} +{% endblock %} + +{% block card_content %} + + +
+
+ {% include "assemblies/info/_info.html.twig" %} +
+ {% if assembly.children is not empty %} +
+ {% include "assemblies/info/_subassemblies.html.twig" %} +
+ {% endif %} +
+ {% include "assemblies/info/_bom.html.twig" %} +
+
+ {% include "assemblies/info/_builds.html.twig" %} +
+
+ {% include "parts/info/_attachments_info.html.twig" with {"part": assembly} %} +
+
+ {% for name, parameters in assembly.groupedParameters %} + {% if name is not empty %}
{{ name }}
{% endif %} + {{ helper.parameters_table(assembly.parameters) }} + {% endfor %} +
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/components/assemblies.macro.html.twig b/templates/components/assemblies.macro.html.twig new file mode 100644 index 000000000..d59005e05 --- /dev/null +++ b/templates/components/assemblies.macro.html.twig @@ -0,0 +1,8 @@ +{% macro assembly_bom_entry_with_missing_instock(assembly_bom_entry, number_of_builds = 1) %} + {# @var \App\Entity\AssemblySystem\AssemblyBOMEntry assembly_bom_entry #} + {{ assembly_bom_entry.part.name }} + {% if assembly_bom_entry.name %} ({{ assembly_bom_entry.name }}){% endif %}: + {{ assembly_bom_entry.part.amountSum | format_amount(assembly_bom_entry.part.partUnit) }} {% trans %}assembly.builds.stocked{% endtrans %} + / + {{ (assembly_bom_entry.quantity * number_of_builds) | format_amount(assembly_bom_entry.part.partUnit) }} {% trans %}assembly.builds.needed{% endtrans %} +{% endmacro %} \ No newline at end of file diff --git a/templates/components/tree_macros.html.twig b/templates/components/tree_macros.html.twig index 366d42fe8..2e55147a1 100644 --- a/templates/components/tree_macros.html.twig +++ b/templates/components/tree_macros.html.twig @@ -7,6 +7,7 @@ ['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')], ['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')], ['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')], + ['assembly', path('tree_assembly_root'), 'assembly.labelp', is_granted('@assemblies.read')], ['tools', path('tree_tools'), 'tools.label', true], ] %} diff --git a/templates/form/collection_types_layout.html.twig b/templates/form/collection_types_layout.html.twig index 96b71bf0f..def235000 100644 --- a/templates/form/collection_types_layout.html.twig +++ b/templates/form/collection_types_layout.html.twig @@ -6,7 +6,7 @@ {# expand button #} {% trans %}project.bom.quantity{% endtrans %} - {% trans %}project.bom.part{% endtrans %} + {% trans %}project.bom.partOrAssembly{% endtrans %} {% trans %}project.bom.name{% endtrans %} {# Remove button #} @@ -41,9 +41,11 @@ {{ form_widget(form.quantity) }} {{ form_errors(form.quantity) }} - - {{ form_widget(form.part) }} + + {{ form_row(form.part) }} {{ form_errors(form.part) }} + {{ form_widget(form.assembly) }} + {{ form_errors(form.assembly) }} {{ form_widget(form.name) }} diff --git a/templates/form/collection_types_layout_assembly.html.twig b/templates/form/collection_types_layout_assembly.html.twig new file mode 100644 index 000000000..c5acebda0 --- /dev/null +++ b/templates/form/collection_types_layout_assembly.html.twig @@ -0,0 +1,80 @@ +{% block assembly_bom_entry_collection_widget %} + {% import 'components/collection_type.macro.html.twig' as collection %} +
+ + + + {# expand button #} + + + + {# Remove button #} + + + + + {% for entry in form %} + {{ form_widget(entry) }} + {% endfor %} + +
{% trans %}assembly.bom.quantity{% endtrans %}{% trans %}assembly.bom.part{% endtrans %}{% trans %}assembly.bom.name{% endtrans %}
+ + +
+ +{% endblock %} + +{% block assembly_bom_entry_widget %} + {% set target_id = 'expand_row-' ~ form.vars.name %} + + {% import 'components/collection_type.macro.html.twig' as collection %} + + + + + + {{ form_widget(form.quantity) }} + {{ form_errors(form.quantity) }} + + + {{ form_widget(form.part) }} + {{ form_errors(form.part) }} + + + {{ form_widget(form.name) }} + {{ form_errors(form.name) }} + + + + {{ form_errors(form) }} + + + + + +
+ {{ form_row(form.mountnames) }} +
+ +
+
+ {{ form_widget(form.price) }} + {{ form_widget(form.priceCurrency) }} +
+ {{ form_errors(form.price) }} + {{ form_errors(form.priceCurrency) }} +
+
+ {{ form_row(form.comment) }} +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/helper.twig b/templates/helper.twig index bd1d2aa7a..3ddb4f7fa 100644 --- a/templates/helper.twig +++ b/templates/helper.twig @@ -76,6 +76,21 @@ {% endif %} {% endmacro %} +{% macro assemblies_status_to_badge(status, class="badge") %} + {% if status is not empty %} + {% set color = " bg-secondary" %} + + {% if status == "in_production" %} + {% set color = " bg-success" %} + {% endif %} + + + + {{ ("assembly.status." ~ status) | trans }} + + {% endif %} +{% endmacro %} + {% macro structural_entity_link(entity, link_type = "list_parts") %} {# @var entity \App\Entity\Base\StructuralDBElement #} {% if entity %} @@ -101,6 +116,7 @@ "category": ["fa-solid fa-tags", "category.label"], "currency": ["fa-solid fa-coins", "currency.label"], "device": ["fa-solid fa-archive", "project.label"], + "assembly": ["fa-solid fa-list", "assembly.label"], "footprint": ["fa-solid fa-microchip", "footprint.label"], "group": ["fa-solid fa-users", "group.label"], "label_profile": ["fa-solid fa-qrcode", "label_profile.label"], diff --git a/templates/projects/build/_form.html.twig b/templates/projects/build/_form.html.twig index a8f772e97..340b86700 100644 --- a/templates/projects/build/_form.html.twig +++ b/templates/projects/build/_form.html.twig @@ -27,7 +27,9 @@ {% if bom_entry.part %} - {{ bom_entry.part.name }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} + {{ 'projects.build.form.part'|trans({'%name%': bom_entry.part.name}) }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} + {% elseif bom_entry.assembly %} + {{ 'projects.build.form.assembly'|trans({'%name%': bom_entry.assembly.name}) }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} {% else %} {{ bom_entry.name }} {% endif %} @@ -45,9 +47,29 @@ {% set lots = build_request.partLotsForBOMEntry(bom_entry) %} + {% set assemblyBomEntriesWithoutPart = build_request.assemblyBomEntriesWithoutPart(bom_entry) %} + {% set assemblyBomEntriesWithPartNoStock = build_request.assemblyBomEntriesWithPartNoStock(bom_entry) %} {% if lots is not null %} + {% set previousLabel = null %} + {% for lot in lots %} {# @var lot \App\Entity\Parts\PartLot #} + + {% set label = '' %} + {% if form["lot_"~lot.id].vars.label is defined and form["lot_"~lot.id].vars.label is not empty %} + {% set label = form["lot_"~lot.id].vars.label %} + {% endif %} + + {% if label != '' and (previousLabel is null or label != previousLabel) %} +
+ +
+ {% endif %} + + {% set previousLabel = label %} +
-
+
/ {{ lot.amount | format_amount(lot.part.partUnit) }} {% trans %}project.builds.stocked{% endtrans %}
{% endfor %} {% endif %} + {% if assemblyBomEntriesWithoutPart is not null %} + {% for bomEntryWithoutPart in assemblyBomEntriesWithoutPart %} +
+ +
+
+ / {% trans %}project.builds.no_stock{% endtrans %} +
+
+ {% endfor %} + {% endif %} + {% if assemblyBomEntriesWithPartNoStock is not null %} + {% for bomEntryWithPartNoStock in assemblyBomEntriesWithPartNoStock %} +
+
+ +
+
+ / {% trans %}project.builds.no_stock{% endtrans %} +
+
+
+ {% endfor %} + {% endif %} {% endfor %} diff --git a/tests/Entity/Attachments/AttachmentTest.php b/tests/Entity/Attachments/AttachmentTest.php index 00a68d7d4..699648eb7 100644 --- a/tests/Entity/Attachments/AttachmentTest.php +++ b/tests/Entity/Attachments/AttachmentTest.php @@ -24,6 +24,8 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Depends; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentTypeAttachment; @@ -81,6 +83,7 @@ public static function subClassesDataProvider(): \Iterator yield [CategoryAttachment::class, Category::class]; yield [CurrencyAttachment::class, Currency::class]; yield [ProjectAttachment::class, Project::class]; + yield [AssemblyAttachment::class, Assembly::class]; yield [FootprintAttachment::class, Footprint::class]; yield [GroupAttachment::class, Group::class]; yield [ManufacturerAttachment::class, Manufacturer::class]; diff --git a/tests/Helpers/Assemblies/AssemblyBuildRequestTest.php b/tests/Helpers/Assemblies/AssemblyBuildRequestTest.php new file mode 100644 index 000000000..210e33018 --- /dev/null +++ b/tests/Helpers/Assemblies/AssemblyBuildRequestTest.php @@ -0,0 +1,177 @@ +. + */ +namespace App\Tests\Helpers\Assemblies; + +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Helpers\Assemblies\AssemblyBuildRequest; +use PHPUnit\Framework\TestCase; + +class AssemblyBuildRequestTest extends TestCase +{ + + /** @var MeasurementUnit $float_unit */ + private MeasurementUnit $float_unit; + + /** @var Assembly */ + private Assembly $assembly1; + /** @var AssemblyBOMEntry */ + private AssemblyBOMEntry $bom_entry1a; + /** @var AssemblyBOMEntry */ + private AssemblyBOMEntry $bom_entry1b; + /** @var AssemblyBOMEntry */ + private AssemblyBOMEntry $bom_entry1c; + + private PartLot $lot1a; + private PartLot $lot1b; + private PartLot $lot2; + + /** @var Part */ + private Part $part1; + /** @var Part */ + private Part $part2; + + + public function setUp(): void + { + $this->float_unit = new MeasurementUnit(); + $this->float_unit->setName('float'); + $this->float_unit->setUnit('f'); + $this->float_unit->setIsInteger(false); + $this->float_unit->setUseSIPrefix(true); + + //Setup some example parts and part lots + $this->part1 = new Part(); + $this->part1->setName('Part 1'); + $this->lot1a = new class extends PartLot { + public function getID(): ?int + { + return 1; + } + }; + $this->part1->addPartLot($this->lot1a); + $this->lot1a->setAmount(10); + $this->lot1a->setDescription('Lot 1a'); + + $this->lot1b = new class extends PartLot { + public function getID(): ?int + { + return 2; + } + }; + $this->part1->addPartLot($this->lot1b); + $this->lot1b->setAmount(20); + $this->lot1b->setDescription('Lot 1b'); + + $this->part2 = new Part(); + + $this->part2->setName('Part 2'); + $this->part2->setPartUnit($this->float_unit); + $this->lot2 = new PartLot(); + $this->part2->addPartLot($this->lot2); + $this->lot2->setAmount(2.5); + $this->lot2->setDescription('Lot 2'); + + $this->bom_entry1a = new AssemblyBOMEntry(); + $this->bom_entry1a->setPart($this->part1); + $this->bom_entry1a->setQuantity(2); + + $this->bom_entry1b = new AssemblyBOMEntry(); + $this->bom_entry1b->setPart($this->part2); + $this->bom_entry1b->setQuantity(1.5); + + $this->bom_entry1c = new AssemblyBOMEntry(); + $this->bom_entry1c->setName('Non-part BOM entry'); + $this->bom_entry1c->setQuantity(4); + + + $this->assembly1 = new Assembly(); + $this->assembly1->setName('Assembly 1'); + $this->assembly1->addBomEntry($this->bom_entry1a); + $this->assembly1->addBomEntry($this->bom_entry1b); + $this->assembly1->addBomEntry($this->bom_entry1c); + } + + public function testInitialization(): void + { + //The values should be already prefilled correctly + $request = new AssemblyBuildRequest($this->assembly1, 10); + //We need totally 20: Take 10 from the first (maximum 10) and 10 from the second (maximum 20) + $this->assertEqualsWithDelta(10.0, $request->getLotWithdrawAmount($this->lot1a), PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(10.0, $request->getLotWithdrawAmount($this->lot1b), PHP_FLOAT_EPSILON); + + //If the needed amount is higher than the maximum, we should get the maximum + $this->assertEqualsWithDelta(2.5, $request->getLotWithdrawAmount($this->lot2), PHP_FLOAT_EPSILON); + } + + public function testGetNumberOfBuilds(): void + { + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + $this->assertSame(5, $build_request->getNumberOfBuilds()); + } + + public function testGetAssembly(): void + { + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + $this->assertEquals($this->assembly1, $build_request->getAssembly()); + } + + public function testGetNeededAmountForBOMEntry(): void + { + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + $this->assertEqualsWithDelta(10.0, $build_request->getNeededAmountForBOMEntry($this->bom_entry1a), PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(7.5, $build_request->getNeededAmountForBOMEntry($this->bom_entry1b), PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(20.0, $build_request->getNeededAmountForBOMEntry($this->bom_entry1c), PHP_FLOAT_EPSILON); + } + + public function testGetSetLotWithdrawAmount(): void + { + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + + //We can set the amount for a lot either via the lot object or via the ID + $build_request->setLotWithdrawAmount($this->lot1a, 2); + $build_request->setLotWithdrawAmount($this->lot1b->getID(), 3); + + //And it should be possible to get the amount via the lot object or via the ID + $this->assertEqualsWithDelta(2.0, $build_request->getLotWithdrawAmount($this->lot1a->getID()), PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(3.0, $build_request->getLotWithdrawAmount($this->lot1b), PHP_FLOAT_EPSILON); + } + + public function testGetWithdrawAmountSum(): void + { + //The sum of all withdraw amounts for an BOM entry (over all lots of the associated part) should be correct + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + + $build_request->setLotWithdrawAmount($this->lot1a, 2); + $build_request->setLotWithdrawAmount($this->lot1b, 3); + + $this->assertEqualsWithDelta(5.0, $build_request->getWithdrawAmountSum($this->bom_entry1a), PHP_FLOAT_EPSILON); + $build_request->setLotWithdrawAmount($this->lot2, 1.5); + $this->assertEqualsWithDelta(1.5, $build_request->getWithdrawAmountSum($this->bom_entry1b), PHP_FLOAT_EPSILON); + } + + +} diff --git a/tests/Services/AssemblySystem/AssemblyBuildHelperTest.php b/tests/Services/AssemblySystem/AssemblyBuildHelperTest.php new file mode 100644 index 000000000..c513ed8de --- /dev/null +++ b/tests/Services/AssemblySystem/AssemblyBuildHelperTest.php @@ -0,0 +1,117 @@ +. + */ +namespace App\Tests\Services\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Services\AssemblySystem\AssemblyBuildHelper; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class AssemblyBuildHelperTest extends WebTestCase +{ + /** @var AssemblyBuildHelper */ + protected $service; + + protected function setUp(): void + { + self::bootKernel(); + $this->service = self::getContainer()->get(AssemblyBuildHelper::class); + } + + public function testGetMaximumBuildableCountForBOMEntryNonPartBomEntry(): void + { + $bom_entry = new AssemblyBOMEntry(); + $bom_entry->setPart(null); + $bom_entry->setQuantity(10); + $bom_entry->setName('Test'); + + $this->expectException(\InvalidArgumentException::class); + $this->service->getMaximumBuildableCountForBOMEntry($bom_entry); + } + + public function testGetMaximumBuildableCountForBOMEntry(): void + { + $assembly_bom_entry = new AssemblyBOMEntry(); + $assembly_bom_entry->setQuantity(10); + + $part = new Part(); + $lot1 = new PartLot(); + $lot1->setAmount(120); + $lot2 = new PartLot(); + $lot2->setAmount(5); + $part->addPartLot($lot1); + $part->addPartLot($lot2); + + $assembly_bom_entry->setPart($part); + + //We have 125 parts in stock, so we can build 12 times the assembly (125 / 10 = 12.5) + $this->assertSame(12, $this->service->getMaximumBuildableCountForBOMEntry($assembly_bom_entry)); + + + $lot1->setAmount(0); + //We have 5 parts in stock, so we can build 0 times the assembly (5 / 10 = 0.5) + $this->assertSame(0, $this->service->getMaximumBuildableCountForBOMEntry($assembly_bom_entry)); + } + + public function testGetMaximumBuildableCount(): void + { + $assembly = new Assembly(); + + $assembly_bom_entry1 = new AssemblyBOMEntry(); + $assembly_bom_entry1->setQuantity(10); + $part = new Part(); + $lot1 = new PartLot(); + $lot1->setAmount(120); + $lot2 = new PartLot(); + $lot2->setAmount(5); + $part->addPartLot($lot1); + $part->addPartLot($lot2); + $assembly_bom_entry1->setPart($part); + $assembly->addBomEntry($assembly_bom_entry1); + + $assembly_bom_entry2 = new AssemblyBOMEntry(); + $assembly_bom_entry2->setQuantity(5); + $part2 = new Part(); + $lot3 = new PartLot(); + $lot3->setAmount(10); + $part2->addPartLot($lot3); + $assembly_bom_entry2->setPart($part2); + $assembly->addBomEntry($assembly_bom_entry2); + + $assembly->addBomEntry((new AssemblyBOMEntry())->setName('Non part entry')->setQuantity(1)); + + //Restricted by the few parts in stock of part2 + $this->assertSame(2, $this->service->getMaximumBuildableCount($assembly)); + + $lot3->setAmount(1000); + //Now the build count is restricted by the few parts in stock of part1 + $this->assertSame(12, $this->service->getMaximumBuildableCount($assembly)); + + $lot3->setAmount(0); + //Now the build count must be 0, as we have no parts in stock + $this->assertSame(0, $this->service->getMaximumBuildableCount($assembly)); + + } +} diff --git a/tests/Services/AssemblySystem/AssemblyBuildPartHelperTest.php b/tests/Services/AssemblySystem/AssemblyBuildPartHelperTest.php new file mode 100644 index 000000000..b8aa0ddc3 --- /dev/null +++ b/tests/Services/AssemblySystem/AssemblyBuildPartHelperTest.php @@ -0,0 +1,52 @@ +. + */ +namespace App\Tests\Services\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use App\Services\AssemblySystem\AssemblyBuildPartHelper; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class AssemblyBuildPartHelperTest extends WebTestCase +{ + /** @var AssemblyBuildPartHelper */ + protected $service; + + protected function setUp(): void + { + self::bootKernel(); + $this->service = self::getContainer()->get(AssemblyBuildPartHelper::class); + } + + public function testGetPartInitialization(): void + { + $assembly = new Assembly(); + $assembly->setName('Assembly 1'); + $assembly->setDescription('Description 1'); + + $part = $this->service->getPartInitialization($assembly); + $this->assertSame('Assembly 1', $part->getName()); + $this->assertSame('Description 1', $part->getDescription()); + $this->assertSame($assembly, $part->getBuiltAssembly()); + $this->assertSame($part, $assembly->getBuildPart()); + } +} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 1f234450e..f5823e8d8 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -4741,6 +4741,18 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Název + + + project.bom.assembly + Sestava + + + + + project.bom.partOrAssembly + Výběr + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9786,6 +9798,18 @@ Element 3 Díl + + + project.bom.assembly + Baugruppe + + + + + project.bom.partOrAssembly + Auswahl + + project.bom.add_entry @@ -9864,6 +9888,42 @@ Element 3 Archivováno + + + assembly.edit.status + Stav + + + + + assembly.status.draft + Návrh + + + + + assembly.status.planning + Plánování + + + + + assembly.status.in_production + Ve výrobě + + + + + assembly.status.finished + Dokončeno + + + + + assembly.status.archived + Archivováno + + part.new_build_part.error.build_part_already_exists @@ -10140,6 +10200,12 @@ Element 3 k dispozici + + + project.builds.no_stock + není uveden žádný sklad + + project.builds.needed @@ -10212,6 +10278,12 @@ Element 3 Cílový inventář + + + project.build.builds_part_lot_label + %name% (%quantity% požadováno) + + project.builds.number_of_builds @@ -13035,6 +13107,634 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Z bezpečnostních důvodů redigováno + + + part.table.name.value.for_part + %value% (Součást) + + + + + part.table.name.value.for_assembly + %value% (Sestava) + + + + + assembly.label + Sestava + + + + + assembly.caption + Sestava + + + + + perm.assemblies + Sestavy + + + + + assembly_bom_entry.label + Součásti + + + + + assembly.labelp + Sestavy + + + + + assembly.edit + Upravit sestavu + + + + + assembly.new + Nová sestava + + + + + assembly.edit.associated_build_part + Přidružená součást + + + + + assembly.edit.associated_build_part.add + Přidat součást + + + + + assembly.edit.associated_build.hint + Tato součást představuje vyrobené instance sestavy. Zadejte, pokud jsou vyrobené instance potřeba. Pokud ne, počet součástí bude použit až při sestavení daného projektu. + + + + + assembly.edit.bom.import_bom + Importovat součásti + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Sestavy + + + + + assembly.bom_import.flash.success + %count% součástí úspěšně importováno do sestavy. + + + + + assembly.bom_import.flash.invalid_entries + Chyba ověření! Zkontrolujte svůj importovaný soubor! + + + + + assembly.bom_import.flash.invalid_file + Soubor nelze importovat. Zkontrolujte, zda jste vybrali správný typ souboru. Chybová zpráva: %message% + + + + + assembly.bom.quantity + Množství + + + + + assembly.bom.mountnames + Názvy osazení + + + + + assembly.bom.instockAmount + Stav na skladě + + + + + assembly.info.title + Info o sestavě + + + + + assembly.info.info.label + Informace + + + + + assembly.info.sub_assemblies.label + Podskupina + + + + + assembly.info.builds.label + Sestavení + + + + + assembly.info.bom_add_parts + Přidat součásti + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Měli byste zkontrolovat, zda opravdu chcete sestavu postavit s tímto stavem!]]> + + + + + assembly.builds.build_not_possible + Sestavení není možné: Nedostatek součástí + + + + + assembly.builds.following_bom_entries_miss_instock + Není dostatek součástí na skladě pro postavení tohoto projektu %number_of_builds% krát. Následující součásti nejsou skladem v dostatečném množství. + + + + + assembly.builds.build_possible + Sestavení je možné + + + + + assembly.builds.number_of_builds_possible + %max_builds% kusů této sestavy.]]> + + + + + assembly.builds.number_of_builds + Počet sestavení + + + + + assembly.build.btn_build + Sestavit + + + + + assembly.builds.no_stocked_builds + Počet skladovaných vyrobených instancí + + + + + assembly.info.bom_entries_count + Součásti + + + + + assembly.info.sub_assemblies_count + Podskupiny + + + + + assembly.builds.stocked + skladem + + + + + assembly.builds.needed + potřebné + + + + + assembly.add_parts_to_assembly + Přidat součásti do sestavy + + + + + assembly.bom.name + Název + + + + + assembly.bom.comment + Poznámky + + + + + assembly.builds.following_bom_entries_miss_instock_n + Není dostatek součástí na skladě pro sestavení této sestavy %number_of_builds% krát. Následující součásti nejsou skladem: + + + + + assembly.build.help + Vyberte, ze kterých zásob se mají brát potřebné součásti pro sestavení (a v jakém množství). Zaškrtněte políčko u každého dílu, pokud jste jej odebrali, nebo použijte horní políčko k výběru všech naráz. + + + + + assembly.build.required_qty + Požadované množství + + + + + assembly.import_bom + Importovat součásti do sestavy + + + + + assembly.bom.part + Součást + + + + + assembly.bom.add_entry + Přidat položku + + + + + assembly.bom.price + Cena + + + + + assembly.build.dont_check_quantity + Neověřovat množství + + + + + assembly.build.dont_check_quantity.help + Pokud je tato volba vybrána, budou vybraná množství odstraněna ze skladu bez ohledu na to, zda je méně nebo více součástí, než je skutečně potřeba pro sestavení sestavy. + + + + + assembly.build.add_builds_to_builds_part + Přidat vyrobené instance do součásti sestavy + + + + + assembly.bom_import.type + Typ + + + + + assembly.bom_import.type.json + JSON pro sestavu + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Smazat existující položky před importem + + + + + assembly.bom_import.clear_existing_bom.help + Pokud je tato možnost vybrána, budou všechny již existující součásti sestavy smazány a nahrazeny importovanými daty součástí. + + + + + assembly.import_bom.template.header.json + Šablona importu JSON pro sestavu + + + + + assembly.import_bom.template.header.kicad_pcbnew + Šablona importu CSV (KiCAD Pcbnew BOM) pro sestavu + + + + + assembly.bom_import.template.entry.name + Název součásti v sestavě + + + + + assembly.bom_import.template.entry.part.mpnr + Unikátní číslo produktu u výrobce + + + + + assembly.bom_import.template.entry.part.ipn + Unikátní IPN součásti + + + + + assembly.bom_import.template.entry.part.name + Unikátní název součásti + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Unikátní jméno výrobce + + + + + assembly.bom_import.template.entry.part.category.name + Unikátní název kategorie + + + + + assembly.bom_import.template.json.table + + + + + Pole + Podmínka + Datový typ + Popis + + + + + quantity + Povinné + Číslo s plovoucí desetinnou čárkou (Float) + Musí být uvedeno a obsahovat hodnotu s plovoucí desetinnou čárkou (Float) větší než 0,0. + + + name + Volitelné + Řetězec (String) + Pokud je přítomen, musí být neprázdný řetězec. + + + part + Volitelné + Objekt/Array + + Pokud je uvedeno, musí to být objekt/array a minimálně jedno pole musí být vyplněno: +
    +
  • part.id
  • +
  • part.name
  • +
+ + + + part.id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID součástky v Part-DB. + + + part.name + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není zadáno part.mpnr nebo part.ipn. + + + part.mpnr + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není zadáno part.name nebo part.ipn. + + + part.ipn + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není zadáno part.name nebo part.mpnr. + + + part.description + Volitelné + Řetězec nebo null + Pokud je přítomen, musí být neprázdný řetězec nebo null. + + + part.manufacturer + Volitelné + Objekt/Array + + Pokud je přítomen, musí to být objekt/array a minimálně jedno pole musí být vyplněno: +
    +
  • manufacturer.id
  • +
  • manufacturer.name
  • +
+ + + + manufacturer.id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID výrobce. + + + manufacturer.name + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno manufacturer.id. + + + part.category + Volitelné + Objekt/Array + + Pokud je přítomen, musí to být objekt/array a minimálně jedno pole musí být vyplněno: +
    +
  • category.id
  • +
  • category.name
  • +
+ + + + category.id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID kategorie součástky. + + + category.name + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno category.id. + + + + ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Očekávané sloupce: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Poznámka: Neprobíhá přiřazení ke konkrétním součástem ze správy kategorií.

+ ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Pole + Podmínka + Datový typ + Popis + + + + + Id + Volitelný + Celé číslo (Integer) + Volný údaj. Jedinečné identifikační číslo pro každou součástku. + + + Designator + Volitelný + Řetězec (String) + Volný údaj. Jedinečný referenční označovač součástky na desce plošných spojů, např. „R1“ pro rezistor 1. Používá se pro název osazení součástky v rámci skupiny součástek. + + + Package + Volitelný + Řetězec (String) + Volný údaj. Pouzdro nebo tvar součástky, např. „0805“ pro SMD rezistory. + + + Množství + Povinný + Celé číslo (Integer) + Počet identických součástek, které jsou potřeba k vytvoření jedné instance sestavy. + + + Určení + Povinný + Řetězec (String) + Popis nebo funkce součástky, např. hodnota rezistoru „10kΩ“ nebo hodnota kondenzátoru „100nF“. Používá se pro název položky v BOM. + + + Dodavatel a ref + Volitelný + Řetězec (String) + Volný údaj. Může obsahovat např. specifické údaje distributora. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (součást) + + + + + typeahead.parts.assembly.name + %name% (sestava) + + + + + projects.build.form.part + Součást "%name%" + + + + + projects.build.form.assembly + Sestava "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (potřebné množství: %quantity%) + + + + + projects.build.form.assembly.bom.entry.no.stock + není skladem + + project.bom_import.map_fields diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index d72589864..f231163a1 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -4748,6 +4748,18 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Navn + + + project.bom.assembly + Montering + + + + + project.bom.partOrAssembly + Valg + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9812,6 +9824,18 @@ Element 3 Komponent + + + project.bom.assembly + Baugruppe + + + + + project.bom.partOrAssembly + Auswahl + + project.bom.add_entry @@ -9890,6 +9914,42 @@ Element 3 Arkiveret + + + assembly.edit.status + Status + + + + + assembly.status.draft + Kladde + + + + + assembly.status.planning + Under planlægning + + + + + assembly.status.in_production + I produktion + + + + + assembly.status.finished + Ophørt + + + + + assembly.status.archived + Arkiveret + + part.new_build_part.error.build_part_already_exists @@ -10166,6 +10226,12 @@ Element 3 På lager + + + project.builds.no_stock + intet lager angivet + + project.builds.needed @@ -10238,6 +10304,12 @@ Element 3 Mål mængde + + + project.build.builds_part_lot_label + %name% (%quantity% påkrævet) + + project.builds.number_of_builds @@ -12196,5 +12268,633 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Du forsøgte at fjerne/tilføje en mængde sat til nul! Der blev ikke foretaget nogen handling. + + + part.table.name.value.for_part + %value% (Del) + + + + + part.table.name.value.for_assembly + %value% (Samlingsenhed) + + + + + assembly.label + Samling + + + + + assembly.caption + Samling + + + + + perm.assemblies + Samlinger + + + + + assembly_bom_entry.label + Komponenter + + + + + assembly.labelp + Samlinger + + + + + assembly.edit + Rediger samling + + + + + assembly.new + Ny samling + + + + + assembly.edit.associated_build_part + Tilknyttet komponent + + + + + assembly.edit.associated_build_part.add + Tilføj komponent + + + + + assembly.edit.associated_build.hint + Denne komponent repræsenterer de fremstillede instanser af samlingen. Angiv, hvis fremstillede instanser er påkrævet. Hvis ikke, vil antallet af komponenter først blive anvendt ved opbygning af det pågældende projekt. + + + + + assembly.edit.bom.import_bom + Importér komponenter + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Samlinger + + + + + assembly.bom_import.flash.success + %count% komponent(er) blev importeret til samlingen med succes. + + + + + assembly.bom_import.flash.invalid_entries + Valideringsfejl! Kontroller venligst den importerede fil! + + + + + assembly.bom_import.flash.invalid_file + Filen kunne ikke importeres. Kontrollér, at du har valgt den korrekte filtype. Fejlmeddelelse: %message% + + + + + assembly.bom.quantity + Mængde + + + + + assembly.bom.mountnames + Monteringsnavne + + + + + assembly.bom.instockAmount + Antal på lager + + + + + assembly.info.title + Samleinfo + + + + + assembly.info.info.label + Info + + + + + assembly.info.sub_assemblies.label + Undergruppe + + + + + assembly.info.builds.label + Byggeri + + + + + assembly.info.bom_add_parts + Tilføj dele + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Du bør kontrollere, om du virkelig ønsker at bygge samlingen med denne status!]]> + + + + + assembly.builds.build_not_possible + Opbygning ikke mulig: Ikke nok komponenter til rådighed + + + + + assembly.builds.following_bom_entries_miss_instock + Der er ikke nok dele på lager til at bygge dette projekt %number_of_builds% gange. Følgende dele mangler på lager: + + + + + assembly.builds.build_possible + Byggeri muligt + + + + + assembly.builds.number_of_builds_possible + %max_builds% eksemplarer af denne samling.]]> + + + + + assembly.builds.number_of_builds + Antal opbygninger + + + + + assembly.build.btn_build + Byg + + + + + assembly.builds.no_stocked_builds + Antal lagrede byggede enheder + + + + + assembly.info.bom_entries_count + Komponenter + + + + + assembly.info.sub_assemblies_count + Undergrupper + + + + + assembly.builds.stocked + på lager + + + + + assembly.builds.needed + nødvendig + + + + + assembly.add_parts_to_assembly + Tilføj dele til samlingen + + + + + assembly.bom.name + Navn + + + + + assembly.bom.comment + Notater + + + + + assembly.builds.following_bom_entries_miss_instock_n + Der er ikke nok dele på lager til at bygge denne samling %number_of_builds% gange. Følgende dele mangler på lager: + + + + + assembly.build.help + Vælg, hvilke lagre de nødvendige dele til bygningen skal tages fra (og i hvilken mængde). Marker afkrydsningsfeltet for hver delpost, når du har fjernet delene, eller brug det øverste afkrydsningsfelt for at markere alle på én gang. + + + + + assembly.build.required_qty + Krævet antal + + + + + assembly.import_bom + Importer dele til samling + + + + + assembly.bom.part + Del + + + + + assembly.bom.add_entry + Tilføj post + + + + + assembly.bom.price + Pris + + + + + assembly.build.dont_check_quantity + Tjek ikke mængder + + + + + assembly.build.dont_check_quantity.help + Hvis denne mulighed vælges, fjernes de valgte mængder fra lageret, uanset om der er mere eller mindre end nødvendigt for at bygge samlingen. + + + + + assembly.build.add_builds_to_builds_part + Tilføj byggede enheder til assemblies del + + + + + assembly.bom_import.type + Type + + + + + assembly.bom_import.type.json + JSON for en samling + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Slet eksisterende poster før import + + + + + assembly.bom_import.clear_existing_bom.help + Hvis dette valg er markeret, slettes alle eksisterende komponentposter i samlingen og erstattes med de importerede. + + + + + assembly.import_bom.template.header.json + JSON-importskabelon til en samling + + + + + assembly.import_bom.template.header.kicad_pcbnew + Importskabelon CSV (KiCAD Pcbnew BOM) til en samling + + + + + assembly.bom_import.template.entry.name + Delens navn i samlingen + + + + + assembly.bom_import.template.entry.part.mpnr + Unik produktnummer hos producenten + + + + + assembly.bom_import.template.entry.part.ipn + Unik IPN for delen + + + + + assembly.bom_import.template.entry.part.name + Unikt komponentnavn + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Unikt producenter navn + + + + + assembly.bom_import.template.entry.part.category.name + Kategoriens unikke navn + + + + + assembly.bom_import.template.json.table + + + + + Felt + Betingelse + Datatype + Beskrivelse + + + + + quantity + Påkrævet + Flydende tal (Float) + Skal være til stede og indeholde en flydende værdi (Float), der er større end 0,0. + + + name + Valgfri + String + Hvis til stede, skal det være en ikke-tom streng. + + + part + Valgfri + Objekt/Array + + Hvis angivet, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: +
    +
  • part.id
  • +
  • part.name
  • +
+ + + + part.id + Valgfri + Heltal (Integer) + Heltal (Integer) > 0. Matcher Part-DB's interne numeriske ID for komponenten. + + + part.name + Valgfri + String + Ikke-tom streng, hvis part.mpnr eller part.ipn ikke er givet. + + + part.mpnr + Valgfri + String + Ikke-tom streng, hvis part.name eller part.ipn ikke er givet. + + + part.ipn + Valgfri + String + Ikke-tom streng, hvis part.name eller part.mpnr ikke er givet. + + + part.description + Valgfri + String eller null + Hvis til stede, skal det være en ikke-tom streng eller null. + + + part.manufacturer + Valgfri + Objekt/Array + + Hvis til stede, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: +
    +
  • manufacturer.id
  • +
  • manufacturer.name
  • +
+ + + + manufacturer.id + Valgfri + Heltal (Integer) + Heltal (Integer) > 0. Matcher producentens interne numeriske ID. + + + manufacturer.name + Valgfri + String + Ikke-tom streng, hvis manufacturer.id ikke er givet. + + + part.category + Valgfri + Objekt/Array + + Hvis til stede, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: +
    +
  • category.id
  • +
  • category.name
  • +
+ + + + category.id + Valgfri + Heltal (Integer) + Heltal (Integer) > 0. Matcher komponentkategoriens interne numeriske ID. + + + category.name + Valgfri + String + Ikke-tom streng, hvis category.id ikke er givet. + + + + ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Forventede kolonner: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Bemærk: Der foretages ingen tildelinger til konkrete komponenter fra kategoristyringen.

+ ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Felt + Betingelse + Datatype + Beskrivelse + + + + + Id + Valgfri + Heltal (Integer) + Fri oplysning. Et unikt identifikationsnummer for hver komponent. + + + Designator + Valgfri + Streng + Fri oplysning. En unik referencebetegnelse for komponenten på printkortet, f.eks. "R1" for modstand 1. Bruges til navngivning af monteringssæt i komponentgruppen. + + + Package + Valgfri + Streng + Fri oplysning. Komponentenheden eller -formatet, f.eks. "0805" for SMD-modstande. + + + Antal + Påkrævet + Heltal (Integer) + Antallet af identiske komponenter, der kræves for at oprette en enkelt instans af samling. + + + Betegnelse + Påkrævet + Streng + Beskrivelse eller funktion for komponenten, f.eks. modstandsværdi "10kΩ" eller kondensatorværdi "100nF". Bruges til navnet i BOM-posten. + + + Leverandør og ref + Valgfri + Streng + Fri oplysning. Kan indeholde f.eks. distributørspecifik værdi. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (del) + + + + + typeahead.parts.assembly.name + %name% (samling) + + + + + projects.build.form.part + Del "%name%" + + + + + projects.build.form.assembly + Samling "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% nødvendig) + + + + + projects.build.form.assembly.bom.entry.no.stock + ikke på lager + + diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 9fb3f6ef6..c32bf46a7 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -4740,6 +4740,18 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr Name + + + part.table.name.value.for_part + %value% (Bauteil) + + + + + part.table.name.value.for_assembly + %value% (Baugruppe) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9788,6 +9800,18 @@ Element 1 -> Element 1.2 Bauteil + + + project.bom.assembly + Baugruppe + + + + + project.bom.partOrAssembly + Auswahl + + project.bom.add_entry @@ -9866,6 +9890,42 @@ Element 1 -> Element 1.2 Archiviert + + + assembly.edit.status + Status + + + + + assembly.status.draft + Entwurf + + + + + assembly.status.planning + In Planung + + + + + assembly.status.in_production + In Produktion + + + + + assembly.status.finished + Abgeschlossen + + + + + assembly.status.archived + Archiviert + + part.new_build_part.error.build_part_already_exists @@ -10142,6 +10202,12 @@ Element 1 -> Element 1.2 vorhanden + + + project.builds.no_stock + kein Lager angegeben + + project.builds.needed @@ -10214,6 +10280,12 @@ Element 1 -> Element 1.2 Ziel-Bestand + + + project.build.builds_part_lot_label + %name% (%quantity% benötigt) + + project.builds.number_of_builds @@ -12851,6 +12923,622 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Externe Version anzeigen + + + assembly.label + Baugruppe + + + + + assembly.caption + Baugruppe + + + + + perm.assemblies + Baugruppen + + + + + assembly_bom_entry.label + Bauteile + + + + + assembly.labelp + Baugruppen + + + + + assembly.edit + Bearbeite Baugruppe + + + + + assembly.new + Neue Baugruppe + + + + + assembly.edit.associated_build_part + Verknüpftes Bauteil + + + + + assembly.edit.associated_build_part.add + Bauteil hinzufügen + + + + + assembly.edit.associated_build.hint + Dieses Bauteil repräsentiert die gebauten Instanzen der Baugruppe. Anzugeben, sofern gebaute Instanzen benötigt werden. Wenn nein, werden die Stückzahlen bzgl. der Baugruppe erst beim Build des jeweiligen Projektes herangezogen. + + + + + assembly.edit.bom.import_bom + Bauteile importieren + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Baugruppen + + + + + assembly.bom_import.flash.success + %count% Part Einträge erfolgreich in Baugruppe importiert. + + + + + assembly.bom_import.flash.invalid_entries + Validierungsfehler! Bitte überprüfen Sie die importierte Datei! + + + + + assembly.bom_import.flash.invalid_file + Datei konnte nicht importiert werden. Überprüfen Sie, dass Sie den richtigen Dateityp gewählt haben. Fehlermeldung: %message% + + + + + assembly.bom.quantity + Menge + + + + + assembly.bom.mountnames + Bestückungsnamen + + + + + assembly.bom.instockAmount + Bestand im Lager + + + + + assembly.info.title + Baugruppen-Info + + + + + assembly.info.info.label + Info + + + + + assembly.info.sub_assemblies.label + Untergruppe + + + + + assembly.info.builds.label + Bau + + + + + assembly.info.bom_add_parts + Bauteile hinzufügen + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Sie sollten überprüfen, ob sie die Baugruppe mit diesem Status wirklich bauen wollen!]]> + + + + + assembly.builds.build_not_possible + Bau nicht möglich: Nicht genügend Bauteile vorhanden + + + + + assembly.builds.following_bom_entries_miss_instock + Es sind nicht genügend Bauteile auf Lager, um dieses Projekt %number_of_builds% mal zu bauen. Von folgenden Bauteilen ist nicht genügend auf Lager. + + + + + assembly.builds.build_possible + Bau möglich + + + + + assembly.builds.number_of_builds_possible + %max_builds% Exemplare dieser Baugruppe zu bauen.]]> + + + + + assembly.builds.number_of_builds + Zu bauende Anzahl + + + + + assembly.build.btn_build + Bauen + + + + + assembly.builds.no_stocked_builds + Anzahl gelagerter gebauter Instanzen + + + + + assembly.info.bom_entries_count + Bauteile + + + + + assembly.info.sub_assemblies_count + Untergruppen + + + + + assembly.builds.stocked + vorhanden + + + + + assembly.builds.needed + benötigt + + + + + assembly.add_parts_to_assembly + Bauteile zur Baugruppe hinzufügen + + + + + assembly.bom.name + Name + + + + + assembly.bom.comment + Notizen + + + + + assembly.builds.following_bom_entries_miss_instock_n + Es sind nicht genügend Bauteile auf Lager, um diese Baugruppe %number_of_builds% mal zu bauen. Von folgenden Bauteilen ist nicht genügend auf Lager: + + + + + assembly.build.help + Wählen Sie aus, aus welchen Beständen die zum Bau notwendigen Bauteile genommen werden sollen (und in welcher Anzahl). Setzen Sie den Haken für jeden Part Eintrag, wenn sie die Bauteile entnommen haben, oder nutzen Sie die oberste Checkbox, um alle Haken auf einmal zu setzen. + + + + + assembly.build.required_qty + Benötigte Anzahl + + + + + assembly.import_bom + Importiere Parts für Baugruppe + + + + + assembly.bom.part + Bauteil + + + + + assembly.bom.add_entry + Eintrag hinzufügen + + + + + assembly.bom.price + Preis + + + + + assembly.build.dont_check_quantity + Mengen nicht überprüfen + + + + + assembly.build.dont_check_quantity.help + Wenn diese Option gewählt wird, werden die gewählten Mengen aus dem Lager entfernt, egal ob mehr oder weniger Bauteile sind, als für den Bau der Baugruppe eigentlich benötigt werden. + + + + + assembly.build.add_builds_to_builds_part + Gebaute Instanzen zum Bauteil der Baugruppe hinzufügen + + + + + assembly.bom_import.type + Typ + + + + + assembly.bom_import.type.json + JSON für eine Baugruppe + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Lösche existierende Bauteil-Einträge vor dem Import + + + + + assembly.bom_import.clear_existing_bom.help + Wenn diese Option ausgewählt ist, werden alle bereits in der Baugruppe existierenden Bauteile gelöscht und mit den importierten Bauteildaten überschrieben. + + + + + assembly.import_bom.template.header.json + Import-Vorlage JSON für eine Baugruppe + + + + + assembly.import_bom.template.header.kicad_pcbnew + Import-Vorlage CSV (KiCAD Pcbnew BOM) für eine Baugruppe + + + + + assembly.bom_import.template.entry.name + Name des Bauteils in der Baugruppe + + + + + assembly.bom_import.template.entry.part.mpnr + Eindeutige Produktnummer innerhalb des Herstellers + + + + + assembly.bom_import.template.entry.part.ipn + Eideutige IPN des Bauteils + + + + + assembly.bom_import.template.entry.part.name + Eindeutiger Name des Bauteils + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Eindeutiger Name des Herstellers + + + + + assembly.bom_import.template.entry.part.category.name + Eindeutiger Name der Kategorie + + + + + assembly.bom_import.template.json.table + + + + + Feld + Bedingung + Datentyp + Beschreibung + + + + + quantity + Pflichtfeld + Gleitkommazahl (Float) + Muss gegeben sein und enthält einen Gleitkommawert (Float), der größer als 0.0 ist. + + + name + Optional + String + Falls vorhanden, muss es ein nicht-leerer String sein. + + + part + Optional + Objekt/Array + + Falls angegeben, muss es ein Objekt/Array sein und mindestens eines der Felder ausgefüllt sein: +
    +
  • part.id
  • +
  • part.name
  • +
+ + + + part.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. + + + part.name + Optional + String + Nicht-leerer String, falls keine part.mpnr- bzw. part.ipn-Angabe gegeben ist. + + + part.mpnr + Optional + String + Nicht-leerer String, falls keine part.name- bzw. part-ipn-Angabe gegeben ist. + + + part.ipn + Optional + String + Nicht-leerer String, falls keine part.name- bzw. part.mpnr-Angabe gegeben ist. + + + part.description + Optional + String oder null + Falls vorhanden, muss es ein nicht-leerer String sein oder null. + + + part.manufacturer + Optional + Objekt/Array + + Falls vorhanden, muss es ein Objekt/Array sein und mindestens eines der Felder ausgefüllt sein: +
    +
  • manufacturer.id
  • +
  • manufacturer.name
  • +
+ + + + manufacturer.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID des Herstellers. + + + manufacturer.name + Optional + String + Nicht-leerer String, falls keine manufacturer.id-Angabe gegeben ist. + + + part.category + Optional + Objekt/Array + + Falls vorhanden, muss es ein Objekt/Array sein und mindestens eines der Felder ausgefüllt sein: +
    +
  • category.id
  • +
  • category.name
  • +
+ + + + category.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID der Kategorie des Bauteils. + + + category.name + Optional + String + Nicht-leerer String, falls keine category.id-Angabe gegeben ist. + + + + ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Erwartete Spalten: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Hinweis: Es findet keine Zuordnung zu konkreten Bauteilen aus der Kategorie-Verwaltung statt.

+ ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Feld + Bedingung + Datentyp + Beschreibung + + + + + Id + Optional + Ganzzahl (Integer) + Offene Angabe. Eine eindeutige Identifikationsnummer für jedes Bauteil. + + + Designator + Optional + String + Offene Angabe. Ein eindeutiger Referenzbezeichner des Bauteils auf der Leiterplatte, z.B. „R1“ für Widerstand 1. Wird für den Bestückungsnamen des Bauteil-Eintrags innerhalb der Bauteilgruppe verwendet. + + + Package + Optional + String + Offene Angabe. Das Gehäuse oder die Bauform des Bauteils, z.B. „0805“ für SMD-Widerstände. + + + Quantity + Pflichtfeld + Ganzzahl (Integer) + Anzahl der identischen Bauteile, die benötigt werden, um eine Instanz der Baugruppe zu erstellen. + + + Designation + Pflichtfeld + String + Beschreibung oder Funktion des Bauteils, z.B. Widerstandswert „10kΩ“ oder Kondensatorwert „100nF“. Wird für den Namen des BOM-Eintrags verwendet. + + + Supplier and ref + Optional + String + Offene Angabe. Kann z.B. Distributor spezifischen Wert enthalten. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (Bauteil) + + + + + typeahead.parts.assembly.name + %name% (Baugruppe) + + + + + projects.build.form.part + Bauteil "%name%" + + + + + projects.build.form.assembly + Baugruppe "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% benötigt) + + + + + projects.build.form.assembly.bom.entry.no.stock + nicht auf Lager + + part.table.actions.error diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index cc17d9be4..8fdb801b7 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1535,5 +1535,693 @@ Επεξεργασία + + + part.table.name.value.for_part + %value% (Μέρος) + + + + + part.table.name.value.for_assembly + %value% (Συναρμολόγηση) + + + + + project.bom.assembly + Συναρμολόγηση + + + + + project.bom.partOrAssembly + Επιλογή + + + + + assembly.edit.status + Κατάσταση + + + + + assembly.status.draft + Προσχέδιο + + + + + assembly.status.planning + Υπό σχεδιασμό + + + + + assembly.status.in_production + Σε παραγωγή + + + + + assembly.status.finished + Ολοκληρώθηκε + + + + + assembly.status.archived + Αρχειοθετήθηκε + + + + + project.builds.no_stock + δεν έχει καθοριστεί απόθεμα + + + + + project.build.builds_part_lot_label + %name% (%quantity% απαιτείται) + + + + + assembly.label + Σύνολο + + + + + assembly.caption + Σύνολο + + + + + perm.assemblies + Συναρμολογήσεις + + + + + assembly_bom_entry.label + Μέρη + + + + + assembly.labelp + Συναρμολογήσεις + + + + + assembly.edit + Επεξεργασία συνόλου + + + + + assembly.new + Νέο σύνολο + + + + + assembly.edit.associated_build_part + Σχετικό μέρος + + + + + assembly.edit.associated_build_part.add + Προσθήκη μέρους + + + + + assembly.edit.associated_build.hint + Αυτό το μέρος αντιπροσωπεύει τις κατασκευασμένες εκδόσεις του συνόλου. Καταχωρίστε το εάν απαιτούνται κατασκευασμένες εκδόσεις. Εάν όχι, οι ποσότητες θα χρησιμοποιηθούν μόνο κατά την κατασκευή του εκάστοτε έργου. + + + + + assembly.edit.bom.import_bom + Εισαγωγή μερών + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Συναρμολογήσεις + + + + + assembly.bom_import.flash.success + %count% εγγραφές εξαρτημάτων εισήχθησαν με επιτυχία στο σύνολο. + + + + + assembly.bom_import.flash.invalid_entries + Σφάλμα επικύρωσης! Ελέγξτε το εισαγόμενο αρχείο! + + + + + assembly.bom_import.flash.invalid_file + Το αρχείο δεν μπόρεσε να εισαχθεί. Ελέγξτε ότι έχετε επιλέξει τον σωστό τύπο αρχείου. Μήνυμα σφάλματος: %message% + + + + + assembly.bom.quantity + Ποσότητα + + + + + assembly.bom.mountnames + Ονόματα συναρμολόγησης + + + + + assembly.bom.instockAmount + Ποσότητα σε απόθεμα + + + + + assembly.info.title + Πληροφορίες συναρμολόγησης + + + + + assembly.info.info.label + Πληροφορίες + + + + + assembly.info.sub_assemblies.label + Υποομάδες + + + + + assembly.info.builds.label + Κατασκευές + + + + + assembly.info.bom_add_parts + Προσθήκη εξαρτημάτων + + + + + assembly.builds.check_assembly_status + «%assembly_status%». Ελέγξτε εάν θέλετε πραγματικά να κατασκευάσετε τη συναρμολόγηση με αυτήν την κατάσταση!]]> + + + + + assembly.builds.build_not_possible + Η κατασκευή δεν είναι δυνατή: Δεν υπάρχουν αρκετά διαθέσιμα εξαρτήματα + + + + + assembly.builds.following_bom_entries_miss_instock + Δεν υπάρχουν αρκετά εξαρτήματα σε απόθεμα για να κατασκευαστεί αυτό το έργο %number_of_builds% φορές. Λείπουν τα ακόλουθα εξαρτήματα: + + + + + assembly.builds.build_possible + Κατασκευή δυνατή + + + + + assembly.builds.number_of_builds_possible + %max_builds% μονάδες αυτής της συναρμολόγησης.]]> + + + + + assembly.builds.number_of_builds + Αριθμός κατασκευών + + + + + assembly.build.btn_build + Κατασκευή + + + + + assembly.builds.no_stocked_builds + Αποθηκευμένα κατασκευασμένα κομμάτια + + + + + assembly.info.bom_entries_count + Εξαρτήματα + + + + + assembly.info.sub_assemblies_count + Υποομάδες + + + + + assembly.builds.stocked + σε απόθεμα + + + + + assembly.builds.needed + απαιτούμενο + + + + + assembly.add_parts_to_assembly + Προσθήκη εξαρτημάτων στη συναρμολόγηση + + + + + assembly.bom.name + Όνομα + + + + + assembly.bom.comment + Σχόλια + + + + + assembly.builds.following_bom_entries_miss_instock_n + Δεν υπάρχουν αρκετά εξαρτήματα σε απόθεμα για να κατασκευαστεί αυτή η συναρμολόγηση %number_of_builds% φορές. Λείπουν τα ακόλουθα εξαρτήματα: + + + + + assembly.build.help + Επιλέξτε από ποια αποθέματα θα αφαιρεθούν τα αναγκαία για την κατασκευή εξαρτήματα (και σε ποια ποσότητα). Σημειώστε το πλαίσιο επιλογής για κάθε εξάρτημα όταν αφαιρέσετε τα εξαρτήματα ή χρησιμοποιήστε το ανώτερο πλαίσιο επιλογής για να τα ελέγξετε όλα ταυτόχρονα. + + + + + assembly.build.required_qty + Απαιτούμενη ποσότητα + + + + + assembly.import_bom + Εισαγωγή εξαρτημάτων συναρμολόγησης + + + + + assembly.bom.part + Εξάρτημα + + + + + assembly.bom.add_entry + Προσθήκη καταχώρησης + + + + + assembly.bom.price + Τιμή + + + + + assembly.build.dont_check_quantity + Μην ελέγχετε την ποσότητα + + + + + assembly.build.dont_check_quantity.help + Εάν επιλεγεί αυτή η επιλογή, οι επιλεγμένες ποσότητες θα αφαιρεθούν από το απόθεμα ανεξάρτητα από το αν είναι περισσότερο ή λιγότερο από το απαιτούμενο για την κατασκευή της συναρμολόγησης. + + + + + assembly.build.add_builds_to_builds_part + Προσθήκη κατασκευασμένων κομματιών στο τμήμα συναρμολόγησης + + + + + assembly.bom_import.type + Τύπος + + + + + assembly.bom_import.type.json + JSON για συναρμολόγηση + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Διαγραφή υπαρχόντων εξαρτημάτων πριν από την εισαγωγή + + + + + assembly.bom_import.clear_existing_bom.help + Εάν επιλεγεί αυτή η επιλογή, όλα τα ήδη υπάρχοντα εξαρτήματα στη συναρμολόγηση θα διαγραφούν και θα αντικατασταθούν με τα δεδομένα εξαρτημάτων που εισάγονται. + + + + + assembly.import_bom.template.header.json + Πρότυπο εισαγωγής JSON για συναρμολόγηση + + + + + assembly.import_bom.template.header.kicad_pcbnew + Πρότυπο εισαγωγής CSV (KiCAD Pcbnew BOM) για συναρμολόγηση + + + + + assembly.bom_import.template.entry.name + Όνομα του εξαρτήματος στη συναρμολόγηση + + + + + assembly.bom_import.template.entry.part.mpnr + Μοναδικός αριθμός προϊόντος από τον κατασκευαστή + + + + + assembly.bom_import.template.entry.part.ipn + Μοναδικός IPN του εξαρτήματος + + + + + assembly.bom_import.template.entry.part.name + Μοναδικό όνομα εξαρτήματος + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Μοναδικό όνομα κατασκευαστή + + + + + assembly.bom_import.template.entry.part.category.name + Μοναδικό όνομα κατηγορίας + + + + + assembly.bom_import.template.json.table + + + + + Πεδίο + Προϋπόθεση + Τύπος Δεδομένων + Περιγραφή + + + + + quantity + Υποχρεωτικό πεδίο + Αριθμός κινητής υποδιαστολής (Float) + Πρέπει να παρέχεται και να περιέχει τιμή κινητής υποδιαστολής (Float) μεγαλύτερη από 0.0. + + + name + Προαιρετικό + Κείμενο (String) + Εάν υπάρχει, πρέπει να είναι μη κενό κείμενο. + + + part + Προαιρετικό + Αντικείμενο/Πίνακας + + Εάν παρέχεται, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα πεδία του να είναι συμπληρωμένο: +
    +
  • part.id
  • +
  • part.name
  • +
+ + + + part.id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος (Integer) > 0. Αντιστοιχεί στην εσωτερική αριθμητική ταυτότητα (ID) του εξαρτήματος στη βάση δεδομένων. + + + part.name + Προαιρετικό + Κείμενο (String) + Μη κενό κείμενο, εάν δεν παρέχονται οι ενδείξεις part.mpnr ή part.ipn. + + + part.mpnr + Προαιρετικό + Κείμενο (String) + Μη κενό κείμενο, εάν δεν παρέχονται οι ενδείξεις part.name ή part.ipn. + + + part.ipn + Προαιρετικό + Κείμενο (String) + Μη κενό κείμενο, εάν δεν παρέχονται οι ενδείξεις part.name ή part.mpnr. + + + part.description + Προαιρετικό + Κείμενο ή null + Εάν υπάρχει, πρέπει να είναι μη κενό κείμενο, ή null. + + + part.manufacturer + Προαιρετικό + Αντικείμενο/Πίνακας + + Εάν υπάρχει, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα πεδία του να είναι συμπληρωμένο: +
    +
  • manufacturer.id
  • +
  • manufacturer.name
  • +
+ + + + manufacturer.id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος (Integer) > 0. Αντιστοιχεί στην εσωτερική αριθμητική ταυτότητα (ID) του κατασκευαστή. + + + manufacturer.name + Προαιρετικό + Κείμενο (String) + Μη κενό κείμενο, εάν δεν παρέχεται η ένδειξη manufacturer.id. + + + part.category + Προαιρετικό + Αντικείμενο/Πίνακας + + Εάν υπάρχει, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα πεδία του να είναι συμπληρωμένο: +
    +
  • category.id
  • +
  • category.name
  • +
+ + + + category.id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος (Integer) > 0. Αντιστοιχεί στην εσωτερική αριθμητική ταυτότητα (ID) της κατηγορίας του εξαρτήματος. + + + category.name + Προαιρετικό + Κείμενο (String) + Μη κενό κείμενο, εάν δεν παρέχεται η ένδειξη category.id. + + + + ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Αναμενόμενες στήλες: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Σημείωση: Δεν πραγματοποιείται αντιστοίχιση με συγκεκριμένα εξαρτήματα από τη διαχείριση κατηγοριών.

+ ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Πεδίο + Εκπλήρωση + Τύπος δεδομένων + Περιγραφή + + + + + Id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ελεύθερη καταχώρηση. Μοναδικός αριθμός ταυτοποίησης για κάθε εξάρτημα. + + + Σχεδιαστής + Προαιρετικό + Συμβολοσειρά (String) + Ελεύθερη καταχώρηση. Μοναδικός αναγνωριστικός δείκτης του εξαρτήματος στην πλακέτα κυκλώματος, π.χ. "R1" για την αντίσταση 1. Χρησιμοποιείται για το όνομα του εξαρτήματος στο πλαίσιο της ομάδας εξαρτημάτων. + + + Συσκευασία + Προαιρετικό + Συμβολοσειρά (String) + Ελεύθερη καταχώρηση. Ο τύπος ή η μορφή του εξαρτήματος, π.χ. "0805" για αντιστάσεις SMD. + + + Ποσότητα + Υποχρεωτικό + Ακέραιος αριθμός (Integer) + Ο αριθμός των πανομοιότυπων εξαρτημάτων που απαιτούνται για τη δημιουργία μίας μονάδας του συνόλου. + + + Ορισμός + Υποχρεωτικό + Συμβολοσειρά (String) + Περιγραφή ή λειτουργία του εξαρτήματος, π.χ. αντίσταση "10kΩ" ή χωρητικότητα "100nF". Χρησιμοποιείται για το όνομα της εγγραφής στο BOM. + + + Προμηθευτής και παραπομπή + Προαιρετικό + Συμβολοσειρά (String) + Ελεύθερη καταχώρηση. Μπορεί να περιλαμβάνει, π.χ., ειδική τιμή από διανομέα. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (Εξάρτημα) + + + + + typeahead.parts.assembly.name + %name% (Συναρμολόγηση) + + + + + projects.build.form.part + Εξάρτημα "%name%" + + + + + projects.build.form.assembly + Συναρμολόγηση "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% απαιτείται) + + + + + projects.build.form.assembly.bom.entry.no.stock + δεν υπάρχει στο απόθεμα + + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index be1e63488..d267c89ac 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -4741,6 +4741,18 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Name + + + part.table.name.value.for_part + %value% (Part) + + + + + part.table.name.value.for_assembly + %value% (Assembly) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9789,6 +9801,18 @@ Element 1 -> Element 1.2 Part + + + project.bom.assembly + Assembly + + + + + project.bom.partOrAssembly + Selection + + project.bom.add_entry @@ -9867,6 +9891,42 @@ Element 1 -> Element 1.2 Archived + + + assembly.edit.status + Project status + + + + + assembly.status.draft + Draft + + + + + assembly.status.planning + Planning + + + + + assembly.status.in_production + In production + + + + + assembly.status.finished + Finished + + + + + assembly.status.archived + Archived + + part.new_build_part.error.build_part_already_exists @@ -10143,6 +10203,12 @@ Element 1 -> Element 1.2 stocked + + + project.builds.no_stock + no stock specified + + project.builds.needed @@ -10215,6 +10281,12 @@ Element 1 -> Element 1.2 Target lot + + + project.build.builds_part_lot_label + %name% (%quantity% needed) + + project.builds.number_of_builds @@ -12852,6 +12924,622 @@ Please note, that you can not impersonate a disabled user. If you try you will g View external version + + + assembly.label + Assembly + + + + + assembly.caption + Assembly + + + + + assembly_bom_entry.label + Parts + + + + + perm.assemblies + Assemblies + + + + + assembly.labelp + Assemblies + + + + + assembly.edit + Edit assembly + + + + + assembly.new + New assembly + + + + + assembly.edit.associated_build_part + Associated builds part + + + + + assembly.edit.associated_build_part.add + Add builds part + + + + + assembly.edit.associated_build.hint + This part represents the builds of this assembly. To indicate if built instances are required. If not, the number of pieces regarding the assembly are only used for the build of the respective project. + + + + + assembly.edit.bom.import_bom + Import BOM + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Assemblies + + + + + assembly.bom_import.flash.success + Imported %count% parts in assembly successfully. + + + + + assembly.bom_import.flash.invalid_entries + Validation error! Please check your data! + + + + + assembly.bom_import.flash.invalid_file + File could not be imported. Please check that you have selected the right file type. Error message: %message% + + + + + assembly.bom.quantity + Quantity + + + + + assembly.bom.mountnames + Mount names + + + + + assembly.bom.instockAmount + Stocked amount + + + + + assembly.info.title + Assembly info + + + + + assembly.info.info.label + Info + + + + + assembly.info.sub_assemblies.label + Sub-assemblies + + + + + assembly.info.builds.label + Build + + + + + assembly.info.bom_add_parts + Add part entries + + + + + assembly.builds.check_assembly_status + "%assembly_status%". You should check if you really want to build the assembly with this status!]]> + + + + + assembly.builds.build_not_possible + Build not possible: Parts not stocked + + + + + assembly.builds.following_bom_entries_miss_instock + You do not have enough parts stocked to build this assembly %number_of_builds% times. The following parts have missing instock: + + + + + assembly.builds.build_possible + Build possible + + + + + assembly.builds.number_of_builds_possible + %max_builds% builds of this assembly.]]> + + + + + assembly.builds.number_of_builds + Build amount + + + + + assembly.build.btn_build + Build + + + + + assembly.builds.no_stocked_builds + Number of stocked builds + + + + + assembly.info.bom_entries_count + Part entries + + + + + assembly.info.sub_assemblies_count + Sub-assemblies + + + + + assembly.builds.stocked + stocked + + + + + assembly.builds.needed + needed + + + + + assembly.add_parts_to_assembly + Add parts to assembly + + + + + assembly.bom.name + Name + + + + + assembly.bom.comment + Notes + + + + + assembly.builds.following_bom_entries_miss_instock_n + You do not have enough parts stocked to build this assembly %number_of_builds% times. The following parts have missing instock: + + + + + assembly.build.help + Choose from which part lots the stock to build this assembly should be taken (and in which amount). Check the checkbox for each part, when you are finished withdrawing the parts, or use the top checkbox to check all boxes at once. + + + + + assembly.build.required_qty + Required quantity + + + + + assembly.import_bom + Import BOM for project + + + + + assembly.bom.part + Part + + + + + assembly.bom.add_entry + Add entry + + + + + assembly.bom.price + Price + + + + + assembly.build.dont_check_quantity + Do not check quantities + + + + + assembly.build.dont_check_quantity.help + If this option is selected, the given withdraw quantities are used as given, no matter if more or less parts are actually required to build this assembly. + + + + + assembly.build.add_builds_to_builds_part + Add builds to assembly builds part + + + + + assembly.bom_import.type + Type + + + + + assembly.bom_import.type.json + JSON for one assembly + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Clear existing part entries before importing + + + + + assembly.bom_import.clear_existing_bom.help + Selecting this option will remove all existing part entries in the assembly and overwrite them with the imported part data! + + + + + assembly.import_bom.template.header.json + Import template JSON format for one assembly + + + + + assembly.import_bom.template.header.kicad_pcbnew + Import template CSV format (KiCAD Pcbnew BOM) for one assembly + + + + + assembly.bom_import.template.entry.name + Name of the part in the assembly + + + + + assembly.bom_import.template.entry.part.mpnr + Unique product number within the manufacturer + + + + + assembly.bom_import.template.entry.part.ipn + Unique IPN of the part + + + + + assembly.bom_import.template.entry.part.name + Unique name of the part + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Unique name of the manufacturer + + + + + assembly.bom_import.template.entry.part.category.name + Unique name of the category + + + + + assembly.bom_import.template.json.table + + + + + Field + Condition + Data type + Description + + + + + quantity + Required + Floating point (Float) + Must be provided and contains a floating-point value (Float) greater than 0.0. + + + name + Optional + String + If present, it must be a non-empty string. + + + part + Optional + Object/Array + + If provided, it must be an object/array and at least one of the fields must be filled: +
    +
  • part.id
  • +
  • part.name
  • +
+ + + + part.id + Optional + Integer + Integer > 0. Matches the Part-DB internal numeric ID of the component. + + + part.name + Optional + String + Non-empty string if no part.mpnr or part.ipn is provided. + + + part.mpnr + Optional + String + Non-empty string if no part.name or part.ipn is provided. + + + part.ipn + Optional + String + Non-empty string if no part.name or part.mpnr is provided. + + + part.description + Optional + String or null + If present, it must be a non-empty string or null. + + + part.manufacturer + Optional + Object/Array + + If present, it must be an object/array and at least one of the fields must be filled: +
    +
  • manufacturer.id
  • +
  • manufacturer.name
  • +
+ + + + manufacturer.id + Optional + Integer + Integer > 0. Matches the internal numeric ID of the manufacturer. + + + manufacturer.name + Optional + String + Non-empty string if no manufacturer.id is provided. + + + part.category + Optional + Object/Array + + If present, it must be an object/array and at least one of the fields must be filled: +
    +
  • category.id
  • +
  • category.name
  • +
+ + + + category.id + Optional + Integer + Integer > 0. Matches the internal numeric ID of the component's category. + + + category.name + Optional + String + Non-empty string if no category.id is provided. + + + + ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Expected Columns: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Note: No mapping is performed with specific components from category management.

+ ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Field + Condition + Data Type + Description + + + + + Id + Optional + Integer + Free-form field. A unique identification number for each component. + + + Designator + Optional + String + Free-form field. A unique reference designator of the component on the PCB, e.g., “R1” for resistor 1. Used for naming the placement in the component group. + + + Package + Optional + String + Free-form field. The casing or form factor of the component, e.g., “0805” for SMD resistors. + + + Quantity + Required + Integer + The number of identical components required to create a single instance of an assembly. + + + Designation + Required + String + The description or function of the component, e.g., resistor value “10kΩ” or capacitor value “100nF.” Used for the name in the BOM entry. + + + Supplier and ref + Optional + String + Free-form field. May include, for example, specific distributor information. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (Part) + + + + + typeahead.parts.assembly.name + %name% (Assembly) + + + + + projects.build.form.part + Part "%name%" + + + + + projects.build.form.assembly + Assembly "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% needed) + + + + + projects.build.form.assembly.bom.entry.no.stock + not in stock + + part.table.actions.error diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index fce38e52f..c3bf96368 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -4740,6 +4740,18 @@ Subelementos serán desplazados hacia arriba. Nombre + + + part.table.name.value.for_part + %value% (Componente) + + + + + part.table.name.value.for_assembly + %value% (Ensamblaje) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9804,6 +9816,18 @@ Elemento 3 Componente + + + project.bom.assembly + Baugruppe + + + + + project.bom.partOrAssembly + Auswahl + + project.bom.add_entry @@ -9882,6 +9906,42 @@ Elemento 3 Archivado + + + assembly.edit.status + Estatus + + + + + assembly.status.draft + Esbozo + + + + + assembly.status.planning + En planificación + + + + + assembly.status.in_production + En producción + + + + + assembly.status.finished + Completado + + + + + assembly.status.archived + Archivado + + part.new_build_part.error.build_part_already_exists @@ -10158,6 +10218,12 @@ Elemento 3 Almacenado + + + project.builds.no_stock + no se ha especificado stock + + project.builds.needed @@ -10230,6 +10296,12 @@ Elemento 3 Lote objetivo + + + project.build.builds_part_lot_label + %name% (se requiere %quantity%) + + project.builds.number_of_builds @@ -12344,6 +12416,622 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Ver versión externa + + + assembly.label + Ensamblaje + + + + + assembly.caption + Ensamblaje + + + + + perm.assemblies + Ensamblajes + + + + + assembly_bom_entry.label + Componentes + + + + + assembly.labelp + Ensamblajes + + + + + assembly.edit + Editar ensamblaje + + + + + assembly.new + Nuevo ensamblaje + + + + + assembly.edit.associated_build_part + Componente asociado + + + + + assembly.edit.associated_build_part.add + Añadir componente + + + + + assembly.edit.associated_build.hint + Este componente representa las instancias fabricadas del ensamblaje. Indique si se necesitan instancias fabricadas. De lo contrario, las cantidades del componente solo se utilizarán cuando se construya el proyecto correspondiente. + + + + + assembly.edit.bom.import_bom + Importar componentes + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Ensamblajes + + + + + assembly.bom_import.flash.success + %count% componente(s) se importaron correctamente al ensamblaje. + + + + + assembly.bom_import.flash.invalid_entries + ¡Error de validación! ¡Revisa el archivo importado! + + + + + assembly.bom_import.flash.invalid_file + No se pudo importar el archivo. Asegúrate de haber seleccionado el tipo de archivo correcto. Mensaje de error: %message% + + + + + assembly.bom.quantity + Cantidad + + + + + assembly.bom.mountnames + Nombres de montaje + + + + + assembly.bom.instockAmount + Cantidad en stock + + + + + assembly.info.title + Información del ensamblaje + + + + + assembly.info.info.label + Información + + + + + assembly.info.sub_assemblies.label + Subconjuntos + + + + + assembly.info.builds.label + Construcciones + + + + + assembly.info.bom_add_parts + Añadir piezas + + + + + assembly.builds.check_assembly_status + "%assembly_status%". ¡Por favor, verifica si realmente deseas construir el ensamblaje con este estado!]]> + + + + + assembly.builds.build_not_possible + Construcción no posible: No hay suficientes componentes disponibles + + + + + assembly.builds.following_bom_entries_miss_instock + No hay suficientes piezas en stock para construir este proyecto %number_of_builds% veces. Faltan las siguientes piezas: + + + + + assembly.builds.build_possible + Construcción posible + + + + + assembly.builds.number_of_builds_possible + %max_builds% unidades de este ensamblaje.]]> + + + + + assembly.builds.number_of_builds + Número de construcciones + + + + + assembly.build.btn_build + Construir + + + + + assembly.builds.no_stocked_builds + Unidades construidas almacenadas + + + + + assembly.info.bom_entries_count + Componentes + + + + + assembly.info.sub_assemblies_count + Subconjuntos + + + + + assembly.builds.stocked + en stock + + + + + assembly.builds.needed + necesario + + + + + assembly.add_parts_to_assembly + Añadir piezas al ensamblaje + + + + + assembly.bom.name + Nombre + + + + + assembly.bom.comment + Comentarios + + + + + assembly.builds.following_bom_entries_miss_instock_n + No hay suficientes piezas en stock para construir este ensamblaje %number_of_builds% veces. Faltan las siguientes piezas: + + + + + assembly.build.help + Seleccione de qué almacenes se tomarán las piezas necesarias para la construcción (y en qué cantidad). Marque la casilla de cada entrada una vez que haya quitado las piezas, o use la casilla superior para marcarlas todas a la vez. + + + + + assembly.build.required_qty + Cantidad requerida + + + + + assembly.import_bom + Importar piezas para ensamblaje + + + + + assembly.bom.part + Pieza + + + + + assembly.bom.add_entry + Añadir entrada + + + + + assembly.bom.price + Precio + + + + + assembly.build.dont_check_quantity + No verificar cantidades + + + + + assembly.build.dont_check_quantity.help + Si se selecciona esta opción, las cantidades seleccionadas se quitarán del inventario independientemente de si hay más o menos de lo necesario para construir el ensamblaje. + + + + + assembly.build.add_builds_to_builds_part + Añadir unidades construidas a la parte del ensamblaje + + + + + assembly.bom_import.type + Tipo + + + + + assembly.bom_import.type.json + JSON para un ensamblaje + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Eliminar entradas de componentes existentes antes de la importación + + + + + assembly.bom_import.clear_existing_bom.help + Si esta opción está seleccionada, se eliminarán todos los componentes existentes en el ensamblaje y serán reemplazados por los datos de los componentes importados. + + + + + assembly.import_bom.template.header.json + Plantilla de importación JSON para un ensamblaje + + + + + assembly.import_bom.template.header.kicad_pcbnew + Plantilla de importación CSV (KiCAD Pcbnew BOM) para un ensamblaje + + + + + assembly.bom_import.template.entry.name + Nombre del componente en el ensamblaje + + + + + assembly.bom_import.template.entry.part.mpnr + Número de parte único dentro del fabricante + + + + + assembly.bom_import.template.entry.part.ipn + IPN único del componente + + + + + assembly.bom_import.template.entry.part.name + Nombre único del componente + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Nombre único del fabricante + + + + + assembly.bom_import.template.entry.part.category.name + Nombre único de la categoría + + + + + assembly.bom_import.template.json.table + + + + + Campo + Condición + Tipo de dato + Descripción + + + + + quantity + Obligatorio + Número decimal (Float) + Debe estar presente y contener un valor decimal (Float) mayor que 0.0. + + + name + Opcional + Cadena de texto (String) + Si está presente, debe ser una cadena de texto no vacía. + + + part + Opcional + Objeto/Array + + Si se proporciona, debe ser un objeto/array y al menos uno de los campos debe estar completado: +
    +
  • part.id
  • +
  • part.name
  • +
+ + + + part.id + Opcional + Entero (Integer) + Entero (Integer) > 0. Corresponde al ID numérico interno del componente en la base de datos. + + + part.name + Opcional + Cadena de texto (String) + Cadena de texto no vacía, si no se proporciona part.mpnr o part.ipn. + + + part.mpnr + Opcional + Cadena de texto (String) + Cadena de texto no vacía, si no se proporciona part.name o part.ipn. + + + part.ipn + Opcional + Cadena de texto (String) + Cadena de texto no vacía, si no se proporciona part.name o part.mpnr. + + + part.description + Opcional + Cadena de texto (String) o null + Si está presente, debe ser una cadena de texto no vacía o null. + + + part.manufacturer + Opcional + Objeto/Array + + Si está presente, debe ser un objeto/array y al menos uno de los campos debe estar completado: +
    +
  • manufacturer.id
  • +
  • manufacturer.name
  • +
+ + + + manufacturer.id + Opcional + Entero (Integer) + Entero (Integer) > 0. Corresponde al ID numérico interno del fabricante. + + + manufacturer.name + Opcional + Cadena de texto (String) + Cadena de texto no vacía, si no se proporciona manufacturer.id. + + + part.category + Opcional + Objeto/Array + + Si está presente, debe ser un objeto/array y al menos uno de los campos debe estar completado: +
    +
  • category.id
  • +
  • category.name
  • +
+ + + + category.id + Opcional + Entero (Integer) + Entero (Integer) > 0. Corresponde al ID numérico interno de la categoría del componente. + + + category.name + Opcional + Cadena de texto (String) + Cadena de texto no vacía, si no se proporciona category.id. + + + + ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Columnas esperadas: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Nota: No se realiza una asociación con componentes específicos de la gestión de categorías.

+ ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Campo + Condición + Tipo de Datos + Descripción + + + + + Id + Opcional + Entero + Campo libre. Un número de identificación único para cada componente. + + + Designador + Opcional + Cadena de texto + Campo libre. Un designador de referencia único para el componente en la PCB, p. ej., "R1" para la resistencia 1. Se utiliza para nombrar la colocación en el grupo de componentes. + + + Package + Opcional + Cadena de texto + Campo libre. El formato o tipo de encapsulado del componente, p. ej., "0805" para resistencias SMD. + + + Cantidad + Obligatorio + Entero + El número de componentes idénticos necesarios para crear una instancia única de un ensamblaje. + + + Designación + Obligatorio + Cadena de texto + La descripción o función del componente, p. ej., el valor de la resistencia "10kΩ" o el valor del condensador "100nF". Se utiliza para el nombre en la entrada del BOM. + + + Proveedor y referencia + Opcional + Cadena de texto + Campo libre. Puede incluir, por ejemplo, información específica del distribuidor. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (Componente) + + + + + typeahead.parts.assembly.name + %name% (Ensamblaje) + + + + + projects.build.form.part + Componente "%name%" + + + + + projects.build.form.assembly + Ensamblaje "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% necesario) + + + + + projects.build.form.assembly.bom.entry.no.stock + sin stock + + part.table.actions.error diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 292dbafaa..5362f9390 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -4703,6 +4703,18 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia Nom + + + part.table.name.value.for_part + %value% (Componente) + + + + + part.table.name.value.for_assembly + %value% (Assemblaggio) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -6947,7 +6959,7 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia company.edit.address.placeholder - Ex. 99 exemple de rue + Ex. 99 exemple de rue exemple de ville @@ -9097,5 +9109,681 @@ exemple de ville Si vous avez des questions à propos de Part-DB , rendez vous sur <a href="%href%" class="link-external" target="_blank">Github</a> + + + project.bom.assembly + Assemblage + + + + + project.bom.partOrAssembly + Sélection + + + + + assembly.edit.status + Statut + + + + + assembly.status.draft + Brouillon + + + + + assembly.status.planning + En planification + + + + + assembly.status.in_production + En production + + + + + assembly.status.finished + Terminé + + + + + assembly.status.archived + Archivé + + + + + project.builds.no_stock + aucun stock indiqué + + + + + project.build.builds_part_lot_label + %name% (%quantity% requis) + + + + + assembly.label + Assemblage + + + + + assembly.caption + Assemblage + + + + + perm.assemblies + Assemblages + + + + + assembly_bom_entry.label + Composants + + + + + assembly.labelp + Assemblages + + + + + assembly.edit + Modifier l'assemblage + + + + + assembly.new + Nouvel assemblage + + + + + assembly.edit.associated_build_part + Composant associé + + + + + assembly.edit.associated_build_part.add + Ajouter un composant + + + + + assembly.edit.associated_build.hint + Ce composant représente les instances fabriquées de l'assemblage. Indiquez si des instances fabriquées sont nécessaires. Sinon, les quantités des composants ne seront appliquées que lors de la construction du projet correspondant. + + + + + assembly.edit.bom.import_bom + Importer des composants + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Assemblages + + + + + assembly.bom_import.flash.success + %count% composant(s) ont été importé(s) avec succès dans l'assemblage. + + + + + assembly.bom_import.flash.invalid_entries + Erreur de validation ! Veuillez vérifier le fichier importé ! + + + + + assembly.bom_import.flash.invalid_file + Le fichier n'a pas pu être importé. Veuillez vérifier que vous avez sélectionné le bon type de fichier. Message d'erreur : %message% + + + + + assembly.bom.quantity + Quantité + + + + + assembly.bom.mountnames + Noms de montage + + + + + assembly.bom.instockAmount + Quantité en stock + + + + + assembly.info.title + Informations sur l'assemblage + + + + + assembly.info.info.label + Informations + + + + + assembly.info.sub_assemblies.label + Sous-ensembles + + + + + assembly.info.builds.label + Constructions + + + + + assembly.info.bom_add_parts + Ajouter des pièces + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Vérifiez bien si vous souhaitez construire l'assemblage avec ce statut !]]> + + + + + assembly.builds.build_not_possible + Construction impossible : pièces insuffisantes disponibles + + + + + assembly.builds.following_bom_entries_miss_instock + Il n'y a pas suffisamment de pièces en stock pour construire ce projet %number_of_builds% fois. Les pièces suivantes manquent en stock : + + + + + assembly.builds.build_possible + Construction possible + + + + + assembly.builds.number_of_builds_possible + %max_builds% unités de cet assemblage.]]> + + + + + assembly.builds.number_of_builds + Nombre d'assemblages à construire + + + + + assembly.build.btn_build + Construire + + + + + assembly.builds.no_stocked_builds + Nombre d'instances construites en stock + + + + + assembly.info.bom_entries_count + Composants + + + + + assembly.info.sub_assemblies_count + Sous-ensembles + + + + + assembly.builds.stocked + en stock + + + + + assembly.builds.needed + nécessaire + + + + + assembly.add_parts_to_assembly + Ajouter des pièces à l'assemblage + + + + + assembly.bom.name + Nom + + + + + assembly.bom.comment + Commentaires + + + + + assembly.builds.following_bom_entries_miss_instock_n + Il n'y a pas suffisamment de pièces en stock pour construire cet assemblage %number_of_builds% fois. Les pièces suivantes manquent en stock : + + + + + assembly.build.help + Sélectionnez les stocks à partir desquels les pièces nécessaires à la construction seront prises (et en quelle quantité). Vérifiez chaque pièce en les retirant, ou utilisez la case supérieure pour les sélectionner toutes à la fois. + + + + + assembly.build.required_qty + Quantité requise + + + + + assembly.import_bom + Importer des pièces pour l'assemblage + + + + + assembly.bom.part + Pièce + + + + + assembly.bom.add_entry + Ajouter une ligne + + + + + assembly.bom.price + Prix + + + + + assembly.build.dont_check_quantity + Ne pas vérifier les quantités + + + + + assembly.build.dont_check_quantity.help + Si cette option est activée, les quantités sélectionnées seront retirées du stock, quelle que soit leur suffisance pour l’assemblage. + + + + + assembly.build.add_builds_to_builds_part + Ajouter les unités construites à la pièce assemblée + + + + + assembly.bom_import.type + Type + + + + + assembly.bom_import.type.json + JSON pour un assemblage + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Supprimer les entrées de pièces existantes avant l’importation + + + + + assembly.bom_import.clear_existing_bom.help + Si cette option est cochée, toutes les pièces existantes dans l’assemblage seront supprimées et remplacées par les données importées. + + + + + assembly.import_bom.template.header.json + Modèle d’importation JSON pour un assemblage + + + + + assembly.import_bom.template.header.kicad_pcbnew + Modèle d’importation CSV (KiCAD Pcbnew BOM) pour un assemblage + + + + + assembly.bom_import.template.entry.name + Nom de la pièce dans l’assemblage + + + + + assembly.bom_import.template.entry.part.mpnr + Numéro unique de la pièce chez le fabricant + + + + + assembly.bom_import.template.entry.part.ipn + Numéro IPN unique de la pièce + + + + + assembly.bom_import.template.entry.part.name + Nom unique de la pièce + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Nom unique du fabricant + + + + + assembly.bom_import.template.entry.part.category.name + Nom unique de la catégorie + + + + + assembly.bom_import.template.json.table + + + + + Champ + Condition + Type de données + Description + + + + + quantity + Obligatoire + Nombre décimal (Float) + Doit être présent et contenir une valeur décimale (Float) supérieure à 0,0. + + + name + Optionnel + Chaîne (String) + Si présent, doit être une chaîne non vide. + + + part + Optionnel + Objet/Tableau + + Si fourni, doit être un objet/un tableau et au moins un des champs doit être rempli : +
    +
  • part.id
  • +
  • part.name
  • +
+ + + + part.id + Optionnel + Entier (Integer) + Entier (Integer) > 0. Correspond à l'ID numérique interne dans Part-DB du composant. + + + part.name + Optionnel + Chaîne (String) + Chaîne non vide si part.mpnr ou part.ipn ne sont pas fournis. + + + part.mpnr + Optionnel + Chaîne (String) + Chaîne non vide si part.name ou part.ipn ne sont pas fournis. + + + part.ipn + Optionnel + Chaîne (String) + Chaîne non vide si part.name ou part.mpnr ne sont pas fournis. + + + part.description + Optionnel + Chaîne ou null + Si présent, doit être une chaîne non vide ou null. + + + part.manufacturer + Optionnel + Objet/Tableau + + Si présent, doit être un objet/un tableau et au moins un des champs doit être rempli : +
    +
  • manufacturer.id
  • +
  • manufacturer.name
  • +
+ + + + manufacturer.id + Optionnel + Entier (Integer) + Entier (Integer) > 0. Correspond à l'ID numérique interne du fabricant. + + + manufacturer.name + Optionnel + Chaîne (String) + Chaîne non vide si manufacturer.id n'est pas fourni. + + + part.category + Optionnel + Objet/Tableau + + Si présent, doit être un objet/un tableau et au moins un des champs doit être rempli : +
    +
  • category.id
  • +
  • category.name
  • +
+ + + + category.id + Optionnel + Entier (Integer) + Entier (Integer) > 0. Correspond à l'ID numérique interne de la catégorie du composant. + + + category.name + Optionnel + Chaîne (String) + Chaîne non vide si category.id n'est pas fourni. + + + + ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Colonnes attendues : + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Remarque : Aucun mappage n'est effectué avec des composants spécifiques issus de la gestion des catégories.

+ ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Champ + Condition + Type de Données + Description + + + + + Id + Optionnel + Entier + Champ libre. Un numéro d'identification unique pour chaque composant. + + + Designeur + Optionnel + Chaîne + Champ libre. Une référence de désignation unique du composant sur le PCB, par exemple, "R1" pour la résistance 1. Utilisé pour nommer la position au sein du groupe de composants. + + + Boîtier + Optionnel + Chaîne + Champ libre. Le type ou format d'encapsulation du composant, par exemple, "0805" pour des résistances CMS. + + + Quantité + Obligatoire + Entier + Le nombre de composants identiques nécessaires pour créer une instance unique d'un ensemble. + + + Désignation + Obligatoire + Chaîne + La description ou la fonction du composant, par exemple, la valeur de résistance "10kΩ" ou la valeur de condensateur "100nF". Utilisé comme nom dans l'entrée de la nomenclature (BOM). + + + Fournisseur et réf + Optionnel + Chaîne + Champ libre. Peut inclure, par exemple, des informations spécifiques au distributeur. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (pièce) + + + + + typeahead.parts.assembly.name + %name% (assemblage) + + + + + projects.build.form.part + Pièce "%name%" + + + + + projects.build.form.assembly + Assemblage "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% nécessaires) + + + + + projects.build.form.assembly.bom.entry.no.stock + Non disponible en stock + + diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 828304eba..0ea57d9f1 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -4742,6 +4742,18 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi Nome + + + part.table.name.value.for_part + %value% (Componente) + + + + + part.table.name.value.for_assembly + %value% (Assemblaggio) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9806,6 +9818,18 @@ Element 3 Componente + + + project.bom.assembly + Assemblaggio + + + + + project.bom.partOrAssembly + Selezione + + project.bom.add_entry @@ -9884,6 +9908,42 @@ Element 3 Archiviato + + + assembly.edit.status + Stato + + + + + assembly.status.draft + Bozza + + + + + assembly.status.planning + In pianificazione + + + + + assembly.status.in_production + In produzione + + + + + assembly.status.finished + Completato + + + + + assembly.status.archived + Archiviato + + part.new_build_part.error.build_part_already_exists @@ -10160,6 +10220,12 @@ Element 3 a magazzino + + + project.builds.no_stock + nessuna scorta specificata + + project.builds.needed @@ -10232,6 +10298,12 @@ Element 3 Lotto target + + + project.build.builds_part_lot_label + %name% (%quantity% richiesti) + + project.builds.number_of_builds @@ -12346,6 +12418,622 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Visualizza la versione esterna + + + assembly.label + Assemblaggio + + + + + assembly.caption + Assemblaggio + + + + + perm.assemblies + Assemblaggi + + + + + assembly_bom_entry.label + Componenti + + + + + assembly.labelp + Assemblaggi + + + + + assembly.edit + Modifica assemblaggio + + + + + assembly.new + Nuovo assemblaggio + + + + + assembly.edit.associated_build_part + Componente associato + + + + + assembly.edit.associated_build_part.add + Aggiungi componente + + + + + assembly.edit.associated_build.hint + Questo componente rappresenta le istanze fabbricate dell'assemblaggio. Specificare se sono necessarie istanze fabbricate. In caso contrario, le quantità di componenti verranno utilizzate solo durante la costruzione del progetto corrispondente. + + + + + assembly.edit.bom.import_bom + Importa componenti + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Assemblaggi + + + + + assembly.bom_import.flash.success + %count% componente(i) importato(i) correttamente nell'assemblaggio. + + + + + assembly.bom_import.flash.invalid_entries + Errore di convalida! Controlla il file importato! + + + + + assembly.bom_import.flash.invalid_file + Impossibile importare il file. Assicurati di aver selezionato il tipo di file corretto. Messaggio di errore: %message% + + + + + assembly.bom.quantity + Quantità + + + + + assembly.bom.mountnames + Nomi di montaggio + + + + + assembly.bom.instockAmount + Quantità in magazzino + + + + + assembly.info.title + Informazioni sul gruppo + + + + + assembly.info.info.label + Info + + + + + assembly.info.sub_assemblies.label + Sotto-gruppi + + + + + assembly.info.builds.label + Costruzioni + + + + + assembly.info.bom_add_parts + Aggiungi componenti + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Controlla se vuoi davvero costruire il gruppo con questo stato!]]> + + + + + assembly.builds.build_not_possible + Costruzione impossibile: componenti insufficienti disponibili + + + + + assembly.builds.following_bom_entries_miss_instock + Non ci sono abbastanza componenti in magazzino per costruire questo progetto %number_of_builds% volte. Mancano i seguenti componenti: + + + + + assembly.builds.build_possible + Costruzione possibile + + + + + assembly.builds.number_of_builds_possible + %max_builds% unità di questo gruppo.]]> + + + + + assembly.builds.number_of_builds + Numero di gruppi da costruire + + + + + assembly.build.btn_build + Costruire + + + + + assembly.builds.no_stocked_builds + Numero di istanze costruite in magazzino + + + + + assembly.info.bom_entries_count + Componenti + + + + + assembly.info.sub_assemblies_count + Sotto-gruppi + + + + + assembly.builds.stocked + disponibile + + + + + assembly.builds.needed + necessari + + + + + assembly.add_parts_to_assembly + Aggiungi componenti al gruppo + + + + + assembly.bom.name + Nome + + + + + assembly.bom.comment + Commenti + + + + + assembly.builds.following_bom_entries_miss_instock_n + Non ci sono abbastanza componenti in magazzino per costruire questo gruppo %number_of_builds% volte. Mancano i seguenti componenti: + + + + + assembly.build.help + Seleziona i magazzini da cui prelevare i componenti necessari per la costruzione (e in che quantità). Spunta ciascun componente una volta prelevato, oppure utilizza la casella superiore per selezionare tutto in una volta. + + + + + assembly.build.required_qty + Quantità necessaria + + + + + assembly.import_bom + Importa componenti per il gruppo + + + + + assembly.bom.part + Componente + + + + + assembly.bom.add_entry + Aggiungi voce + + + + + assembly.bom.price + Prezzo + + + + + assembly.build.dont_check_quantity + Non controllare le quantità + + + + + assembly.build.dont_check_quantity.help + Se abilitata, le quantità selezionate verranno rimosse dal magazzino indipendentemente dalla loro sufficienza per il gruppo. + + + + + assembly.build.add_builds_to_builds_part + Aggiungi istanze costruite al gruppo componenti + + + + + assembly.bom_import.type + Tipo + + + + + assembly.bom_import.type.json + JSON per un gruppo + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.clear_existing_bom + Elimina i componenti esistenti prima di importare + + + + + assembly.bom_import.clear_existing_bom.help + Se abilitata, tutti i componenti esistenti verranno rimossi e sostituiti dai dati importati. + + + + + assembly.import_bom.template.header.json + Template di importazione JSON per un gruppo + + + + + assembly.import_bom.template.header.kicad_pcbnew + Template di importazione CSV (KiCAD Pcbnew BOM) per un gruppo + + + + + assembly.bom_import.template.entry.name + Nome del componente nel gruppo + + + + + assembly.bom_import.template.entry.part.mpnr + Numero univoco del componente del produttore + + + + + assembly.bom_import.template.entry.part.ipn + IPN univoco del componente + + + + + assembly.bom_import.template.entry.part.name + Nome univoco del componente + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Nome univoco del produttore + + + + + assembly.bom_import.template.entry.part.category.name + Nome univoco della categoria + + + + + assembly.bom_import.template.json.table + + + + + Campo + Condizione + Tipo di dato + Descrizione + + + + + quantity + Obbligatorio + Numero decimale (Float) + Deve essere presente e contenere un valore decimale (Float) maggiore di 0,0. + + + name + Opzionale + Stringa (String) + Se presente, deve essere una stringa non vuota. + + + part + Opzionale + Oggetto/Array + + Se fornito, deve essere un oggetto/un array e almeno uno dei campi deve essere compilato: +
    +
  • part.id
  • +
  • part.name
  • +
+ + + + part.id + Opzionale + Intero (Integer) + Intero (Integer) > 0. Corrisponde all'ID numerico interno di Part-DB per il componente. + + + part.name + Opzionale + Stringa (String) + Stringa non vuota se part.mpnr o part.ipn non sono forniti. + + + part.mpnr + Opzionale + Stringa (String) + Stringa non vuota se part.name o part.ipn non sono forniti. + + + part.ipn + Opzionale + Stringa (String) + Stringa non vuota se part.name o part.mpnr non sono forniti. + + + part.description + Opzionale + Stringa o null + Se presente, deve essere una stringa non vuota o null. + + + part.manufacturer + Opzionale + Oggetto/Array + + Se presente, deve essere un oggetto/un array e almeno uno dei campi deve essere compilato: +
    +
  • manufacturer.id
  • +
  • manufacturer.name
  • +
+ + + + manufacturer.id + Opzionale + Intero (Integer) + Intero (Integer) > 0. Corrisponde all'ID numerico interno del produttore. + + + manufacturer.name + Opzionale + Stringa (String) + Stringa non vuota se manufacturer.id non è fornito. + + + part.category + Opzionale + Oggetto/Array + + Se presente, deve essere un oggetto/un array e almeno uno dei campi deve essere compilato: +
    +
  • category.id
  • +
  • category.name
  • +
+ + + + category.id + Opzionale + Intero (Integer) + Intero (Integer) > 0. Corrisponde all'ID numerico interno della categoria del componente. + + + category.name + Opzionale + Stringa (String) + Stringa non vuota se category.id non è fornito. + + + + ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Colonne previste: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Nota: Non viene eseguita alcuna mappatura con componenti specifici dalla gestione delle categorie.

+ ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Campo + Condizione + Tipo di Dati + Descrizione + + + + + Id + Opzionale + Intero + Campo libero. Un numero identificativo unico per ogni componente. + + + Designatore + Opzionale + Stringa + Campo libero. Un designatore di riferimento unico per il componente sul PCB, ad esempio, "R1" per il resistore 1. Utilizzato per nominare la posizione nel gruppo di componenti. + + + Package + Opzionale + Stringa + Campo libero. Il tipo o formato del contenitore del componente, ad esempio, "0805" per le resistenze SMD. + + + Quantità + Obbligatorio + Intero + Il numero di componenti identici necessari per creare una singola unità di assemblaggio. + + + Designazione + Obbligatorio + Stringa + La descrizione o la funzione del componente, ad esempio, il valore della resistenza "10kΩ" o il valore del condensatore "100nF". Utilizzato per il nome nell'entrata della lista dei materiali (BOM). + + + Fornitore e riferimento + Opzionale + Stringa + Campo libero. Può includere, ad esempio, informazioni specifiche del distributore. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (componente) + + + + + typeahead.parts.assembly.name + %name% (gruppo) + + + + + projects.build.form.part + Componente "%name%" + + + + + projects.build.form.assembly + Gruppo "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% necessari) + + + + + projects.build.form.assembly.bom.entry.no.stock + Non disponibile in magazzino + + part.table.actions.error diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 4becc319c..5de5f83ef 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -4703,6 +4703,18 @@ 名称 + + + part.table.name.value.for_part + %value%(部品) + + + + + part.table.name.value.for_assembly + %value%(アセンブリ) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -8834,5 +8846,645 @@ Exampletown Part-DBについての質問は、<a href="%href%" class="link-external" target="_blank">GitHub</a> にスレッドがあります。 + + + project.bom.assembly + アセンブリ + + + + + project.bom.partOrAssembly + 選択 + + + + + assembly.edit.status + ステータス + + + + + assembly.status.draft + 下書き + + + + + assembly.status.planning + 計画中 + + + + + assembly.status.in_production + 製作中 + + + + + assembly.status.finished + 完成 + + + + + assembly.status.archived + アーカイブ済み + + + + + project.builds.no_stock + nessuna scorta specificata + + + + + project.build.builds_part_lot_label + %name% (必要数: %quantity%) + + + + + assembly.label + アセンブリ + + + + + assembly.caption + アセンブリ + + + + + perm.assemblies + アセンブリ一覧 + + + + + assembly_bom_entry.label + コンポーネント + + + + + assembly.labelp + アセンブリ一覧 + + + + + assembly.edit + アセンブリを編集 + + + + + assembly.new + 新しいアセンブリ + + + + + assembly.edit.associated_build_part + 関連コンポーネント + + + + + assembly.edit.associated_build_part.add + コンポーネントを追加 + + + + + assembly.edit.associated_build.hint + このコンポーネントは、アセンブリの製造されたインスタンスを表します。製造されたインスタンスが必要な場合は登録してください。それ以外の場合、コンポーネントの数量は該当するプロジェクトを構築する際にのみ使用されます。 + + + + + assembly.edit.bom.import_bom + コンポーネントをインポート + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + アセンブリ一覧 + + + + + assembly.bom_import.flash.success + %count% 個のコンポーネントが正常にアセンブリへインポートされました。 + + + + + assembly.bom_import.flash.invalid_entries + 検証エラー! インポートしたファイルを確認してください! + + + + + assembly.bom_import.flash.invalid_file + ファイルをインポートできませんでした。正しいファイル形式を選択しているか確認してください。エラーメッセージ: %message% + + + + + assembly.bom.quantity + 数量 + + + + + assembly.bom.mountnames + 取り付け名 + + + + + assembly.bom.instockAmount + 在庫数量 + + + + + assembly.info.title + アセンブリ情報 + + + + + assembly.info.info.label + 情報 + + + + + assembly.info.sub_assemblies.label + サブアセンブリ + + + + + assembly.info.builds.label + ビルド + + + + + assembly.info.bom_add_parts + 部品を追加 + + + + + assembly.builds.check_assembly_status + "%assembly_status%"です。この状態でビルドを続行してよろしいですか?]]> + + + + + assembly.builds.build_not_possible + ビルド不可能: 必要な部品が不足しています + + + + + assembly.builds.following_bom_entries_miss_instock + %number_of_builds% 回のビルドを行うのに十分な部品在庫がありません。不足している部品: + + + + + assembly.builds.build_possible + ビルド可能 + + + + + assembly.builds.number_of_builds_possible + %max_builds% 回のアセンブリをビルドできます。]]> + + + + + assembly.builds.number_of_builds + ビルドするアセンブリ数 + + + + + assembly.build.btn_build + ビルド + + + + + assembly.builds.no_stocked_builds + 在庫のビルド済みアセンブリ数 + + + + + assembly.info.bom_entries_count + 部品 + + + + + assembly.info.sub_assemblies_count + サブアセンブリ + + + + + assembly.builds.stocked + 在庫あり + + + + + assembly.builds.needed + 必要数量 + + + + + assembly.add_parts_to_assembly + アセンブリに部品を追加 + + + + + assembly.build.required_qty + 必要な数量 + + + + + assembly.build.yes_button + はい + + + + + assembly.build.no_button + いいえ + + + + + assembly.confirmation.required + + + + + + assembly.build.success + ビルドが正常に完了しました! + + + + + assembly.build.cancelled + ビルドがキャンセルされました。 + + + + + assembly.bom_import.type + タイプ + + + + + assembly.bom_import.type.json + アセンブリ用 JSON + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.clear_existing_bom + インポート前に既存の BOM をクリアする + + + + + assembly.bom_import.clear_existing_bom.help + 有効にすると、既存のすべての BOM エントリが削除され、インポートされたデータに置き換えられます。 + + + + + assembly.import_bom.template.header.json + アセンブリ用 JSON テンプレート + + + + + assembly.import_bom.template.header.kicad_pcbnew + アセンブリ用 CSV テンプレート(KiCAD Pcbnew BOM) + + + + + assembly.bom_import.template.entry.name + アセンブリ内の部品名 + + + + + assembly.bom_import.template.entry.part.mpnr + メーカーの部品番号 + + + + + assembly.bom_import.template.entry.part.ipn + 部品の一意の IPN + + + + + assembly.bom_import.template.entry.part.name + 部品名 + + + + + assembly.bom_import.template.entry.part.manufacturer.name + メーカー名 + + + + + assembly.bom_import.template.entry.part.category.name + カテゴリ名 + + + + + assembly.bom_import.template.json.table + + + + + フィールド + 条件 + データ型 + 説明 + + + + + quantity + 必須 + 浮動小数点数 (Float) + 指定され、0.0より大きい浮動小数点値 (Float) を含む必要があります。 + + + name + 任意 + 文字列 (String) + 指定されている場合、空でない文字列でなければなりません。 + + + part + 任意 + オブジェクト/配列 + + 指定された場合、オブジェクト/配列であり、以下のフィールドのうち少なくとも1つが入力されている必要があります: +
    +
  • part.id
  • +
  • part.name
  • +
+ + + + part.id + 任意 + 整数 (Integer) + 整数 (Integer) > 0。部品の Part-DB 内部数値 ID に対応します。 + + + part.name + 任意 + 文字列 (String) + part.mpnr または part.ipn が指定されていない場合、空でない文字列でなければなりません。 + + + part.mpnr + 任意 + 文字列 (String) + part.name または part.ipn が指定されていない場合、空でない文字列でなければなりません。 + + + part.ipn + 任意 + 文字列 (String) + part.name または part.mpnr が指定されていない場合、空でない文字列でなければなりません。 + + + part.description + 任意 + 文字列または null + 指定されている場合、空でない文字列または null でなければなりません。 + + + part.manufacturer + 任意 + オブジェクト/配列 + + 指定されている場合、オブジェクト/配列であり、以下のフィールドのうち少なくとも1つが入力されている必要があります: +
    +
  • manufacturer.id
  • +
  • manufacturer.name
  • +
+ + + + manufacturer.id + 任意 + 整数 (Integer) + 整数 (Integer) > 0。製造元の内部数値 ID に対応します。 + + + manufacturer.name + 任意 + 文字列 (String) + manufacturer.id が指定されていない場合、空でない文字列でなければなりません。 + + + part.category + 任意 + オブジェクト/配列 + + 指定されている場合、オブジェクト/配列であり、以下のフィールドのうち少なくとも1つが入力されている必要があります: +
    +
  • category.id
  • +
  • category.name
  • +
+ + + + category.id + 任意 + 整数 (Integer) + 整数 (Integer) > 0。コンポーネントカテゴリの内部数値 ID に対応します。 + + + category.name + 任意 + 文字列 (String) + category.id が指定されていない場合、空でない文字列でなければなりません。 + + + + ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + 予想される列: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + 注意: カテゴリ管理から特定のコンポーネントへのマッピングは行われません。

+ ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.table + + + + + フィールド + 条件 + データタイプ + 説明 + + + + + ID + 任意 + 整数 + 自由形式フィールド。各コンポーネントの一意の識別番号。 + + + デジネータ + 任意 + 文字列 + 自由形式フィールド。PCB上のコンポーネントの一意の参照デジネータ(例: 抵抗1の「R1」)。コンポーネントグループ内の配置の命名に使用されます。 + + + パッケージ + 任意 + 文字列 + 自由形式フィールド。コンポーネントのケースまたはフォームファクタ(例: SMD抵抗「0805」)。 + + + 数量 + 必須 + 整数 + アセンブリの単一インスタンスを作成するために必要な同一コンポーネントの数。 + + + 指定 + 必須 + 文字列 + コンポーネントの説明または機能(例: 抵抗値「10kΩ」やコンデンサ値「100nF」)。部品表(BOM)エントリ内の名前として使用されます。 + + + サプライヤーと参照 + 任意 + 文字列 + 自由形式フィールド。たとえば、特定のディストリビューター情報を含む場合があります。 + + + + ]]> + + + + + + typeahead.parts.part.name + %name%(部品) + + + + + typeahead.parts.assembly.name + %name%(アセンブリ) + + + + + projects.build.form.part + 部品「%name%」 + + + + + projects.build.form.assembly + アセンブリ「%name%」 + + + + + projects.build.form.assembly.bom.entry + %name% (必要数量: %quantity%) + + + + + projects.build.form.assembly.bom.entry.no.stock + 在庫なし + + diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 760533d7c..b7392b4d7 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -724,5 +724,729 @@ Weet u zeker dat u wilt doorgaan? + + + part.table.name.value.for_part + %value% (Onderdeel) + + + + + part.table.name.value.for_assembly + %value% (Samenstelling) + + + + + project.bom.assembly + Assemblage + + + + + project.bom.partOrAssembly + Selectie + + + + + assembly.edit.status + Κατάσταση + + + + + assembly.status.draft + Προσχέδιο + + + + + assembly.status.planning + Υπό σχεδιασμό + + + + + assembly.status.in_production + Σε παραγωγή + + + + + assembly.status.finished + Ολοκληρώθηκε + + + + + assembly.status.archived + Αρχειοθετήθηκε + + + + + project.builds.no_stock + geen voorraad opgegeven + + + + + project.build.builds_part_lot_label + %name% (%quantity% vereist) + + + + + assembly.label + Assemblage + + + + + assembly.caption + Assemblage + + + + + perm.assemblies + Assemblages + + + + + assembly_bom_entry.label + Componenten + + + + + assembly.labelp + Assemblages + + + + + assembly.edit + Assemblage bewerken + + + + + assembly.new + Nieuwe assemblage + + + + + assembly.edit.associated_build_part + Geassocieerd onderdeel + + + + + assembly.edit.associated_build_part.add + Onderdeel toevoegen + + + + + assembly.edit.associated_build.hint + Dit onderdeel vertegenwoordigt de vervaardigde exemplaren van de assemblage. Geef aan of vervaardigde exemplaren nodig zijn. Zo niet, dan worden de aantallen onderdelen alleen gebruikt bij het bouwen van het bijbehorende project. + + + + + assembly.edit.bom.import_bom + Componenten importeren + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Assemblages + + + + + assembly.bom_import.flash.success + %count% component(en) zijn succesvol geïmporteerd in de assemblage. + + + + + assembly.bom_import.flash.invalid_entries + Validatiefout! Controleer het geïmporteerde bestand! + + + + + assembly.bom_import.flash.invalid_file + Het bestand kon niet worden geïmporteerd. Controleer of je het correcte bestandstype hebt geselecteerd. Foutmelding: %message% + + + + + assembly.bom.quantity + Aantal + + + + + assembly.bom.mountnames + Montagenamen + + + + + assembly.bom.instockAmount + Beschikbaar in voorraad + + + + + assembly.info.title + Assemblage-informatie + + + + + assembly.info.info.label + Informatie + + + + + assembly.info.sub_assemblies.label + Subassemblages + + + + + assembly.info.builds.label + Bouw + + + + + assembly.info.bom_add_parts + Onderdelen toevoegen + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Bevestig dat je hiermee wilt doorgaan!]]> + + + + + assembly.builds.build_not_possible + Bouwen is niet mogelijk: niet voldoende onderdelen beschikbaar + + + + + assembly.builds.following_bom_entries_miss_instock + Er zijn niet voldoende onderdelen in voorraad om %number_of_builds% keer te bouwen. De volgende onderdelen ontbreken: + + + + + assembly.builds.build_possible + Bouwen mogelijk + + + + + assembly.builds.number_of_builds_possible + %max_builds% assemblages te bouwen.]]> + + + + + assembly.builds.number_of_builds + Aantal te bouwen assemblages + + + + + assembly.build.btn_build + Bouwen + + + + + assembly.builds.no_stocked_builds + Aantal geassembleerde onderdelen op voorraad + + + + + assembly.info.bom_entries_count + Onderdelen + + + + + assembly.info.sub_assemblies_count + Subgroepen + + + + + assembly.builds.stocked + Op voorraad + + + + + assembly.builds.needed + Nodig + + + + + assembly.add_parts_to_assembly + Onderdelen toevoegen aan assemblage + + + + + assembly.bom.name + Naam + + + + + assembly.bom.comment + Notities + + + + + assembly.builds.following_bom_entries_miss_instock_n + Er zijn niet genoeg onderdelen op voorraad om deze assemblage %number_of_builds% keer te bouwen. Van de volgende onderdelen is er niet genoeg op voorraad: + + + + + assembly.build.help + Selecteer uit welke voorraden de benodigde onderdelen voor de bouw gehaald moeten worden (en in welke hoeveelheid). Vink elk onderdeel afzonderlijk aan als het is verwijderd, of gebruik de bovenste selectievak om alle selectievakjes in één keer aan te vinken. + + + + + assembly.build.required_qty + Benodigde hoeveelheid + + + + + assembly.import_bom + Importeer onderdelen voor assemblage + + + + + assembly.bom.part + Onderdeel + + + + + assembly.bom.add_entry + Voer item in + + + + + assembly.bom.price + Prijs + + + + + assembly.build.dont_check_quantity + Hoeveelheden niet controleren + + + + + assembly.build.dont_check_quantity.help + Als deze optie is geselecteerd, worden de geselecteerde hoeveelheden uit de voorraad verwijderd, ongeacht of er meer of minder onderdelen zijn dan nodig is voor de assemblage. + + + + + assembly.build.add_builds_to_builds_part + Gemaakte instanties toevoegen aan onderdeel van assemblage + + + + + assembly.build.required_qty + Benodigd aantal + + + + + assembly.build.yes_button + Ja + + + + + assembly.build.no_button + Nee + + + + + assembly.confirmation.required + + + + + + assembly.build.success + De assemblage is succesvol gebouwd! + + + + + assembly.build.cancelled + De assemblage is geannuleerd. + + + + + assembly.bom_import.type + Type + + + + + assembly.bom_import.type.json + JSON voor assemblage + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.clear_existing_bom + Bestaande BOM wissen vóór importeren + + + + + assembly.bom_import.clear_existing_bom.help + Wanneer dit is ingeschakeld, worden alle bestaande BOM-items verwijderd en vervangen door de geïmporteerde gegevens. + + + + + assembly.import_bom.template.header.json + JSON-sjabloon voor assemblage + + + + + assembly.import_bom.template.header.kicad_pcbnew + CSV-sjabloon voor assemblage (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.template.entry.name + Naam van onderdeel in de assemblage + + + + + assembly.bom_import.template.entry.part.mpnr + Onderdeelnummer van de fabrikant + + + + + assembly.bom_import.template.entry.part.ipn + Unieke IPN van het onderdeel + + + + + assembly.bom_import.template.entry.part.name + Unieke naam van het onderdeel + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Unieke naam van de fabrikant + + + + + assembly.bom_import.template.entry.part.category.name + Unieke naam van de categorie + + + + + assembly.bom_import.template.json.table + + + + + Veld + Voorwaarde + Gegevenstype + Beschrijving + + + + + quantity + Verplicht veld + Kommagetal (Float) + Moet opgegeven zijn en bevat een kommagetal (Float) dat groter is dan 0,0. + + + name + Optioneel + String + Indien aanwezig, moet het een niet-lege string zijn. + + + part + Optioneel + Object/Array + + Indien opgegeven, moet het een object/array zijn en ten minste één van de velden ingevuld zijn: +
    +
  • part.id
  • +
  • part.name
  • +
+ + + + part.id + Optioneel + Geheel getal (Integer) + Geheel getal (Integer) > 0. Komt overeen met de interne numerieke ID van het onderdeel in de Part-DB. + + + part.name + Optioneel + String + Niet-lege string, indien geen part.mpnr- of part.ipn-vermelding is gegeven. + + + part.mpnr + Optioneel + String + Niet-lege string, indien geen part.name- of part.ipn-vermelding is gegeven. + + + part.ipn + Optioneel + String + Niet-lege string, indien geen part.name- of part.mpnr-vermelding is gegeven. + + + part.description + Optioneel + String of null + Indien aanwezig, moet het een niet-lege string zijn of null. + + + part.manufacturer + Optioneel + Object/Array + + Indien aanwezig, moet het een object/array zijn en ten minste één van de velden ingevuld zijn: +
    +
  • manufacturer.id
  • +
  • manufacturer.name
  • +
+ + + + manufacturer.id + Optioneel + Geheel getal (Integer) + Geheel getal (Integer) > 0. Komt overeen met de interne numerieke ID van de fabrikant. + + + manufacturer.name + Optioneel + String + Niet-lege string, indien geen manufacturer.id-vermelding is gegeven. + + + part.category + Optioneel + Object/Array + + Indien aanwezig, moet het een object/array zijn en ten minste één van de velden ingevuld zijn: +
    +
  • category.id
  • +
  • category.name
  • +
+ + + + category.id + Optioneel + Geheel getal (Integer) + Geheel getal (Integer) > 0. Komt overeen met de interne numerieke ID van de categorie van het onderdeel. + + + category.name + Optioneel + String + Niet-lege string, indien geen category.id-vermelding is gegeven. + + + + ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Verwachte kolommen: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Opmerking: Er wordt geen mapping uitgevoerd met specifieke componenten uit de categoriebeheer.

+ ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Veld + Voorwaarde + Gegevenstype + Beschrijving + + + + + Id + Optioneel + Integer + Vrij veld. Een unieke identificatienummer voor elk onderdeel. + + + Ontwerper + Optioneel + Tekst + Vrij veld. Een unieke referentie-ontwerper voor het onderdeel op de PCB, bijvoorbeeld "R1" voor weerstand 1. Gebruikt voor de naamgeving van de plaatsing in de componentgroep. + + + Omhulsel + Optioneel + Tekst + Vrij veld. Het type of de vormfactor van het onderdeel, bijvoorbeeld "0805" voor SMD-weerstanden. + + + Aantal + Verplicht + Integer + Het aantal identieke onderdelen dat nodig is om een enkele instantie van een assemblage te maken. + + + Aanduiding + Verplicht + Tekst + De beschrijving of functie van het onderdeel, bijvoorbeeld de weerstandswaarde "10kΩ" of de condensatorwaarde "100nF". Wordt gebruikt als naam in de BOM-invoer. + + + Leverancier en referentie + Optioneel + Tekst + Vrij veld. Kan bijvoorbeeld informatie bevatten die specifiek is voor de distributeur. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (Onderdeel) + + + + + typeahead.parts.assembly.name + %name% (Assemblage) + + + + + projects.build.form.part + Onderdelen "%name%" + + + + + projects.build.form.assembly + Assemblage "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% benodigd) + + + + + projects.build.form.assembly.bom.entry.no.stock + niet op voorraad + + diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index b769e2737..7290e5fe1 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -4745,6 +4745,18 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo Nazwa + + + part.table.name.value.for_part + %value%(部品) + + + + + part.table.name.value.for_assembly + %value%(アセンブリ) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9809,6 +9821,18 @@ Element 3 Komponent + + + project.bom.assembly + Zespół + + + + + project.bom.partOrAssembly + Wybór + + project.bom.add_entry @@ -9887,6 +9911,42 @@ Element 3 Zarchiwizowany + + + assembly.edit.status + Status + + + + + assembly.status.draft + Wersja robocza + + + + + assembly.status.planning + W planowaniu + + + + + assembly.status.in_production + W produkcji + + + + + assembly.status.finished + Zakończony + + + + + assembly.status.archived + Zarchiwizowany + + part.new_build_part.error.build_part_already_exists @@ -10163,6 +10223,12 @@ Element 3 dostępny + + + project.builds.no_stock + brak podanego stanu magazynowego + + project.builds.needed @@ -10235,6 +10301,12 @@ Element 3 Partia docelowa + + + project.build.builds_part_lot_label + %name% (%quantity% wymagane) + + project.builds.number_of_builds @@ -12223,5 +12295,621 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Wygenerowany kod + + + assembly.label + Zespół + + + + + assembly.caption + Zespół + + + + + perm.assemblies + Zespoły + + + + + assembly_bom_entry.label + Komponenty + + + + + assembly.labelp + Zespoły + + + + + assembly.edit + Edytuj zespół + + + + + assembly.new + Nowy zespół + + + + + assembly.edit.associated_build_part + Powiązany komponent + + + + + assembly.edit.associated_build_part.add + Dodaj komponent + + + + + assembly.edit.associated_build.hint + Ten komponent reprezentuje wyprodukowane instancje zespołu. Określ, czy są potrzebne wyprodukowane instancje. W przeciwnym razie ilości komponentów zostaną zastosowane tylko podczas budowy odpowiedniego projektu. + + + + + assembly.edit.bom.import_bom + Importuj komponenty + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Zespoły + + + + + assembly.bom_import.flash.success + Pomyślnie zaimportowano %count% komponent(ów) do zespołu. + + + + + assembly.bom_import.flash.invalid_entries + Błąd walidacji! Sprawdź zaimportowany plik! + + + + + assembly.bom_import.flash.invalid_file + Nie udało się zaimportować pliku. Sprawdź, czy wybrano poprawny typ pliku. Komunikat błędu: %message% + + + + + assembly.bom.quantity + Ilość + + + + + assembly.bom.mountnames + Nazwy montażu + + + + + assembly.bom.instockAmount + Ilość na magazynie + + + + + assembly.info.title + Informacje o zespole + + + + + assembly.info.info.label + Informacje + + + + + assembly.info.sub_assemblies.label + Podzespoły + + + + + assembly.info.builds.label + Budowa + + + + + assembly.info.bom_add_parts + Dodaj części + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Upewnij się, że chcesz zbudować zespół w tym statusie!]]> + + + + + assembly.builds.build_not_possible + Budowa niemożliwa: niewystarczająca ilość części + + + + + assembly.builds.following_bom_entries_miss_instock + Brakuje wystarczającej ilości części na magazynie, aby zbudować ten projekt %number_of_builds% razy. Brakujące części to: + + + + + assembly.builds.build_possible + Budowa możliwa + + + + + assembly.builds.number_of_builds_possible + %max_builds% egzemplarzy tego zespołu.]]> + + + + + assembly.builds.number_of_builds + Liczba budowanych egzemplarzy + + + + + assembly.build.btn_build + Zbuduj + + + + + assembly.builds.no_stocked_builds + Liczba zbudowanych i zmagazynowanych egzemplarzy + + + + + assembly.info.bom_entries_count + Elementy + + + + + assembly.info.sub_assemblies_count + Podzespoły + + + + + assembly.builds.stocked + na magazynie + + + + + assembly.builds.needed + potrzebne + + + + + assembly.add_parts_to_assembly + Dodaj części do zespołu + + + + + assembly.bom.name + Nazwa + + + + + assembly.bom.comment + Uwagi + + + + + assembly.builds.following_bom_entries_miss_instock_n + Brakuje wystarczającej ilości części na magazynie, aby zbudować ten zespół %number_of_builds% razy. Brakujące części to: + + + + + assembly.build.help + Wybierz, z których magazynów mają być pobrane części potrzebne do budowy (i w jakiej ilości). Zaznacz każdą pozycję, jeśli części zostały pobrane, lub użyj głównego pola wyboru, aby zaznaczyć wszystkie na raz. + + + + + assembly.build.required_qty + Wymagana ilość + + + + + assembly.import_bom + Importuj części dla zespołu + + + + + assembly.bom.part + Część + + + + + assembly.bom.add_entry + Dodaj pozycję + + + + + assembly.bom.price + Cena + + + + + assembly.build.dont_check_quantity + Nie sprawdzaj ilości + + + + + assembly.build.dont_check_quantity.help + Jeśli opcja jest wybrana, zadeklarowana ilość zostanie odjęta z magazynu, niezależnie od tego, czy jest wystarczająca do budowy zespołu. + + + + + assembly.build.add_builds_to_builds_part + Dodaj zbudowane egzemplarze jako część zespołu + + + + + assembly.bom_import.type + Typ + + + + + assembly.bom_import.type.json + JSON dla zespołu + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.clear_existing_bom + Usuń istniejące dane przed importem + + + + + assembly.bom_import.clear_existing_bom.help + Jeśli wybrano, wszystkie istniejące wpisy części zostaną usunięte i zastąpione danymi z importu. + + + + + assembly.import_bom.template.header.json + Szablon importu JSON dla zespołu + + + + + assembly.import_bom.template.header.kicad_pcbnew + Szablon importu CSV (KiCAD Pcbnew BOM) dla zespołu + + + + + assembly.bom_import.template.entry.name + Nazwa części w zespole + + + + + assembly.bom_import.template.entry.part.mpnr + Unikalny numer katalogowy producenta + + + + + assembly.bom_import.template.entry.part.ipn + Unikalny IPN części + + + + + assembly.bom_import.template.entry.part.name + Unikalna nazwa części + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Unikalna nazwa producenta + + + + + assembly.bom_import.template.entry.part.category.name + Unikalna nazwa kategorii + + + + + assembly.bom_import.template.json.table + + + + + Pole + Warunek + Typ danych + Opis + + + + + quantity + Wymagane + Typ zmiennoprzecinkowy (Float) + Musi być obecne i zawierać wartość zmiennoprzecinkową (Float) większą niż 0,0. + + + name + Opcjonalne + Ciąg znaków (String) + Jeśli obecne, musi być niepustym ciągiem znaków. + + + part + Opcjonalne + Obiekt/Tablica + + Jeśli podane, musi być obiektem/tablicą i co najmniej jedno z poniższych pól musi być wypełnione: +
    +
  • part.id
  • +
  • part.name
  • +
+ + + + part.id + Opcjonalne + Liczba całkowita (Integer) + Liczba całkowita (Integer) > 0. Odpowiada wewnętrznemu numerowi ID komponentu w Part-DB. + + + part.name + Opcjonalne + Ciąg znaków (String) + Niepusty ciąg znaków, jeśli part.mpnr ani part.ipn nie są podane. + + + part.mpnr + Opcjonalne + Ciąg znaków (String) + Niepusty ciąg znaków, jeśli part.name ani part.ipn nie są podane. + + + part.ipn + Opcjonalne + Ciąg znaków (String) + Niepusty ciąg znaków, jeśli part.name ani part.mpnr nie są podane. + + + part.description + Opcjonalne + Ciąg znaków lub null + Jeśli obecne, musi być niepustym ciągiem znaków lub null. + + + part.manufacturer + Opcjonalne + Obiekt/Tablica + + Jeśli obecne, musi być obiektem/tablicą i co najmniej jedno z poniższych pól musi być wypełnione: +
    +
  • manufacturer.id
  • +
  • manufacturer.name
  • +
+ + + + manufacturer.id + Opcjonalne + Liczba całkowita (Integer) + Liczba całkowita (Integer) > 0. Odpowiada wewnętrznemu identyfikatorowi numerowemu producenta. + + + manufacturer.name + Opcjonalne + Ciąg znaków (String) + Niepusty ciąg znaków, jeśli manufacturer.id nie jest podane. + + + part.category + Opcjonalne + Obiekt/Tablica + + Jeśli obecne, musi być obiektem/tablicą i co najmniej jedno z poniższych pól musi być wypełnione: +
    +
  • category.id
  • +
  • category.name
  • +
+ + + + category.id + Opcjonalne + Liczba całkowita (Integer) + Liczba całkowita (Integer) > 0. Odpowiada wewnętrznemu numerowi ID kategorii komponentu. + + + category.name + Opcjonalne + Ciąg znaków (String) + Niepusty ciąg znaków, jeśli category.id nie jest podane. + + + + ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Oczekiwane kolumny: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Uwaga: Nie wykonano mapowania z określonymi komponentami z zarządzania kategoriami.

+ ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Pole + Warunek + Typ Danych + Opis + + + + + Id + Opcjonalne + Liczba całkowita + Pole dowolne. Unikalny numer identyfikacyjny dla każdego komponentu. + + + Designer + Opcjonalne + Łańcuch znaków + Pole dowolne. Unikalny oznacznik referencyjny komponentu na PCB, np. "R1" dla rezystora 1. Używany do nazewnictwa położenia w grupie komponentów. + + + Obudowa + Opcjonalne + Łańcuch znaków + Pole dowolne. Typ lub forma obudowy komponentu, np. "0805" dla rezystorów SMD. + + + Ilość + Wymagane + Liczba całkowita + Liczba identycznych komponentów potrzebnych do stworzenia jednej instancji złożenia. + + + Oznaczenie + Wymagane + Łańcuch znaków + Opis lub funkcja komponentu, np. wartość rezystora "10kΩ" lub wartość kondensatora "100nF". Używane jako nazwa w pozycji na liście materiałowej (BOM). + + + Dostawca i referencja + Opcjonalne + Łańcuch znaków + Pole dowolne. Może zawierać, np. informacje specyficzne dla dystrybutora. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (część) + + + + + typeahead.parts.assembly.name + %name% (zespół) + + + + + projects.build.form.part + Część "%name%" + + + + + projects.build.form.assembly + Zespół "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (wymagana ilość: %quantity%) + + + + + projects.build.form.assembly.bom.entry.no.stock + brak na magazynie + + diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 62570acb0..540b9e355 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -4751,6 +4751,18 @@ Имя + + + part.table.name.value.for_part + %value% (Часть) + + + + + part.table.name.value.for_assembly + %value% (Сборка) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9813,6 +9825,18 @@ Компонент + + + project.bom.assembly + Сборка + + + + + project.bom.partOrAssembly + Выбор + + project.bom.add_entry @@ -9891,6 +9915,42 @@ Архивный + + + assembly.edit.status + Статус + + + + + assembly.status.draft + Черновик + + + + + assembly.status.planning + Планирование + + + + + assembly.status.in_production + В производстве + + + + + assembly.status.finished + Завершен + + + + + assembly.status.archived + Архивный + + part.new_build_part.error.build_part_already_exists @@ -10167,6 +10227,12 @@ запасено + + + project.builds.no_stock + склад не указан + + project.builds.needed @@ -10239,6 +10305,12 @@ Целевой лот + + + project.build.builds_part_lot_label + %name% (требуется: %quantity%) + + project.builds.number_of_builds @@ -12323,5 +12395,621 @@ Профиль сохранен! + + + assembly.label + Сборка + + + + + assembly.caption + Сборка + + + + + perm.assemblies + Сборки + + + + + assembly_bom_entry.label + Компоненты + + + + + assembly.labelp + Сборки + + + + + assembly.edit + Редактировать сборку + + + + + assembly.new + Новая сборка + + + + + assembly.edit.associated_build_part + Связанный компонент + + + + + assembly.edit.associated_build_part.add + Добавить компонент + + + + + assembly.edit.associated_build.hint + Этот компонент представляет изготовленные экземпляры сборки. Укажите, нужны ли изготовленные экземпляры. В противном случае количество компонентов будет использоваться только при создании соответствующего проекта. + + + + + assembly.edit.bom.import_bom + Импортировать компоненты + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Сборки + + + + + assembly.bom_import.flash.success + %count% компонент(ов) успешно импортировано в сборку. + + + + + assembly.bom_import.flash.invalid_entries + Ошибка валидации! Проверьте импортированный файл! + + + + + assembly.bom_import.flash.invalid_file + Не удалось импортировать файл. Убедитесь, что выбран правильный тип файла. Сообщение об ошибке: %message% + + + + + assembly.bom.quantity + Количество + + + + + assembly.bom.mountnames + Названия монтажей + + + + + assembly.bom.instockAmount + Количество на складе + + + + + assembly.info.title + Информация о сборке + + + + + assembly.info.info.label + Информация + + + + + assembly.info.sub_assemblies.label + Подсборки + + + + + assembly.info.builds.label + Сборка + + + + + assembly.info.bom_add_parts + Добавить детали + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Убедитесь, что действительно хотите выполнить сборку с этим статусом!]]> + + + + + assembly.builds.build_not_possible + Сборка невозможна: недостаточно деталей + + + + + assembly.builds.following_bom_entries_miss_instock + Недостаточно деталей на складе для сборки %number_of_builds% экземпляров. Следующие детали отсутствуют в достаточном количестве: + + + + + assembly.builds.build_possible + Сборка возможна + + + + + assembly.builds.number_of_builds_possible + %max_builds% экземпляров.]]> + + + + + assembly.builds.number_of_builds + Количество сборок + + + + + assembly.build.btn_build + Собрать + + + + + assembly.builds.no_stocked_builds + Собранные экземпляры на складе + + + + + assembly.info.bom_entries_count + Детали + + + + + assembly.info.sub_assemblies_count + Подсборки + + + + + assembly.builds.stocked + На складе + + + + + assembly.builds.needed + Необходимо + + + + + assembly.add_parts_to_assembly + Добавить детали в сборку + + + + + assembly.bom.name + Название + + + + + assembly.bom.comment + Примечания + + + + + assembly.builds.following_bom_entries_miss_instock_n + Недостаточно деталей на складе для сборки %number_of_builds% экземпляров. У следующих деталей недостаточное количество: + + + + + assembly.build.help + Выберите, из каких запасов брать необходимые для сборки детали (и в каком количестве). Установите галочку для каждой позиции, если детали были взяты, или используйте основную галочку, чтобы отметить все позиции сразу. + + + + + assembly.build.required_qty + Необходимое количество + + + + + assembly.import_bom + Импортировать детали для сборки + + + + + assembly.bom.part + Компонент + + + + + assembly.bom.add_entry + Добавить запись + + + + + assembly.bom.price + Цена + + + + + assembly.build.dont_check_quantity + Не проверять количество + + + + + assembly.build.dont_check_quantity.help + Если выбрано, указанные количества будут списаны со склада независимо от того, достаточно их или нет для указанной сборки. + + + + + assembly.build.add_builds_to_builds_part + Добавить собранные экземпляры как компонент для подсборки + + + + + assembly.bom_import.type + Тип + + + + + assembly.bom_import.type.json + JSON для сборки + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.clear_existing_bom + Очистить текущие данные перед импортом + + + + + assembly.bom_import.clear_existing_bom.help + Если выбрано, все существующие записи о деталях будут удалены и заменены импортированными. + + + + + assembly.import_bom.template.header.json + Шаблон импорта JSON для сборки + + + + + assembly.import_bom.template.header.kicad_pcbnew + Шаблон импорта CSV (KiCAD Pcbnew BOM) для сборки + + + + + assembly.bom_import.template.entry.name + Название детали в сборке + + + + + assembly.bom_import.template.entry.part.mpnr + Уникальный каталожный номер производителя + + + + + assembly.bom_import.template.entry.part.ipn + Уникальный IPN компонента + + + + + assembly.bom_import.template.entry.part.name + Уникальное имя компонента + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Уникальное название производителя + + + + + assembly.bom_import.template.entry.part.category.name + Уникальное название категории + + + + + assembly.bom_import.template.json.table + + + + + Поле + Условие + Тип данных + Описание + + + + + quantity + Обязательное + Дробное число (Float) + Поле должно быть заполнено и содержать дробное значение (Float), большее 0,0. + + + name + Опциональное + Строка (String) + Если присутствует, должно быть непустой строкой. + + + part + Опциональное + Объект/Массив + + Если указано, должно быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: +
    +
  • part.id
  • +
  • part.name
  • +
+ + + + part.id + Опциональное + Целое число (Integer) + Целое число (Integer) > 0. Соответствует внутреннему цифровому идентификатору компонента в Part-DB. + + + part.name + Опциональное + Строка (String) + Непустая строка, если part.mpnr или part.ipn не указаны. + + + part.mpnr + Опциональное + Строка (String) + Непустая строка, если part.name или part.ipn не указаны. + + + part.ipn + Опциональное + Строка (String) + Непустая строка, если part.name или part.mpnr не указаны. + + + part.description + Опциональное + Строка или null + Если присутствует, должно быть непустой строкой или null. + + + part.manufacturer + Опциональное + Объект/Массив + + Если присутствует, должно быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: +
    +
  • manufacturer.id
  • +
  • manufacturer.name
  • +
+ + + + manufacturer.id + Опциональное + Целое число (Integer) + Целое число (Integer) > 0. Соответствует внутреннему идентификатору производителя. + + + manufacturer.name + Опциональное + Строка (String) + Непустая строка, если manufacturer.id не указано. + + + part.category + Опциональное + Объект/Массив + + Если присутствует, должно быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: +
    +
  • category.id
  • +
  • category.name
  • +
+ + + + category.id + Опциональное + Целое число (Integer) + Целое число (Integer) > 0. Соответствует внутреннему цифровому идентификатору категории компонента. + + + category.name + Опциональное + Строка (String) + Непустая строка, если category.id не указано. + + + + ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Ожидаемые столбцы: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Примечание: Сопоставление с конкретными компонентами из управления категориями не выполняется.

+ ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Поле + Условие + Тип данных + Описание + + + + + ID + Опционально + Целое число + Произвольное поле. Уникальный идентификационный номер для каждого компонента. + + + Дизигнатор + Опционально + Строка + Произвольное поле. Уникальный референсный обозначитель компонента на печатной плате, например, "R1" для резистора 1. Используется для именования позиции в группе компонентов. + + + Корпус + Опционально + Строка + Произвольное поле. Тип или форм-фактор корпуса компонента, например, "0805" для SMD-резисторов. + + + Количество + Обязательно + Целое число + Количество одинаковых компонентов, необходимое для создания одной единицы сборки. + + + Обозначение + Обязательно + Строка + Описание или функция компонента, например, номинал резистора "10kΩ" или номинал конденсатора "100nF". Используется в качестве имени в позиции списка материалов (BOM). + + + Поставщик и ссылка + Опционально + Строка + Произвольное поле. Может содержать, например, информацию, специфичную для дистрибьютора. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (Деталь) + + + + + typeahead.parts.assembly.name + %name% (Сборка) + + + + + projects.build.form.part + Компонент "%name%" + + + + + projects.build.form.assembly + Сборка "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (необходимо: %quantity%) + + + + + projects.build.form.assembly.bom.entry.no.stock + Нет на складе + + diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 668c32f28..bf924444c 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -4749,6 +4749,18 @@ 名称 + + + part.table.name.value.for_part + %value%(部件) + + + + + part.table.name.value.for_assembly + %value%(组件) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9812,6 +9824,18 @@ Element 3 部件 + + + project.bom.assembly + 装配 + + + + + project.bom.partOrAssembly + 选择 + + project.bom.add_entry @@ -9890,6 +9914,42 @@ Element 3 已存档 + + + assembly.edit.status + 状态 + + + + + assembly.status.draft + 草稿 + + + + + assembly.status.planning + 策划 + + + + + assembly.status.in_production + 生产中 + + + + + assembly.status.finished + 已完成 + + + + + assembly.status.archived + 已归档 + + part.new_build_part.error.build_part_already_exists @@ -10166,6 +10226,12 @@ Element 3 在库 + + + project.builds.no_stock + 未指定库存 + + project.builds.needed @@ -10238,6 +10304,12 @@ Element 3 目标批次 + + + project.build.builds_part_lot_label + %name% (需求数量: %quantity%) + + project.builds.number_of_builds @@ -12208,5 +12280,621 @@ Element 3 成功创建 %COUNT% 个元素。 + + + assembly.label + 装配 + + + + + assembly.caption + 装配 + + + + + perm.assemblies + 装配列表 + + + + + assembly_bom_entry.label + 组件 + + + + + assembly.labelp + 装配列表 + + + + + assembly.edit + 编辑装配 + + + + + assembly.new + 新装配 + + + + + assembly.edit.associated_build_part + 关联组件 + + + + + assembly.edit.associated_build_part.add + 添加组件 + + + + + assembly.edit.associated_build.hint + 此组件表示装配的生产实例。指定是否需要生产实例。如果不需要,则组件数量仅在构建相关项目时使用。 + + + + + assembly.edit.bom.import_bom + 导入组件 + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + 装配列表 + + + + + assembly.bom_import.flash.success + 成功导入 %count% 个组件到装配中。 + + + + + assembly.bom_import.flash.invalid_entries + 验证错误!请检查导入的文件! + + + + + assembly.bom_import.flash.invalid_file + 文件导入失败。请确保选择了正确的文件格式。错误信息:%message% + + + + + assembly.bom.quantity + 数量 + + + + + assembly.bom.mountnames + 安装名称 + + + + + assembly.bom.instockAmount + 库存数量 + + + + + assembly.info.title + 装配信息 + + + + + assembly.info.info.label + 信息 + + + + + assembly.info.sub_assemblies.label + 子组件 + + + + + assembly.info.builds.label + 构建 + + + + + assembly.info.bom_add_parts + 添加零件 + + + + + assembly.builds.check_assembly_status + "%assembly_status%"。请确认您是否要在该状态下构建组件!]]> + + + + + assembly.builds.build_not_possible + 无法构建:零件数量不足 + + + + + assembly.builds.following_bom_entries_miss_instock + 库存中缺少足够的零件,无法构建 %number_of_builds% 次。缺少的零件包括: + + + + + assembly.builds.build_possible + 可以构建 + + + + + assembly.builds.number_of_builds_possible + %max_builds% 个该组件。]]> + + + + + assembly.builds.number_of_builds + 构建数量 + + + + + assembly.build.btn_build + 构建 + + + + + assembly.builds.no_stocked_builds + 已构建并库存的数量 + + + + + assembly.info.bom_entries_count + 条目 + + + + + assembly.info.sub_assemblies_count + 子组件 + + + + + assembly.builds.stocked + 库存中 + + + + + assembly.builds.needed + 需要 + + + + + assembly.add_parts_to_assembly + 添加零件到组件 + + + + + assembly.bom.name + 名称 + + + + + assembly.bom.comment + 备注 + + + + + assembly.builds.following_bom_entries_miss_instock_n + 库存不足,无法构建 %number_of_builds% 次。缺少零件包括: + + + + + assembly.build.help + 选择部分库存零件及数量用于构建。每项零件使用复选框,如果零件已提取,也可以使用主复选框来选择所有项目。 + + + + + assembly.build.required_qty + 所需数量 + + + + + assembly.import_bom + 导入组件的零件 + + + + + assembly.bom.part + 零件 + + + + + assembly.bom.add_entry + 添加条目 + + + + + assembly.bom.price + 价格 + + + + + assembly.build.dont_check_quantity + 不检查数量 + + + + + assembly.build.dont_check_quantity.help + 如果选中,即使库存不足,系统也会从库存中扣除声明的数量。 + + + + + assembly.build.add_builds_to_builds_part + 将已构建的零件添加到组件 + + + + + assembly.bom_import.type + 类型 + + + + + assembly.bom_import.type.json + JSON 文件(组件) + + + + + assembly.bom_import.type.kicad_pcbnew + CSV 文件(KiCAD Pcbnew) + + + + + assembly.bom_import.clear_existing_bom + 在导入前清空现有数据 + + + + + assembly.bom_import.clear_existing_bom.help + 如果选中,所有现有零件条目将被删除,新的导入数据将取而代之。 + + + + + assembly.import_bom.template.header.json + 装配 JSON 导入模板 + + + + + assembly.import_bom.template.header.kicad_pcbnew + 装配 CSV 模板(KiCAD Pcbnew BOM) + + + + + assembly.bom_import.template.entry.name + 组件的零件名称 + + + + + assembly.bom_import.template.entry.part.mpnr + 唯一制造商零件编号 + + + + + assembly.bom_import.template.entry.part.ipn + 唯一 IPN 序列号 + + + + + assembly.bom_import.template.entry.part.name + 零件名称 + + + + + assembly.bom_import.template.entry.part.manufacturer.name + 制造商名称 + + + + + assembly.bom_import.template.entry.part.category.name + 类别名称 + + + + + assembly.bom_import.template.json.table + + + + + 字段 + 条件 + 数据类型 + 描述 + + + + + quantity + 必填 + 浮点数 (Float) + 必须存在,并包含大于 0.0 的浮点值 (Float)。 + + + name + 可选 + 字符串 (String) + 如果存在,必须是非空字符串。 + + + part + 可选 + 对象/数组 + + 如果提供,则必须是对象/数组,并且以下字段中至少有一个被填写: +
    +
  • part.id
  • +
  • part.name
  • +
+ + + + part.id + 可选 + 整数 (Integer) + 整数 (Integer) > 0。表示组件在 Part-DB 中的内部数字 ID。 + + + part.name + 可选 + 字符串 (String) + 如果未提供 part.mpnr 或 part.ipn,则必须是非空字符串。 + + + part.mpnr + 可选 + 字符串 (String) + 如果未提供 part.name 或 part.ipn,则必须是非空字符串。 + + + part.ipn + 可选 + 字符串 (String) + 如果未提供 part.name 或 part.mpnr,则必须是非空字符串。 + + + part.description + 可选 + 字符串或 null + 如果存在,必须是非空字符串或 null。 + + + part.manufacturer + 可选 + 对象/数组 + + 如果存在,则必须是对象/数组,并且以下字段中至少有一个被填写: +
    +
  • manufacturer.id
  • +
  • manufacturer.name
  • +
+ + + + manufacturer.id + 可选 + 整数 (Integer) + 整数 (Integer) > 0。表示制造商的内部数字 ID。 + + + manufacturer.name + 可选 + 字符串 (String) + 如果未提供 manufacturer.id,则必须是非空字符串。 + + + part.category + 可选 + 对象/数组 + + 如果存在,则必须是对象/数组,并且以下字段中至少有一个被填写: +
    +
  • category.id
  • +
  • category.name
  • +
+ + + + category.id + 可选 + 整数 (Integer) + 整数 (Integer) > 0。表示组件类别的内部数字 ID。 + + + category.name + 可选 + 字符串 (String) + 如果未提供 category.id,则必须是非空字符串。 + + + + ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + 预期的列: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + 注意: 未对类别管理中的特定组件进行映射。

+ ]]> +
+
+
+ + + assembly.bom_import.template.kicad_pcbnew.table + + + + + 字段 + 条件 + 数据类型 + 描述 + + + + + ID + 可选 + 整数 + 自由格式字段。每个组件的唯一标识号。 + + + 设计ator + 可选 + 字符串 + 自由格式字段。PCB上组件的唯一参考标识符,例如电阻1的"R1"。用于命名组件组中的位置。 + + + 封装 + 可选 + 字符串 + 自由格式字段。组件的封装类型或形式因子,例如对于SMD电阻"0805"。 + + + 数量 + 必填 + 整数 + 创建一个组装实例所需的相同组件的数量。 + + + 描述 + 必填 + 字符串 + 组件的描述或功能,例如电阻值"10kΩ"或电容值"100nF"。在物料清单(BOM)条目中用作名称。 + + + 供应商和参考 + 可选 + 字符串 + 自由格式字段。例如,可以包含特定分销商的信息。 + + + + ]]> + + + + + + typeahead.parts.part.name + %name%(零件) + + + + + typeahead.parts.assembly.name + %name%(组件) + + + + + projects.build.form.part + 零件“%name%” + + + + + projects.build.form.assembly + 组件“%name%” + + + + + projects.build.form.assembly.bom.entry + %name%(需数量:%quantity%) + + + + + projects.build.form.assembly.bom.entry.no.stock + 库存不足 + + diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index c298266af..06354533e 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -245,6 +245,12 @@ Musíte vybrat díl pro položku BOM dílu nebo nastavit název pro položku BOM bez dílu. + + + validator.project.bom_entry.only_part_or_assembly_allowed + Je povoleno vybrat pouze jednu součástku nebo sestavu. Upravit prosím svůj výběr! + + project.bom_entry.name_already_in_bom @@ -365,5 +371,23 @@ Neplatný kód. Zkontrolujte, zda je vaše ověřovací aplikace správně nastavena a zda je čas správně nastaven jak na serveru, tak na ověřovacím zařízení. + + + assembly.bom_entry.part_already_in_bom + Tato součást již existuje ve skupině! + + + + + assembly.bom_entry.name_already_in_bom + Již existuje součást s tímto názvem! + + + + + validator.assembly.bom_entry.name_or_part_needed + Musíte vybrat součást nebo nastavit název pro nesoučást! + + diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 21149f0e7..9a9dea4cd 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -245,6 +245,12 @@ Du skal vælge en komponent eller angive et navn til en ikke-komponent styklistepost! + + + validator.project.bom_entry.only_part_or_assembly_allowed + Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! + + project.bom_entry.name_already_in_bom @@ -341,5 +347,23 @@ Denne leverandørstregkodeværdi er allerede brugt til en anden beholdning. Stregkoden skal være unik! + + + assembly.bom_entry.part_already_in_bom + Denne del eksisterer allerede i gruppen! + + + + + assembly.bom_entry.name_already_in_bom + Der findes allerede en del med dette navn! + + + + + validator.assembly.bom_entry.name_or_part_needed + Du skal vælge en del eller sætte et navn for en ikke-del! + + diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 9c123fd85..6fde3419a 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -242,7 +242,13 @@ validator.project.bom_entry.name_or_part_needed - Sie müssen ein Bauteil auswählen, oder einen Namen für ein nicht-Bauteil BOM-Eintrag setzen! + Sie müssen ein Bauteil bzw. eine Baugruppe auswählen, oder einen Namen für ein nicht-Bauteil BOM-Eintrag setzen! + + + + + validator.project.bom_entry.only_part_or_assembly_allowed + Es darf nur ein Bauteil oder eine Baugruppe ausgewählt werden. Bitte passen Sie Ihre Auswahl an! @@ -365,5 +371,23 @@ Ungültiger Code. Überprüfen Sie, dass die Authenticator App korrekt eingerichtet ist und dass der Server und das Gerät beide die korrekte Uhrzeit eingestellt haben. + + + assembly.bom_entry.part_already_in_bom + Dieses Bauteil existiert bereits in der Gruppe! + + + + + assembly.bom_entry.name_already_in_bom + Es gibt bereits einen Bauteil mit diesem Namen! + + + + + validator.assembly.bom_entry.name_or_part_needed + Sie müssen ein Bauteil auswählen, oder einen Namen für ein nicht-Bauteil setzen! + + diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index 9ef5b3de4..ee27863c0 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -7,5 +7,29 @@ Ο εσωτερικός αριθμός εξαρτήματος πρέπει να είναι μοναδικός. {{ value }} χρησιμοποιείται ήδη! + + + validator.project.bom_entry.only_part_or_assembly_allowed + Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! + + + + + assembly.bom_entry.part_already_in_bom + Αυτό το εξάρτημα υπάρχει ήδη στην ομάδα! + + + + + assembly.bom_entry.name_already_in_bom + Υπάρχει ήδη ένα εξάρτημα με αυτό το όνομα! + + + + + validator.assembly.bom_entry.name_or_part_needed + Πρέπει να επιλέξετε ένα εξάρτημα ή να βάλετε ένα όνομα για ένα μη εξάρτημα! + + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 6ad144607..86525b6a8 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -242,7 +242,13 @@ validator.project.bom_entry.name_or_part_needed - You have to choose a part for a part BOM entry or set a name for a non-part BOM entry. + You have to select a part or assembly, or set a name for a non-component Bom entry! + + + + + validator.project.bom_entry.only_part_or_assembly_allowed + Only one part or assembly may be selected. Please modify your selection! @@ -365,5 +371,23 @@ Invalid code. Check that your authenticator app is set up correctly and that both the server and authentication device has the time set correctly. + + + assembly.bom_entry.part_already_in_bom + __assembly.bom_entry.part_already_in_bom + + + + + assembly.bom_entry.name_already_in_bom + __assembly.bom_entry.name_already_in_bom + + + + + validator.assembly.bom_entry.name_or_part_needed + __validator.assembly.bom_entry.name_or_part_needed + + diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index e86ab9ccc..e9bf32597 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -203,5 +203,29 @@ L'emplacement de stockage a été marqué comme "Composant seul", par conséquent aucun nouveau composant ne peut être ajouté. + + + validator.project.bom_entry.only_part_or_assembly_allowed + Seule une pièce ou un assemblage peut être sélectionné. Veuillez ajuster votre sélection! + + + + + assembly.bom_entry.part_already_in_bom + Cette pièce existe déjà dans le groupe! + + + + + assembly.bom_entry.name_already_in_bom + Il existe déjà une pièce avec ce nom! + + + + + validator.assembly.bom_entry.name_or_part_needed + Vous devez sélectionner une pièce ou attribuer un nom pour un non-élément ! + + diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 29e32a16c..9c9c3960c 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -245,6 +245,12 @@ Morate odabrati dio za unos u BOM ili postaviti naziv za unos koji nije dio. + + + validator.project.bom_entry.only_part_or_assembly_allowed + Dozvoljeno je odabrati samo jednu komponentu ili sklop. Molimo prilagodite svoj odabir! + + project.bom_entry.name_already_in_bom @@ -359,5 +365,23 @@ Neispravan kod. Provjerite je li vaša aplikacija za autentifikaciju ispravno postavljena i jesu li poslužitelj i uređaj za autentifikaciju ispravno postavili vrijeme. + + + assembly.bom_entry.part_already_in_bom + Ovaj dio već postoji u grupi! + + + + + assembly.bom_entry.name_already_in_bom + Već postoji dio s tim nazivom! + + + + + validator.assembly.bom_entry.name_or_part_needed + Morate odabrati dio ili unijeti naziv za nedio! + + diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index 7043f4f34..2f747bc5c 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -245,6 +245,12 @@ È necessario selezionare un componente o assegnare un nome ad una voce BOM che non indica un componente! + + + validator.project.bom_entry.only_part_or_assembly_allowed + È consentito selezionare solo una parte o un assieme. Si prega di modificare la selezione! + + project.bom_entry.name_already_in_bom @@ -359,5 +365,23 @@ Codice non valido. Controlla che la tua app di autenticazione sia impostata correttamente e che sia il server che il dispositivo di autenticazione abbiano l'ora impostata correttamente. + + + assembly.bom_entry.part_already_in_bom + Questa parte è già presente nel gruppo! + + + + + assembly.bom_entry.name_already_in_bom + Esiste già una parte con questo nome! + + + + + validator.assembly.bom_entry.name_or_part_needed + È necessario selezionare una parte o inserire un nome per un non-parte! + + diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index 01cc3f77b..0156ffefc 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -203,5 +203,29 @@ 新しい部品を追加できません。保管場所は「1つの部品のみ」とマークされています。 + + + validator.project.bom_entry.only_part_or_assembly_allowed + 部品またはアセンブリのみ選択可能です。選択内容を調整してください! + + + + + assembly.bom_entry.part_already_in_bom + この部品はすでにグループに存在します! + + + + + assembly.bom_entry.name_already_in_bom + この名前の部品はすでに存在します! + + + + + validator.assembly.bom_entry.name_or_part_needed + 部品を選択するか、非部品の名前を入力する必要があります! + + diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 6c9977983..2cc4aef43 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -245,6 +245,12 @@ Należy wybrać część dla wpisu BOM części lub ustawić nazwę dla wpisu BOM niebędącego częścią. + + + validator.project.bom_entry.only_part_or_assembly_allowed + Można wybrać tylko jedną część lub zespół. Proszę dostosować swój wybór! + + project.bom_entry.name_already_in_bom @@ -359,5 +365,23 @@ Nieprawidłowy kod. Sprawdź, czy aplikacja uwierzytelniająca jest poprawnie skonfigurowana i czy zarówno serwer, jak i urządzenie uwierzytelniające mają poprawnie ustawiony czas. + + + assembly.bom_entry.part_already_in_bom + Ten element już istnieje w grupie! + + + + + assembly.bom_entry.name_already_in_bom + Element o tej nazwie już istnieje! + + + + + validator.assembly.bom_entry.name_or_part_needed + Musisz wybrać element lub przypisać nazwę dla elementu niestandardowego! + + diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 0f97c4781..4049b453e 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -245,6 +245,12 @@ Вам необходимо выбрать компонент или задать имя для BOM, не относящейся к компоненту! + + + validator.project.bom_entry.only_part_or_assembly_allowed + Можно выбрать только деталь или сборку. Пожалуйста, измените ваш выбор! + + project.bom_entry.name_already_in_bom @@ -359,5 +365,23 @@ Неверный код. Проверьте, что приложение аутентификации настроено правильно и что на сервере и устройстве аутентификации установлено правильное время. + + + assembly.bom_entry.part_already_in_bom + Эта деталь уже существует в группе! + + + + + assembly.bom_entry.name_already_in_bom + Деталь с таким названием уже существует! + + + + + validator.assembly.bom_entry.name_or_part_needed + Необходимо выбрать деталь или ввести название для недетали! + + diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 08c9f014e..3eab5c4e3 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -245,6 +245,12 @@ 您必须为 BOM 条目选择部件,或为非部件 BOM 条目设置名称。 + + + validator.project.bom_entry.only_part_or_assembly_allowed + 只能选择一个零件或组件。请修改您的选择! + + project.bom_entry.name_already_in_bom @@ -347,5 +353,23 @@ 由于技术限制,在32位系统中无法选择2038年1月19日之后的日期! + + + assembly.bom_entry.part_already_in_bom + 此零件已存在于组中! + + + + + assembly.bom_entry.name_already_in_bom + 具有此名称的零件已存在! + + + + + validator.assembly.bom_entry.name_or_part_needed + 必须选择零件或为非零件指定名称! + + From 31093abdcaf6109afe14d66081a7c6c26bcc6534 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 20 Mar 2025 09:55:48 +0100 Subject: [PATCH 02/83] =?UTF-8?q?Default-Sortierung=20f=C3=BCr=20Assemblie?= =?UTF-8?q?s=20per=20YAML-Konfiguration=20einf=C3=BChren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 8 ++++++ config/parameters.yaml | 4 +++ config/services.yaml | 3 ++ .../AssemblyBomEntriesDataTable.php | 28 +++++++++++-------- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/.env b/.env index 982d4bbd0..201633989 100644 --- a/.env +++ b/.env @@ -60,6 +60,14 @@ ERROR_PAGE_ADMIN_EMAIL='' # If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them... ERROR_PAGE_SHOW_HELP=1 +################################################################################## +# Part table settings +################################################################################## + +# Configure which columns will be visible by default in the specific table (and in which order). +# This is a comma separated list of column names. See documentation for available values. +TABLE_ASSEMBLIES_DEFAULT_COLUMNS=quantity,manufacturer,name,description,category + ################################################################################### # SAML Single sign on-settings diff --git a/config/parameters.yaml b/config/parameters.yaml index 154fbd8a5..a993d2d5e 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -43,6 +43,10 @@ parameters: ###################################################################################################################### partdb.saml.enabled: '%env(bool:SAML_ENABLED)%' # If this is set to true, SAML authentication is enabled + ###################################################################################################################### + # Table settings + ###################################################################################################################### + partdb.table.assemblies.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_DEFAULT_COLUMNS)%' # The default columns in assembly tables and their order ###################################################################################################################### # Miscellaneous diff --git a/config/services.yaml b/config/services.yaml index 17611ceab..80ca69771 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -170,6 +170,9 @@ services: App\DataTables\Helpers\ColumnSortHelper: shared: false # Service has a state so not share it between different tables + App\DataTables\AssemblyBomEntriesDataTable: + arguments: + $visible_columns: '%partdb.table.assemblies.default_columns%' #################################################################################################################### # Label system diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php index 7149ed5f5..a953179a9 100644 --- a/src/DataTables/AssemblyBomEntriesDataTable.php +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -25,6 +25,7 @@ use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Helpers\ColumnSortHelper; use App\DataTables\Helpers\PartDataTableHelper; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; @@ -41,14 +42,19 @@ class AssemblyBomEntriesDataTable implements DataTableTypeInterface { - public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter) - { + public function __construct( + protected TranslatorInterface $translator, + protected PartDataTableHelper $partDataTableHelper, + protected EntityURLGenerator $entityURLGenerator, + protected AmountFormatter $amountFormatter, + private string $visible_columns, + private ColumnSortHelper $csh + ){ } - public function configure(DataTable $dataTable, array $options): void { - $dataTable + $this->csh //->add('select', SelectColumn::class) ->add('picture', TextColumn::class, [ 'label' => '', @@ -62,7 +68,6 @@ public function configure(DataTable $dataTable, array $options): void ]) ->add('id', TextColumn::class, [ 'label' => $this->translator->trans('part.table.id'), - 'visible' => false, ]) ->add('quantity', TextColumn::class, [ 'label' => $this->translator->trans('assembly.bom.quantity'), @@ -97,11 +102,12 @@ public function configure(DataTable $dataTable, array $options): void ->add('ipn', TextColumn::class, [ 'label' => $this->translator->trans('part.table.ipn'), 'orderField' => 'NATSORT(part.ipn)', - 'visible' => false, 'render' => function ($value, AssemblyBOMEntry $context) { if($context->getPart() instanceof Part) { return $context->getPart()->getIpn(); } + + return ''; } ]) ->add('description', MarkdownColumn::class, [ @@ -142,7 +148,6 @@ public function configure(DataTable $dataTable, array $options): void ]) ->add('instockAmount', TextColumn::class, [ 'label' => 'assembly.bom.instockAmount', - 'visible' => false, 'render' => function ($value, AssemblyBOMEntry $context) { if ($context->getPart() !== null) { return $this->partDataTableHelper->renderAmount($context->getPart()); @@ -153,7 +158,6 @@ public function configure(DataTable $dataTable, array $options): void ]) ->add('storageLocations', TextColumn::class, [ 'label' => 'part.table.storeLocations', - 'visible' => false, 'render' => function ($value, AssemblyBOMEntry $context) { if ($context->getPart() !== null) { return $this->partDataTableHelper->renderStorageLocations($context->getPart()); @@ -164,15 +168,17 @@ public function configure(DataTable $dataTable, array $options): void ]) ->add('addedDate', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.addedDate'), - 'visible' => false, ]) ->add('lastModified', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.lastModified'), - 'visible' => false, ]) ; - $dataTable->addOrderBy('name', DataTable::SORT_ASCENDING); + //Apply the user configured order and visibility and add the columns to the table + $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->visible_columns, + "TABLE_ASSEMBLIES_DEFAULT_COLUMNS"); + + $dataTable->addOrderBy('name'); $dataTable->createAdapter(ORMAdapter::class, [ 'entity' => Attachment::class, From 39d3c49e83bc0b4e220e35e19a463a40c33eaaf8 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 20 Mar 2025 10:02:30 +0100 Subject: [PATCH 03/83] =?UTF-8?q?configuration.md:=20Info=20f=C3=BCr=20Def?= =?UTF-8?q?ault-Sortierung=20zu=20Assemblies=20einf=C3=BCgen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/configuration.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index d4b217816..efa3efd34 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -137,6 +137,10 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`. +* `TABLE_ASSEMBLIES_DEFAULT_COLUMNS`: The columns in assemblies tables, which are visible by default (when loading table for first + time). + Also specify the default order of the columns. This is a comma separated list of column names. Available columns + are: `name`, `id`, `quantity`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `mountnames`, `instockAmount`, `storageLocations`, `addedDate`, `lastModified`. ### History/Eventlog-related settings From 618f0edb9eac4d948f4d5bd8779d0b99c30a9f53 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 2 Apr 2025 12:24:14 +0200 Subject: [PATCH 04/83] Anpassungen aus Benutzersicht, um den Fokus auf die reine Baugruppen-Konfiguration zu legen --- .../AdminPages/BaseAdminController.php | 2 + src/Form/Type/AssemblySelectType.php | 1 - templates/admin/assembly_admin.html.twig | 4 +- templates/admin/base_admin.html.twig | 2 +- templates/assemblies/info/_bom.html.twig | 22 ---- .../assemblies/info/_info_card.html.twig | 15 --- templates/assemblies/info/_part.html.twig | 5 + .../assemblies/info/_subassemblies.html.twig | 28 ----- templates/assemblies/info/info.html.twig | 100 ++++++++++-------- .../form/collection_types_layout.html.twig | 1 + 10 files changed, 66 insertions(+), 114 deletions(-) delete mode 100644 templates/assemblies/info/_bom.html.twig create mode 100644 templates/assemblies/info/_part.html.twig delete mode 100644 templates/assemblies/info/_subassemblies.html.twig diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index edc5917ac..8c8d7520d 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -232,6 +232,7 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit 'timeTravel' => $timeTravel_timestamp, 'repo' => $repo, 'partsContainingElement' => $repo instanceof PartsContainingRepositoryInterface, + 'showParameters' => !($this instanceof AssemblyAdminController), ]); } @@ -382,6 +383,7 @@ protected function _new(Request $request, EntityManagerInterface $em, EntityImpo 'import_form' => $import_form, 'mass_creation_form' => $mass_creation_form, 'route_base' => $this->route_base, + 'showParameters' => !($this instanceof AssemblyAdminController), ]); } diff --git a/src/Form/Type/AssemblySelectType.php b/src/Form/Type/AssemblySelectType.php index ee6cf7c2a..10e858f26 100644 --- a/src/Form/Type/AssemblySelectType.php +++ b/src/Form/Type/AssemblySelectType.php @@ -71,7 +71,6 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setDefaults([ 'class' => Assembly::class, 'choice_label' => 'name', - 'placeholder' => 'None', 'compound' => true, 'error_bubbling' => false, ]); diff --git a/templates/admin/assembly_admin.html.twig b/templates/admin/assembly_admin.html.twig index d8b3ab257..57dde7d15 100644 --- a/templates/admin/assembly_admin.html.twig +++ b/templates/admin/assembly_admin.html.twig @@ -15,7 +15,7 @@ {% endblock %} {% block additional_pills %} - + {% endblock %} {% block quick_links %} @@ -47,7 +47,7 @@ {% endblock %} {% block additional_panes %} -
+
{% form_theme form.bom_entries with ['form/collection_types_layout_assembly.html.twig'] %} {{ form_errors(form.bom_entries) }} {{ form_widget(form.bom_entries) }} diff --git a/templates/admin/base_admin.html.twig b/templates/admin/base_admin.html.twig index 51790c3c1..e9fc0fb99 100644 --- a/templates/admin/base_admin.html.twig +++ b/templates/admin/base_admin.html.twig @@ -86,7 +86,7 @@ - {% if entity.parameters is defined %} + {% if entity.parameters is defined and showParameters == true %} diff --git a/templates/assemblies/info/_bom.html.twig b/templates/assemblies/info/_bom.html.twig deleted file mode 100644 index 6a2ca3e03..000000000 --- a/templates/assemblies/info/_bom.html.twig +++ /dev/null @@ -1,22 +0,0 @@ -{% import "components/datatables.macro.html.twig" as datatables %} - - - -{{ datatables.datatable(datatable, 'elements/datatables/datatables', 'assemblies') }} \ No newline at end of file diff --git a/templates/assemblies/info/_info_card.html.twig b/templates/assemblies/info/_info_card.html.twig index 508b2b069..2d0c535b2 100644 --- a/templates/assemblies/info/_info_card.html.twig +++ b/templates/assemblies/info/_info_card.html.twig @@ -37,12 +37,6 @@ {% trans %}entity.info.attachments.tab{% endtrans %} {% endif %} - {% if assembly.parameters is not empty %} - - - {% trans %}entity.info.parameters.tab{% endtrans %} - - {% endif %} {% if assembly.comment is not empty %} @@ -108,15 +102,6 @@
{% endif %} - {% if assembly.parameters is not empty %} -
- {% for name, parameters in assembly.groupedParameters %} - {% if name is not empty %}
{{ name }}
{% endif %} - {{ helper.parameters_table(assembly) }} - {% endfor %} -
- {% endif %} - {% if assembly.comment is not empty %}
diff --git a/templates/assemblies/info/_part.html.twig b/templates/assemblies/info/_part.html.twig new file mode 100644 index 000000000..1fa8b90ed --- /dev/null +++ b/templates/assemblies/info/_part.html.twig @@ -0,0 +1,5 @@ +{% import "components/datatables.macro.html.twig" as datatables %} + +
+ +{{ datatables.datatable(datatable, 'elements/datatables/datatables', 'assemblies') }} \ No newline at end of file diff --git a/templates/assemblies/info/_subassemblies.html.twig b/templates/assemblies/info/_subassemblies.html.twig deleted file mode 100644 index 8c92c5e91..000000000 --- a/templates/assemblies/info/_subassemblies.html.twig +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - {% for subassembly in assembly.children %} - - - - - - - {% endfor %} - -
{% trans %}name.label{% endtrans %}{% trans %}description.label{% endtrans %}# {% trans %}assembly.info.bom_entries_count{% endtrans %}# {% trans %}assembly.info.sub_assemblies_count{% endtrans %}
{# Name #} - {{ subassembly.name }} - {# Description #} - {{ subassembly.description | format_markdown }} - - {{ subassembly.bomEntries | length }} - - {{ subassembly.children | length }} -
\ No newline at end of file diff --git a/templates/assemblies/info/info.html.twig b/templates/assemblies/info/info.html.twig index f5dac1e63..166535a64 100644 --- a/templates/assemblies/info/info.html.twig +++ b/templates/assemblies/info/info.html.twig @@ -5,8 +5,49 @@ {% trans %}assembly.info.title{% endtrans %}: {{ assembly.name }} {% endblock %} -{% block content %} +{% block before_card %} +
+{% endblock %} + +{% block content %} {{ helper.breadcrumb_entity_link(assembly) }} {{ parent() }} {% endblock %} @@ -23,30 +64,20 @@ {% block card_content %}
-
- {% include "assemblies/info/_info.html.twig" %} +
+ {% include "assemblies/info/_part.html.twig" %}
- {% if assembly.children is not empty %} -
- {% include "assemblies/info/_subassemblies.html.twig" %} -
- {% endif %} -
- {% include "assemblies/info/_bom.html.twig" %} +
+ {% include "assemblies/info/_info.html.twig" %}
{% include "assemblies/info/_builds.html.twig" %} @@ -94,12 +110,6 @@
{% include "parts/info/_attachments_info.html.twig" with {"part": assembly} %}
-
- {% for name, parameters in assembly.groupedParameters %} - {% if name is not empty %}
{{ name }}
{% endif %} - {{ helper.parameters_table(assembly.parameters) }} - {% endfor %} -
{% endblock %} \ No newline at end of file diff --git a/templates/form/collection_types_layout.html.twig b/templates/form/collection_types_layout.html.twig index def235000..0175aeaf6 100644 --- a/templates/form/collection_types_layout.html.twig +++ b/templates/form/collection_types_layout.html.twig @@ -44,6 +44,7 @@ {{ form_row(form.part) }} {{ form_errors(form.part) }} +
{{ form_widget(form.assembly) }} {{ form_errors(form.assembly) }} From 87d2f061e25f20971fe24d60fa6a5b34c76038a6 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 3 Apr 2025 12:13:24 +0200 Subject: [PATCH 05/83] =?UTF-8?q?=C3=9Cbersetzung=20zu=20"assembly.bom=5Fi?= =?UTF-8?q?mport.template.kicad=5Fpcbnew.table"=20anpassen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/assemblies/import_bom.html.twig | 2 +- translations/messages.cs.xlf | 56 +++++++++++------------ translations/messages.da.xlf | 44 +++++++++--------- translations/messages.de.xlf | 24 +++++----- translations/messages.el.xlf | 52 ++++++++++----------- translations/messages.en.xlf | 30 ++++++------ translations/messages.es.xlf | 34 +++++++------- translations/messages.fr.xlf | 34 +++++++------- translations/messages.it.xlf | 34 +++++++------- translations/messages.ja.xlf | 38 +++++++-------- translations/messages.nl.xlf | 46 +++++++++---------- translations/messages.pl.xlf | 42 ++++++++--------- translations/messages.ru.xlf | 44 +++++++++--------- translations/messages.zh.xlf | 38 +++++++-------- 14 files changed, 259 insertions(+), 259 deletions(-) diff --git a/templates/assemblies/import_bom.html.twig b/templates/assemblies/import_bom.html.twig index 53168b438..dc9430422 100644 --- a/templates/assemblies/import_bom.html.twig +++ b/templates/assemblies/import_bom.html.twig @@ -52,7 +52,7 @@
Id;Designator;Package;Quantity;Designation;Supplier and ref
{{ 'assembly.bom_import.template.kicad_pcbnew.exptected_columns.note'|trans|raw }} - {{ 'assembly.bom_import.template.json.table'|trans|raw }} + {{ 'assembly.bom_import.template.kicad_pcbnew.table'|trans|raw }}
diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index f5823e8d8..cbe8833ea 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -13642,56 +13642,56 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz - + assembly.bom_import.template.kicad_pcbnew.table - - Pole - Podmínka - Datový typ - Popis - + + Pole + Podmínka + Datový typ + Popis + Id - Volitelný - Celé číslo (Integer) - Volný údaj. Jedinečné identifikační číslo pro každou součástku. + Volitelné + Celé číslo + Volné pole. Unikátní identifikační číslo pro každou součástku. Designator - Volitelný - Řetězec (String) - Volný údaj. Jedinečný referenční označovač součástky na desce plošných spojů, např. „R1“ pro rezistor 1. Používá se pro název osazení součástky v rámci skupiny součástek. + Volitelné + Řetězec + Volné pole. Unikátní referenční označení součástky na PCB, např. „R1“ pro rezistor 1.
Používá se pro název umístění v rámci součástkové skupiny. Package - Volitelný - Řetězec (String) - Volný údaj. Pouzdro nebo tvar součástky, např. „0805“ pro SMD rezistory. + Volitelné + Řetězec + Volné pole. Pouzdro nebo formát součástky, např. „0805“ pro SMD rezistory.
Není použito pro záznam součástky v rámci sestavy. - Množství - Povinný - Celé číslo (Integer) - Počet identických součástek, které jsou potřeba k vytvoření jedné instance sestavy. + Quantity + Povinné pole + Celé číslo + Počet identických součástek potřebných k vytvoření jedné instance sestavy.
Použito jako počet záznamu součástky v rámci sestavy. - Určení - Povinný - Řetězec (String) - Popis nebo funkce součástky, např. hodnota rezistoru „10kΩ“ nebo hodnota kondenzátoru „100nF“. Používá se pro název položky v BOM. + Designation + Povinné pole + Řetězec + Popis nebo funkce součástky, např. hodnota rezistoru „10kΩ“ nebo hodnota kondenzátoru „100nF“.
Použito jako název záznamu součástky v rámci sestavy. - Dodavatel a ref - Volitelný - Řetězec (String) - Volný údaj. Může obsahovat např. specifické údaje distributora. + Supplier and ref + Volitelné + Řetězec + Volné pole. Může obsahovat např. specifické informace o distributorovi.
Používá se jako poznámka k záznamu součástky v rámci sestavy. diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index f231163a1..2461f8c77 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -12803,56 +12803,56 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
- + assembly.bom_import.template.kicad_pcbnew.table - - Felt - Betingelse - Datatype - Beskrivelse - + + Felt + Betingelse + Datatype + Beskrivelse + Id - Valgfri - Heltal (Integer) - Fri oplysning. Et unikt identifikationsnummer for hver komponent. + Valgfrit + Heltal + Fri tekst. Et unikt identifikationsnummer for hver komponent. Designator - Valgfri + Valgfrit Streng - Fri oplysning. En unik referencebetegnelse for komponenten på printkortet, f.eks. "R1" for modstand 1. Bruges til navngivning af monteringssæt i komponentgruppen. + Fri tekst. En unik referencebetegnelse for komponenten på PCB'en, f.eks. “R1” for modstand 1.
Bruges som navnet på placeringen i komponentgruppen. Package - Valgfri + Valgfrit Streng - Fri oplysning. Komponentenheden eller -formatet, f.eks. "0805" for SMD-modstande. + Fri tekst. Komponentens kabinet eller formfaktor, f.eks. “0805” for SMD-modstande.
Ikke inkluderet som komponentoplysning i samlingen. - Antal + Quantity Påkrævet - Heltal (Integer) - Antallet af identiske komponenter, der kræves for at oprette en enkelt instans af samling. + Heltal + Antallet af identiske komponenter, der kræves for at oprette en enkelt forekomst af samlingen.
Bruges som antal af komponentoplysning i samlingen. - Betegnelse + Designation Påkrævet Streng - Beskrivelse eller funktion for komponenten, f.eks. modstandsværdi "10kΩ" eller kondensatorværdi "100nF". Bruges til navnet i BOM-posten. + Beskrivelse eller funktion af komponenten, f.eks. modstandsværdi “10kΩ” eller kondensatorværdi “100nF”.
Bruges som navnet på komponentoplysningen i samlingen. - Leverandør og ref - Valgfri + Supplier and ref + Valgfrit Streng - Fri oplysning. Kan indeholde f.eks. distributørspecifik værdi. + Fri tekst. Kan indeholde, for eksempel, specifikke oplysninger fra en distributør.
Bruges som en note til komponentoplysningen i samlingen. diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index c32bf46a7..bc0cac33f 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -13446,19 +13446,19 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
- + assembly.bom_import.template.kicad_pcbnew.table - - Feld - Bedingung - Datentyp - Beschreibung - + + Feld + Bedingung + Datentyp + Beschreibung + @@ -13471,31 +13471,31 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Designator Optional String - Offene Angabe. Ein eindeutiger Referenzbezeichner des Bauteils auf der Leiterplatte, z.B. „R1“ für Widerstand 1. Wird für den Bestückungsnamen des Bauteil-Eintrags innerhalb der Bauteilgruppe verwendet. + Offene Angabe. Ein eindeutiger Referenzbezeichner des Bauteils auf der Leiterplatte, z.B. „R1“ für Widerstand 1.
Wird in den Bestückungsnamen des Bauteil-Eintrags in der Baugruppe übernommen. Package Optional String - Offene Angabe. Das Gehäuse oder die Bauform des Bauteils, z.B. „0805“ für SMD-Widerstände. + Offene Angabe. Das Gehäuse oder die Bauform des Bauteils, z.B. „0805“ für SMD-Widerstände.
Wird für ein Bauteil-Eintrag innerhalb der Baugruppe nicht übernommen. Quantity Pflichtfeld Ganzzahl (Integer) - Anzahl der identischen Bauteile, die benötigt werden, um eine Instanz der Baugruppe zu erstellen. + Anzahl der identischen Bauteile, die benötigt werden, um eine Instanz der Baugruppe zu erstellen.
Wird als Anzahl des Bauteil-Eintrags innerhalb der Baugruppe übernommen. Designation Pflichtfeld String - Beschreibung oder Funktion des Bauteils, z.B. Widerstandswert „10kΩ“ oder Kondensatorwert „100nF“. Wird für den Namen des BOM-Eintrags verwendet. + Beschreibung oder Funktion des Bauteils, z.B. Widerstandswert „10kΩ“ oder Kondensatorwert „100nF“.
Wird in den Namen des Bauteil-Eintrags innerhalb der Baugruppe übernommen. Supplier and ref Optional String - Offene Angabe. Kann z.B. Distributor spezifischen Wert enthalten. + Offene Angabe. Kann z.B. Distributor spezifischen Wert enthalten.
Wird als Notiz zum Bauteil-Eintrag innerhalb der Baugruppe übernommen. diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 8fdb801b7..b54131896 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -2130,56 +2130,56 @@
- + assembly.bom_import.template.kicad_pcbnew.table - - Πεδίο - Εκπλήρωση - Τύπος δεδομένων - Περιγραφή - + + Πεδίο + Προϋπόθεση + Τύπος Δεδομένων + Περιγραφή + Id Προαιρετικό - Ακέραιος αριθμός (Integer) - Ελεύθερη καταχώρηση. Μοναδικός αριθμός ταυτοποίησης για κάθε εξάρτημα. + Ακέραιος + Πεδίο ελεύθερης μορφής. Ένας μοναδικός αριθμός ταυτοποίησης για κάθε εξάρτημα. - Σχεδιαστής + Designator Προαιρετικό - Συμβολοσειρά (String) - Ελεύθερη καταχώρηση. Μοναδικός αναγνωριστικός δείκτης του εξαρτήματος στην πλακέτα κυκλώματος, π.χ. "R1" για την αντίσταση 1. Χρησιμοποιείται για το όνομα του εξαρτήματος στο πλαίσιο της ομάδας εξαρτημάτων. + Συμβολοσειρά + Πεδίο ελεύθερης μορφής. Ένας μοναδικός αναγνωριστικός δείκτης για το εξάρτημα στην πλακέτα PCB, π.χ. "R1" για τον αντιστάτη 1.
Χρησιμοποιείται για την ονομασία της θέσης στην ομάδα εξαρτημάτων. - Συσκευασία + Package Προαιρετικό - Συμβολοσειρά (String) - Ελεύθερη καταχώρηση. Ο τύπος ή η μορφή του εξαρτήματος, π.χ. "0805" για αντιστάσεις SMD. + Συμβολοσειρά + Πεδίο ελεύθερης μορφής. Η θήκη ή ο μορφολογικός τύπος του εξαρτήματος, π.χ. "0805" για τους SMD αντιστάτες.
Δεν περιλαμβάνεται στις πληροφορίες εξαρτήματος της συναρμολόγησης. - Ποσότητα - Υποχρεωτικό - Ακέραιος αριθμός (Integer) - Ο αριθμός των πανομοιότυπων εξαρτημάτων που απαιτούνται για τη δημιουργία μίας μονάδας του συνόλου. + Quantity + Απαιτείται + Ακέραιος + Ο αριθμός πανομοιότυπων εξαρτημάτων που απαιτούνται για τη δημιουργία μιας μοναδικής παρουσίας της συναρμολόγησης.
Χρησιμοποιείται ως ποσότητα στις πληροφορίες εξαρτήματος της συναρμολόγησης. - Ορισμός - Υποχρεωτικό - Συμβολοσειρά (String) - Περιγραφή ή λειτουργία του εξαρτήματος, π.χ. αντίσταση "10kΩ" ή χωρητικότητα "100nF". Χρησιμοποιείται για το όνομα της εγγραφής στο BOM. + Designation + Απαιτείται + Συμβολοσειρά + Η περιγραφή ή η λειτουργία του εξαρτήματος, π.χ. τιμή αντιστάτη "10kΩ" ή τιμή πυκνωτή "100nF".
Χρησιμοποιείται ως το όνομα στις πληροφορίες εξαρτήματος της συναρμολόγησης. - Προμηθευτής και παραπομπή + Supplier and ref Προαιρετικό - Συμβολοσειρά (String) - Ελεύθερη καταχώρηση. Μπορεί να περιλαμβάνει, π.χ., ειδική τιμή από διανομέα. + Συμβολοσειρά + Πεδίο ελεύθερης μορφής. Μπορεί να περιλαμβάνει, για παράδειγμα, συγκεκριμένες πληροφορίες για τον προμηθευτή.
Χρησιμοποιείται ως σημείωση για τις πληροφορίες εξαρτήματος της συναρμολόγησης. diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index d267c89ac..8ac704001 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13447,56 +13447,56 @@ Please note, that you can not impersonate a disabled user. If you try you will g
- + assembly.bom_import.template.kicad_pcbnew.table - - Field - Condition - Data Type - Description - + + Field + Condition + Data Type + Description + Id Optional Integer - Free-form field. A unique identification number for each component. + Free-form field. A unique identification number for each part. Designator Optional String - Free-form field. A unique reference designator of the component on the PCB, e.g., “R1” for resistor 1. Used for naming the placement in the component group. + Free-form field. A unique reference designator of the part on the PCB, e.g., “R1” for resistor 1.
Used for the placement name of the part entry in the assembly. Package Optional String - Free-form field. The casing or form factor of the component, e.g., “0805” for SMD resistors. + Free-form field. The case or form factor of the part, e.g., “0805” for SMD resistors.
Not adopted for a part entry within the assembly. Quantity - Required + Required field Integer - The number of identical components required to create a single instance of an assembly. + The number of identical parts needed to create an instance of the assembly.
Adopted as the quantity of the part entry within the assembly. Designation - Required + Required field String - The description or function of the component, e.g., resistor value “10kΩ” or capacitor value “100nF.” Used for the name in the BOM entry. + Description or function of the part, e.g., resistor value “10kΩ” or capacitor value “100nF.”
Adopted into the name of the part entry within the assembly. Supplier and ref Optional String - Free-form field. May include, for example, specific distributor information. + Free-form field. May, for example, contain distributor-specific information.
Adopted as a note for the part entry within the assembly. diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index c3bf96368..0383f964f 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -12939,7 +12939,7 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S
- + assembly.bom_import.template.kicad_pcbnew.table @@ -12958,37 +12958,37 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Id Opcional Entero - Campo libre. Un número de identificación único para cada componente. + Campo libre. Un número único de identificación para cada componente. - Designador + Designator Opcional - Cadena de texto - Campo libre. Un designador de referencia único para el componente en la PCB, p. ej., "R1" para la resistencia 1. Se utiliza para nombrar la colocación en el grupo de componentes. + Cadena + Campo libre. Un designador de referencia único para el componente en la PCB, por ejemplo, "R1" para la resistencia 1.
Se utiliza como el nombre de ubicación en la entrada de componentes dentro del ensamblaje. Package Opcional - Cadena de texto - Campo libre. El formato o tipo de encapsulado del componente, p. ej., "0805" para resistencias SMD. + Cadena + Campo libre. El encapsulado o formato del componente, por ejemplo, "0805" para resistencias SMD.
No se incluye en la entrada del componente dentro del ensamblaje. - Cantidad - Obligatorio + Quantity + Requerido Entero - El número de componentes idénticos necesarios para crear una instancia única de un ensamblaje. + El número de componentes idénticos necesarios para crear una instancia del ensamblaje.
Se usa como la cantidad en la entrada de componentes dentro del ensamblaje. - Designación - Obligatorio - Cadena de texto - La descripción o función del componente, p. ej., el valor de la resistencia "10kΩ" o el valor del condensador "100nF". Se utiliza para el nombre en la entrada del BOM. + Designation + Requerido + Cadena + Descripción o función del componente, por ejemplo, valor de resistencia "10kΩ" o valor de condensador "100nF".
Se usa como el nombre de la entrada del componente dentro del ensamblaje. - Proveedor y referencia + Supplier and ref Opcional - Cadena de texto - Campo libre. Puede incluir, por ejemplo, información específica del distribuidor. + Cadena + Campo libre. Puede contener, por ejemplo, información específica del distribuidor.
Se usa como una nota en la entrada del componente dentro del ensamblaje. diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 5362f9390..320121909 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -9692,19 +9692,19 @@ exemple de ville
- + assembly.bom_import.template.kicad_pcbnew.table - - Champ - Condition - Type de Données - Description - + + Champ + Condition + Type de Données + Description + @@ -9714,34 +9714,34 @@ exemple de ville Champ libre. Un numéro d'identification unique pour chaque composant. - Designeur + Designator Optionnel Chaîne - Champ libre. Une référence de désignation unique du composant sur le PCB, par exemple, "R1" pour la résistance 1. Utilisé pour nommer la position au sein du groupe de composants. + Champ libre. Une désignation de référence unique pour le composant sur le PCB, par exemple, "R1" pour la résistance 1.
Utilisé comme le nom de l'emplacement de l'entrée composant dans l'assemblage. - Boîtier + Package Optionnel Chaîne - Champ libre. Le type ou format d'encapsulation du composant, par exemple, "0805" pour des résistances CMS. + Champ libre. Le boîtier ou le format du composant, par exemple, "0805" pour les résistances CMS.
Non inclus dans l'entrée composant pour l'assemblage. - Quantité + Quantity Obligatoire Entier - Le nombre de composants identiques nécessaires pour créer une instance unique d'un ensemble. + Le nombre de composants identiques nécessaires pour créer une instance de l'assemblage.
Utilisé comme la quantité dans l'entrée composant de l'assemblage. - Désignation + Designation Obligatoire Chaîne - La description ou la fonction du composant, par exemple, la valeur de résistance "10kΩ" ou la valeur de condensateur "100nF". Utilisé comme nom dans l'entrée de la nomenclature (BOM). + La description ou la fonction du composant, par exemple, valeur de résistance "10kΩ" ou valeur de condensateur "100nF".
Utilisé comme le nom dans l'entrée composant pour l'assemblage. - Fournisseur et réf + Supplier and ref Optionnel Chaîne - Champ libre. Peut inclure, par exemple, des informations spécifiques au distributeur. + Champ libre. Peut contenir par exemple des informations spécifiques sur le fournisseur.
Utilisé comme une note dans l'entrée composant pour l'assemblage. diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 0ea57d9f1..f6e3c9446 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -12941,56 +12941,56 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a
- + assembly.bom_import.template.kicad_pcbnew.table - - Campo - Condizione - Tipo di Dati - Descrizione - + + Campo + Condizione + Tipo di Dati + Descrizione + Id Opzionale Intero - Campo libero. Un numero identificativo unico per ogni componente. + Campo libero. Un numero identificativo univoco per ogni componente. - Designatore + Designator Opzionale Stringa - Campo libero. Un designatore di riferimento unico per il componente sul PCB, ad esempio, "R1" per il resistore 1. Utilizzato per nominare la posizione nel gruppo di componenti. + Campo libero. Un riferimento univoco al componente su PCB, ad esempio "R1" per il resistore 1.
Utilizzato come nome della posizione nella voce componenti all'interno dell'assemblaggio. Package Opzionale Stringa - Campo libero. Il tipo o formato del contenitore del componente, ad esempio, "0805" per le resistenze SMD. + Campo libero. L'involucro o il fattore di forma del componente, ad esempio "0805" per i resistori SMD.
Non incluso nelle informazioni del componente nell'assemblaggio. - Quantità + Quantity Obbligatorio Intero - Il numero di componenti identici necessari per creare una singola unità di assemblaggio. + Il numero di componenti identici richiesti per creare un'istanza dell'assemblaggio.
Utilizzato come quantità nella voce componenti dell'assemblaggio. - Designazione + Designation Obbligatorio Stringa - La descrizione o la funzione del componente, ad esempio, il valore della resistenza "10kΩ" o il valore del condensatore "100nF". Utilizzato per il nome nell'entrata della lista dei materiali (BOM). + Descrizione o funzione del componente, ad esempio valore resistore "10kΩ" o valore condensatore "100nF".
Utilizzato come nome nella voce componenti dell'assemblaggio. - Fornitore e riferimento + Supplier and ref Opzionale Stringa - Campo libero. Può includere, ad esempio, informazioni specifiche del distributore. + Campo libero. Può contenere, ad esempio, informazioni specifiche del fornitore.
Utilizzato come nota nelle informazioni del componente nell'assemblaggio. diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 5de5f83ef..a8f5f9151 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -9393,56 +9393,56 @@ Exampletown
- + assembly.bom_import.template.kicad_pcbnew.table - - フィールド - 条件 - データタイプ - 説明 - + + フィールド + 条件 + データ型 + 説明 + - ID + Id 任意 整数 - 自由形式フィールド。各コンポーネントの一意の識別番号。 + 自由形式のフィールド。各コンポーネントのユニークな識別番号。 - デジネータ + Designator 任意 文字列 - 自由形式フィールド。PCB上のコンポーネントの一意の参照デジネータ(例: 抵抗1の「R1」)。コンポーネントグループ内の配置の命名に使用されます。 + 自由形式のフィールド。PCB上のコンポーネントごとの一意のリファレンス識別子。例: 抵抗 "R1"。
アセンブリ内の部品エントリの配置名として使用。 - パッケージ + Package 任意 文字列 - 自由形式フィールド。コンポーネントのケースまたはフォームファクタ(例: SMD抵抗「0805」)。 + 自由形式のフィールド。コンポーネントのパッケージまたはフォームファクタ。例: 表面実装抵抗器 "0805"。
アセンブリ内の部品エントリ情報には含まれません。 - 数量 + Quantity 必須 整数 - アセンブリの単一インスタンスを作成するために必要な同一コンポーネントの数。 + アセンブリの一つのインスタンスを作るために必要な同一部品の数。
アセンブリの部品情報で数量として使用。 - 指定 + Designation 必須 文字列 - コンポーネントの説明または機能(例: 抵抗値「10kΩ」やコンデンサ値「100nF」)。部品表(BOM)エントリ内の名前として使用されます。 + コンポーネントの説明や機能。例: 抵抗の値 "10kΩ" やコンデンサの値 "100nF"。
アセンブリの部品情報で名称として使用。 - サプライヤーと参照 + Supplier and ref 任意 文字列 - 自由形式フィールド。たとえば、特定のディストリビューター情報を含む場合があります。 + 自由形式フィールド。例: 供給元に関する特定の情報を含む場合がある。
アセンブリの部品情報の注記として使用。 diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index b7392b4d7..f98e9c784 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1355,19 +1355,19 @@
- + assembly.bom_import.template.kicad_pcbnew.table - - Veld - Voorwaarde - Gegevenstype - Beschrijving - + + Veld + Voorwaarde + Gegevenstype + Beschrijving + @@ -1377,34 +1377,34 @@ Vrij veld. Een unieke identificatienummer voor elk onderdeel. - Ontwerper + Designator Optioneel - Tekst - Vrij veld. Een unieke referentie-ontwerper voor het onderdeel op de PCB, bijvoorbeeld "R1" voor weerstand 1. Gebruikt voor de naamgeving van de plaatsing in de componentgroep. + String + Vrij veld. Een unieke referentienaam voor het onderdeel op de PCB, bijvoorbeeld "R1" voor weerstand 1.
Wordt gebruikt als positioneringsnaam in de onderdelenlijst van de assemblage. - Omhulsel + Package Optioneel - Tekst - Vrij veld. Het type of de vormfactor van het onderdeel, bijvoorbeeld "0805" voor SMD-weerstanden. + String + Vrij veld. De behuizing of vormfactor van het onderdeel, bijvoorbeeld "0805" voor SMD-weerstanden.
Wordt niet opgenomen in de onderdelenlijst binnen de assemblage. - Aantal - Verplicht + Quantity + Vereist Integer - Het aantal identieke onderdelen dat nodig is om een enkele instantie van een assemblage te maken. + Het aantal identieke onderdelen dat nodig is om een assemblage-instantie te creëren.
Wordt gebruikt als hoeveelheid in de onderdelenlijst binnen de assemblage. - Aanduiding - Verplicht - Tekst - De beschrijving of functie van het onderdeel, bijvoorbeeld de weerstandswaarde "10kΩ" of de condensatorwaarde "100nF". Wordt gebruikt als naam in de BOM-invoer. + Designation + Vereist + String + De beschrijving of functie van het onderdeel, zoals weerstandwaarde "10kΩ" of condensatorwaarde "100nF".
Wordt gebruikt als de naam in de onderdelenlijst binnen de assemblage. - Leverancier en referentie + Supplier and ref Optioneel - Tekst - Vrij veld. Kan bijvoorbeeld informatie bevatten die specifiek is voor de distributeur. + String + Vrij veld. Kan bijvoorbeeld specifieke informatie over leveranciers bevatten.
Wordt gebruikt als notitie in de onderdelenlijst binnen de assemblage. diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 7290e5fe1..d1a095072 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -12818,19 +12818,19 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli
- + assembly.bom_import.template.kicad_pcbnew.table - - Pole - Warunek - Typ Danych - Opis - + + Pole + Warunek + Typ Danych + Opis + @@ -12840,34 +12840,34 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Pole dowolne. Unikalny numer identyfikacyjny dla każdego komponentu. - Designer + Designator Opcjonalne - Łańcuch znaków - Pole dowolne. Unikalny oznacznik referencyjny komponentu na PCB, np. "R1" dla rezystora 1. Używany do nazewnictwa położenia w grupie komponentów. + Tekst + Pole dowolne. Jednoznaczny znacznik referencyjny komponentu na PCB, np. "R1" dla rezystora 1.
Używane jako nazwa pozycji w pozycji komponentu w montażu. - Obudowa + Package Opcjonalne - Łańcuch znaków - Pole dowolne. Typ lub forma obudowy komponentu, np. "0805" dla rezystorów SMD. + Tekst + Pole dowolne. Obudowa lub forma komponentu, np. "0805" dla rezystorów SMD.
Niewykorzystywane w pozycji komponentu w montażu. - Ilość + Quantity Wymagane Liczba całkowita - Liczba identycznych komponentów potrzebnych do stworzenia jednej instancji złożenia. + Liczba identycznych komponentów potrzebna do utworzenia jednej instancji montażu.
Używane jako ilość w pozycji komponentu w montażu. - Oznaczenie + Designation Wymagane - Łańcuch znaków - Opis lub funkcja komponentu, np. wartość rezystora "10kΩ" lub wartość kondensatora "100nF". Używane jako nazwa w pozycji na liście materiałowej (BOM). + Tekst + Opis lub funkcja komponentu, np. wartość rezystora "10kΩ" lub wartość kondensatora "100nF".
Używane jako nazwa w pozycji komponentu w montażu. - Dostawca i referencja + Supplier and ref Opcjonalne - Łańcuch znaków - Pole dowolne. Może zawierać, np. informacje specyficzne dla dystrybutora. + Tekst + Pole dowolne. Może zawierać np. specyficzne informacje o dostawcy.
Używane jako notatka w pozycji komponentu w montażu. diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 540b9e355..429a82eca 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -12918,56 +12918,56 @@
- + assembly.bom_import.template.kicad_pcbnew.table - - Поле - Условие - Тип данных - Описание - + + Поле + Условие + Тип данных + Описание + - ID + Id Опционально - Целое число - Произвольное поле. Уникальный идентификационный номер для каждого компонента. + Целое + Свободное поле. Уникальный идентификационный номер для каждого компонента. - Дизигнатор - Опционально + Package + Designator Строка - Произвольное поле. Уникальный референсный обозначитель компонента на печатной плате, например, "R1" для резистора 1. Используется для именования позиции в группе компонентов. + Свободное поле. Уникальная ссылочная метка компонента на печатной плате, например, "R1" для резистора 1.
Используется как наименование позиции в компоненте сборки. - Корпус + Package Опционально Строка - Произвольное поле. Тип или форм-фактор корпуса компонента, например, "0805" для SMD-резисторов. + Свободное поле. Тип корпуса или форм-фактор компонента, например, "0805" для SMD-резисторов.
Не включается в информацию о компоненте сборки. - Количество + Quantity Обязательно - Целое число - Количество одинаковых компонентов, необходимое для создания одной единицы сборки. + Целое + Количество одинаковых компонентов, необходимых для создания одной версии сборки.
Используется как количество в информации о компоненте сборки. - Обозначение + Designation Обязательно Строка - Описание или функция компонента, например, номинал резистора "10kΩ" или номинал конденсатора "100nF". Используется в качестве имени в позиции списка материалов (BOM). + Описание или функция компонента, например, значение резистора "10kΩ" или значение конденсатора "100nF".
Используется как наименование в информации о компоненте сборки. - Поставщик и ссылка + Supplier and ref Опционально Строка - Произвольное поле. Может содержать, например, информацию, специфичную для дистрибьютора. + Свободное поле. Может содержать, например, информацию о конкретных поставщиках.
Используется как примечание в информации о компоненте сборки. diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index bf924444c..7e90daa28 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -12803,56 +12803,56 @@ Element 3
- + assembly.bom_import.template.kicad_pcbnew.table - - 字段 - 条件 - 数据类型 - 描述 - + + 字段 + 条件 + 数据类型 + 描述 + - ID + Id 可选 整数 - 自由格式字段。每个组件的唯一标识号。 + 自由字段。每个组件的唯一标识号。 - 设计ator + Designator 可选 字符串 - 自由格式字段。PCB上组件的唯一参考标识符,例如电阻1的"R1"。用于命名组件组中的位置。 + 自由字段。PCB上组件的唯一参考标识符,例如电阻 "R1"。
用于装配部件条目中的位置名称。 - 封装 + Package 可选 字符串 - 自由格式字段。组件的封装类型或形式因子,例如对于SMD电阻"0805"。 + 自由字段。组件的封装类型或外形规格,例如表面贴装电阻器 "0805"。
不包含在装配部件条目信息中。 - 数量 + Quantity 必填 整数 - 创建一个组装实例所需的相同组件的数量。 + 创建一个装配实例所需的相同部件的数量。
用于装配部件条目中的数量。 - 描述 + Designation 必填 字符串 - 组件的描述或功能,例如电阻值"10kΩ"或电容值"100nF"。在物料清单(BOM)条目中用作名称。 + 组件的描述或功能,例如电阻的值 "10kΩ" 或电容的值 "100nF"。
用于装配部件条目中的名称。 - 供应商和参考 + 供应商及参考 可选 字符串 - 自由格式字段。例如,可以包含特定分销商的信息。 + 自由字段。例如,可以包含有关供应商的特定信息。
用作装配部件信息中的备注。 From 228f6e52ae4feb91825a81bd7b8b742d438a1d53 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Fri, 4 Apr 2025 10:50:47 +0200 Subject: [PATCH 06/83] =?UTF-8?q?Assembly=20Konfiguration=20in=20BOM=20aus?= =?UTF-8?q?blenden,=20wenn=20bisher=20keine=20Zuordnung=20zu=20mindestens?= =?UTF-8?q?=20einem=20Eintrag=20stattgefunden=20hat=20(Ber=C3=BCcksichtigu?= =?UTF-8?q?ng=20Rechtekonfiguration)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Twig/AssemblyTwigExtension.php | 26 +++++++++++++++++++ .../form/collection_types_layout.html.twig | 15 ++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 src/Twig/AssemblyTwigExtension.php diff --git a/src/Twig/AssemblyTwigExtension.php b/src/Twig/AssemblyTwigExtension.php new file mode 100644 index 000000000..3430f7d1d --- /dev/null +++ b/src/Twig/AssemblyTwigExtension.php @@ -0,0 +1,26 @@ +getAssembly() !== null) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/templates/form/collection_types_layout.html.twig b/templates/form/collection_types_layout.html.twig index 0175aeaf6..552fd5421 100644 --- a/templates/form/collection_types_layout.html.twig +++ b/templates/form/collection_types_layout.html.twig @@ -44,9 +44,18 @@ {{ form_row(form.part) }} {{ form_errors(form.part) }} -
- {{ form_widget(form.assembly) }} - {{ form_errors(form.assembly) }} + + {% if form.vars.value is not null and form.vars.value.project is not null %} + {% set hasAssembly = false %} + {% if is_granted("@assemblies.read") or has_assembly(form.vars.value.project.bomEntries.toArray) %} +
+ {{ form_widget(form.assembly) }} + {{ form_errors(form.assembly) }} + {% endif %} + {% elseif is_granted("@assemblies.read") %} + {{ form_widget(form.assembly) }} + {{ form_errors(form.assembly) }} + {% endif %} {{ form_widget(form.name) }} From d5389acb5aa920efa83638d0287af45dc225aeac Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Fri, 11 Apr 2025 12:07:55 +0200 Subject: [PATCH 07/83] PartController -> new Methode Variablennamen korrigieren --- src/Controller/PartController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 9e7e61b03..246dbbb22 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -185,7 +185,7 @@ public function new(Request $requ } elseif ($assembly instanceof Assembly) { //Initialize a new part for a build part from the given assembly //Ensure that the assembly has not already a build part - if ($project->getBuildPart() instanceof Part) { + if ($assembly->getBuildPart() instanceof Part) { $this->addFlash('error', 'part.new_build_part.error.build_part_already_exists'); return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]); } From 4653065e1703e9a0bcacf9e07330baa29981b6de Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Fri, 11 Apr 2025 14:00:11 +0200 Subject: [PATCH 08/83] =?UTF-8?q?JSON=20Importer=20mit=20Minimaldaten=20we?= =?UTF-8?q?iterentwickeln.=20Validierung=20mit=20Violations=20einf=C3=BChr?= =?UTF-8?q?en=20und=20beim=20Import-Versuch=20zus=C3=A4tzlich=20mit=20ausg?= =?UTF-8?q?eben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Controller/AssemblyController.php | 28 +- .../ImportExportSystem/BOMImporter.php | 307 ++++++++++++++---- .../ImportExportSystem/ImporterResult.php | 60 ++++ templates/assemblies/import_bom.html.twig | 25 +- translations/validators.cs.xlf | 54 +++ translations/validators.da.xlf | 54 +++ translations/validators.de.xlf | 56 +++- translations/validators.el.xlf | 54 +++ translations/validators.en.xlf | 54 +++ translations/validators.fr.xlf | 56 +++- translations/validators.hr.xlf | 54 +++ translations/validators.it.xlf | 54 +++ translations/validators.ja.xlf | 54 +++ translations/validators.pl.xlf | 54 +++ translations/validators.ru.xlf | 54 +++ translations/validators.zh.xlf | 54 +++ 16 files changed, 978 insertions(+), 94 deletions(-) create mode 100644 src/Services/ImportExportSystem/ImporterResult.php diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php index 9710e9bec..54cc1abb0 100644 --- a/src/Controller/AssemblyController.php +++ b/src/Controller/AssemblyController.php @@ -29,7 +29,6 @@ use App\Form\AssemblySystem\AssemblyAddPartsType; use App\Form\AssemblySystem\AssemblyBuildType; use App\Helpers\Assemblies\AssemblyBuildRequest; -use App\Repository\PartRepository; use App\Services\ImportExportSystem\BOMImporter; use App\Services\AssemblySystem\AssemblyBuildHelper; use Doctrine\Common\Collections\ArrayCollection; @@ -52,14 +51,10 @@ #[Route(path: '/assembly')] class AssemblyController extends AbstractController { - private PartRepository $partRepository; - public function __construct( private readonly DataTableFactory $dataTableFactory, - private readonly EntityManagerInterface $entityManager, private readonly TranslatorInterface $translator, ) { - $this->partRepository = $this->entityManager->getRepository(Part::class); } #[Route(path: '/{id}/info', name: 'assembly_info', requirements: ['id' => '\d+'])] @@ -161,15 +156,14 @@ public function importBOM(Request $request, EntityManagerInterface $entityManage $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - - //Clear existing BOM entries if requested + // Clear existing entries if requested if ($form->get('clear_existing_bom')->getData()) { $assembly->getBomEntries()->clear(); $entityManager->flush(); } try { - $entries = $BOMImporter->importFileIntoAssembly($form->get('file')->getData(), $assembly, [ + $importerResult = $BOMImporter->importFileIntoAssembly($form->get('file')->getData(), $assembly, [ 'type' => $form->get('type')->getData(), ]); @@ -177,24 +171,17 @@ public function importBOM(Request $request, EntityManagerInterface $entityManage $errors = $validator->validateProperty($assembly, 'bom_entries'); //If no validation errors occured, save the changes and redirect to edit page - if (count ($errors) === 0) { - foreach ($entries as $entry) { - if ($entry instanceof AssemblyBOMEntry && $entry->getPart() !== null) { - $part = $entry->getPart(); - if ($part->getID() === null) { - $this->partRepository->save($part); - } - } - } + if (count ($errors) === 0 && $importerResult->getViolations()->count() === 0) { + $entries = $importerResult->getBomEntries(); $this->addFlash('success', t('assembly.bom_import.flash.success', ['%count%' => count($entries)])); $entityManager->flush(); + return $this->redirectToRoute('assembly_edit', ['id' => $assembly->getID()]); } - //When we get here, there were validation errors + //Show validation errors $this->addFlash('error', t('assembly.bom_import.flash.invalid_entries')); - } catch (\UnexpectedValueException|\RuntimeException|SyntaxError $e) { $this->addFlash('error', t('assembly.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()])); } @@ -226,7 +213,8 @@ public function importBOM(Request $request, EntityManagerInterface $entityManage 'assembly' => $assembly, 'jsonTemplate' => $jsonTemplate, 'form' => $form, - 'errors' => $errors ?? null, + 'validationErrors' => $errors ?? null, + 'importerErrors' => isset($importerResult) ? $importerResult->getViolations() : null, ]); } diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index fd32f0657..e76726233 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -39,8 +39,9 @@ use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\OptionsResolver\OptionsResolver; -use RuntimeException; +use Symfony\Contracts\Translation\TranslatorInterface; use UnexpectedValueException; +use Symfony\Component\Validator\ConstraintViolation; /** * @see \App\Tests\Services\ImportExportSystem\BOMImporterTest @@ -57,6 +58,8 @@ class BOMImporter 5 => 'Supplier and ref', ]; + private string $jsonRoot = ''; + public function __construct( private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, @@ -64,7 +67,8 @@ public function __construct( private readonly PartRepository $partRepository, private readonly ManufacturerRepository $manufacturerRepository, private readonly CategoryRepository $categoryRepository, - private readonly DBElementRepository $assemblyBOMEntryRepository + private readonly DBElementRepository $assemblyBOMEntryRepositor, + private readonly TranslatorInterface $translator ) { } @@ -102,20 +106,21 @@ public function importFileIntoProject(File $file, Project $project, array $optio } /** - * Converts the given file into an array of BOM entries using the given options and save them into the given assembly. + * Converts the given file into an ImporterResult with an array of BOM entries using the given options and save them into the given assembly. * The changes are not saved into the database yet. - * @return AssemblyBOMEntry[] */ - public function importFileIntoAssembly(File $file, Assembly $assembly, array $options): array + public function importFileIntoAssembly(File $file, Assembly $assembly, array $options): ImporterResult { - $bomEntries = $this->fileToBOMEntries($file, $options, AssemblyBOMEntry::class); + $importerResult = $this->fileToImporterResult($file, $options, AssemblyBOMEntry::class); - //Assign the bom_entries to the assembly - foreach ($bomEntries as $bom_entry) { - $assembly->addBomEntry($bom_entry); + if ($importerResult->getViolations()->count() === 0) { + //Assign the bom_entries to the assembly + foreach ($importerResult->getBomEntries() as $bomEntry) { + $assembly->addBomEntry($bomEntry); + } } - return $bomEntries; + return $importerResult; } /** @@ -127,6 +132,14 @@ public function fileToBOMEntries(File $file, array $options, string $objectType return $this->stringToBOMEntries($file->getContent(), $options, $objectType); } + /** + * Converts the given file into an ImporterResult with an array of BOM entries using the given options. + */ + public function fileToImporterResult(File $file, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult + { + return $this->stringToImporterResult($file->getContent(), $options, $objectType); + } + /** * Validate BOM data before importing * @return array Validation result with errors, warnings, and info @@ -163,14 +176,33 @@ public function stringToBOMEntries(string $data, array $options, string $objectT }; } - private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntry::class): array + /** + * Import string data into an array of BOM entries, which are not yet assigned to a project. + * @param string $data The data to import + * @param array $options An array of options + * @return ProjectBOMEntry[]|AssemblyBOMEntry[] An array of imported entries + */ + public function stringToImporterResult(string $data, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult { + $resolver = new OptionsResolver(); + $resolver = $this->configureOptions($resolver); + $options = $resolver->resolve($options); + + return match ($options['type']) { + 'kicad_pcbnew' => $this->parseKiCADPCB($data, $objectType), + 'json' => $this->parseJson($data, $options, $objectType), + default => throw new InvalidArgumentException('Invalid import type!'), + }; + } + + private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntry::class): ImporterResult + { + $result = new ImporterResult(); + $csv = Reader::createFromString($data); $csv->setDelimiter(';'); $csv->setHeaderOffset(0); - $bom_entries = []; - foreach ($csv->getRecords() as $offset => $entry) { //Translate the german field names to english $entry = $this->normalizeColumnNames($entry); @@ -200,10 +232,10 @@ private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntr $bom_entry->setComment($entry['Supplier and ref'] ?? ''); $bom_entry->setQuantity((float) ($entry['Quantity'] ?? 1)); - $bom_entries[] = $bom_entry; + $result->addBomEntry($bom_entry); } - return $bom_entries; + return $result; } /** @@ -263,30 +295,47 @@ private function validateKiCADSchematicData(string $data, array $options): array return $this->validationService->validateBOMEntries($mapped_entries, $options); } - private function parseJson(string $data, array $options = [], string $objectType = ProjectBOMEntry::class): array + private function parseJson(string $data, array $options = [], string $objectType = ProjectBOMEntry::class): ImporterResult { - $result = []; + $result = new ImporterResult(); + $this->jsonRoot = 'JSON Import for '.$objectType === ProjectBOMEntry::class ? 'Project' : 'Assembly'; $data = json_decode($data, true); - foreach ($data as $entry) { + foreach ($data as $key => $entry) { // Check quantity if (!isset($entry['quantity'])) { - throw new UnexpectedValueException('quantity missing'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.quantity.required', + "entry[$key].quantity" + )); } - if (!is_float($entry['quantity']) || $entry['quantity'] <= 0) { - throw new UnexpectedValueException('quantity expected as float greater than 0.0'); + + if (isset($entry['quantity']) && (!is_float($entry['quantity']) || $entry['quantity'] <= 0)) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.quantity.float', + "entry[$key].quantity", + $entry['quantity'] + )); } // Check name if (isset($entry['name']) && !is_string($entry['name'])) { - throw new UnexpectedValueException('name of part list entry expected as string'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.string.notEmpty', + "entry[$key].name", + $entry['name'] + )); } // Check if part is assigned with relevant information if (isset($entry['part'])) { if (!is_array($entry['part'])) { - throw new UnexpectedValueException('The property "part" should be an array'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.array', + "entry[$key].part", + $entry['part'] + )); } $partIdValid = isset($entry['part']['id']) && is_int($entry['part']['id']) && $entry['part']['id'] > 0; @@ -295,9 +344,12 @@ private function parseJson(string $data, array $options = [], string $objectType $partIpnValid = isset($entry['part']['ipn']) && is_string($entry['part']['ipn']) && trim($entry['part']['ipn']) !== ''; if (!$partIdValid && !$partNameValid && !$partMpnrValid && !$partIpnValid) { - throw new UnexpectedValueException( - 'The property "part" must have either assigned: "id" as integer greater than 0, "name", "mpnr", or "ipn" as non-empty string' - ); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.subproperties', + "entry[$key].part", + $entry['part'], + ['%propertyString%' => '"id", "name", "mpnr", or "ipn"'] + )); } $part = $partIdValid ? $this->partRepository->findOneBy(['id' => $entry['part']['id']]) : null; @@ -306,28 +358,71 @@ private function parseJson(string $data, array $options = [], string $objectType $part = $part ?? ($partNameValid ? $this->partRepository->findOneBy(['name' => trim($entry['part']['name'])]) : null); if ($part === null) { - $part = new Part(); - $part->setName($entry['part']['name']); + $value = sprintf('part.id: %s, part.mpnr: %s, part.ipn: %s, part.name: %s', + isset($entry['part']['id']) ? '' . $entry['part']['id'] . '' : '-', + isset($entry['part']['mpnr']) ? '' . $entry['part']['mpnr'] . '' : '-', + isset($entry['part']['ipn']) ? '' . $entry['part']['ipn'] . '' : '-', + isset($entry['part']['name']) ? '' . $entry['part']['name'] . '' : '-', + ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.notFoundFor', + "entry[$key].part", + $entry['part'], + ['%value%' => $value] + )); } - if ($partNameValid && $part->getName() !== trim($entry['part']['name'])) { - throw new RuntimeException(sprintf('Part name does not match exact the given name. Given for import: %s, found part: %s', $entry['part']['name'], $part->getName())); + if ($partNameValid && $part !== null && isset($entry['part']['name']) && $part->getName() !== trim($entry['part']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.name", + $entry['part']['name'], + [ + '%importValue%' => '' . $entry['part']['name'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getName() . '' + ] + )); } - if ($partIpnValid && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) { - throw new RuntimeException(sprintf('Part mpnr does not match exact the given mpnr. Given for import: %s, found part: %s', $entry['part']['mpnr'], $part->getManufacturerProductNumber())); + if ($partMpnrValid && $part !== null && isset($entry['part']['mpnr']) && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.mpnr", + $entry['part']['mpnr'], + [ + '%importValue%' => '' . $entry['part']['mpnr'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getManufacturerProductNumber() . '' + ] + )); } - if ($partIpnValid && $part->getIpn() !== trim($entry['part']['ipn'])) { - throw new RuntimeException(sprintf('Part ipn does not match exact the given ipn. Given for import: %s, found part: %s', $entry['part']['ipn'], $part->getIpn())); + if ($partIpnValid && $part !== null && isset($entry['part']['ipn']) && $part->getIpn() !== trim($entry['part']['ipn'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.ipn", + $entry['part']['ipn'], + [ + '%importValue%' => '' . $entry['part']['ipn'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getIpn() . '' + ] + )); } // Part: Description check - if (isset($entry['part']['description']) && !is_null($entry['part']['description'])) { + if (isset($entry['part']['description'])) { if (!is_string($entry['part']['description']) || trim($entry['part']['description']) === '') { - throw new UnexpectedValueException('The property path "part.description" must be a non-empty string if not null'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.string.notEmpty', + 'entry[$key].part.description', + $entry['part']['description'] + )); } } + $partDescription = $entry['part']['description'] ?? ''; // Part: Manufacturer check @@ -335,7 +430,11 @@ private function parseJson(string $data, array $options = [], string $objectType $manufacturerNameValid = false; if (array_key_exists('manufacturer', $entry['part'])) { if (!is_array($entry['part']['manufacturer'])) { - throw new UnexpectedValueException('The property path "part.manufacturer" must be an array'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.array', + 'entry[$key].part.manufacturer', + $entry['part']['manufacturer']) ?? null + ); } $manufacturerIdValid = isset($entry['part']['manufacturer']['id']) && is_int($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] > 0; @@ -343,23 +442,43 @@ private function parseJson(string $data, array $options = [], string $objectType // Stellen sicher, dass mindestens eine Bedingung für manufacturer erfüllt sein muss if (!$manufacturerIdValid && !$manufacturerNameValid) { - throw new UnexpectedValueException( - 'The property "manufacturer" must have either assigned: "id" as integer greater than 0, or "name" as non-empty string' - ); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties', + "entry[$key].part.manufacturer", + $entry['part']['manufacturer'], + )); } } $manufacturer = $manufacturerIdValid ? $this->manufacturerRepository->findOneBy(['id' => $entry['part']['manufacturer']['id']]) : null; $manufacturer = $manufacturer ?? ($manufacturerNameValid ? $this->manufacturerRepository->findOneBy(['name' => trim($entry['part']['manufacturer']['name'])]) : null); - if ($manufacturer === null) { - throw new RuntimeException( - 'Manufacturer not found' + if (($manufacturerIdValid || $manufacturerNameValid) && $manufacturer === null) { + $value = sprintf( + 'manufacturer.id: %s, manufacturer.name: %s', + isset($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] !== null ? '' . $entry['part']['manufacturer']['id'] . '' : '-', + isset($entry['part']['manufacturer']['name']) && $entry['part']['manufacturer']['name'] !== null ? '' . $entry['part']['manufacturer']['name'] . '' : '-' ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.notFoundFor', + "entry[$key].part.manufacturer", + $entry['part']['manufacturer'], + ['%value%' => $value] + )); } - if ($manufacturerNameValid && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) { - throw new RuntimeException(sprintf('Manufacturer name does not match exact the given name. Given for import: %s, found manufacturer: %s', $entry['manufacturer']['name'], $manufacturer->getName())); + if ($manufacturerNameValid && $manufacturer !== null && isset($entry['part']['manufacturer']['name']) && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.manufacturer.name", + $entry['part']['manufacturer']['name'], + [ + '%importValue%' => '' . $entry['part']['manufacturer']['name'] . '', + '%foundId%' => $manufacturer->getID(), + '%foundValue%' => '' . $manufacturer->getName() . '' + ] + )); } // Part: Category check @@ -367,49 +486,82 @@ private function parseJson(string $data, array $options = [], string $objectType $categoryNameValid = false; if (array_key_exists('category', $entry['part'])) { if (!is_array($entry['part']['category'])) { - throw new UnexpectedValueException('part.category must be an array'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.array', + 'entry[$key].part.category', + $entry['part']['category']) ?? null + ); } $categoryIdValid = isset($entry['part']['category']['id']) && is_int($entry['part']['category']['id']) && $entry['part']['category']['id'] > 0; $categoryNameValid = isset($entry['part']['category']['name']) && is_string($entry['part']['category']['name']) && trim($entry['part']['category']['name']) !== ''; if (!$categoryIdValid && !$categoryNameValid) { - throw new UnexpectedValueException( - 'The property "category" must have either assigned: "id" as integer greater than 0, or "name" as non-empty string' - ); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties', + "entry[$key].part.category", + $entry['part']['category'] + )); } } $category = $categoryIdValid ? $this->categoryRepository->findOneBy(['id' => $entry['part']['category']['id']]) : null; $category = $category ?? ($categoryNameValid ? $this->categoryRepository->findOneBy(['name' => trim($entry['part']['category']['name'])]) : null); - if ($category === null) { - throw new RuntimeException( - 'Category not found' + if (($categoryIdValid || $categoryNameValid) && $category === null) { + $value = sprintf( + 'category.id: %s, category.name: %s', + isset($entry['part']['category']['id']) && $entry['part']['category']['id'] !== null ? '' . $entry['part']['category']['id'] . '' : '-', + isset($entry['part']['category']['name']) && $entry['part']['category']['name'] !== null ? '' . $entry['part']['category']['name'] . '' : '-' ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.notFoundFor', + "entry[$key].part.category", + $entry['part']['category'], + ['%value%' => $value] + )); } - if ($categoryNameValid && $category->getName() !== trim($entry['part']['category']['name'])) { - throw new RuntimeException(sprintf('Category name does not match exact the given name. Given for import: %s, found category: %s', $entry['category']['name'], $category->getName())); + if ($categoryNameValid && $category !== null && isset($entry['part']['category']['name']) && $category->getName() !== trim($entry['part']['category']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.category.name", + $entry['part']['category']['name'], + [ + '%importValue%' => '' . $entry['part']['category']['name'] . '', + '%foundId%' => $category->getID(), + '%foundValue%' => '' . $category->getName() . '' + ] + )); } - $part->setDescription($partDescription); - $part->setManufacturer($manufacturer); - $part->setCategory($category); + if ($result->getViolations()->count() > 0) { + continue; + } - if ($partMpnrValid) { - $part->setManufacturerProductNumber($entry['part']['mpnr'] ?? ''); + if ($partDescription !== '') { + //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe die Beschreibung des Bauteils mit übernehmen. + $part->setDescription($partDescription); } - if ($partIpnValid) { - $part->setIpn($entry['part']['ipn'] ?? ''); + + if ($manufacturer !== null && $manufacturer->getID() !== $part->getManufacturerID()) { + //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe des Hersteller des Bauteils mit übernehmen. + $part->setManufacturer($manufacturer); + } + + if ($category !== null && $category->getID() !== $part->getCategoryID()) { + //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe die Kategorie des Bauteils mit übernehmen. + $part->setCategory($category); } if ($objectType === AssemblyBOMEntry::class) { $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['part' => $part]); if ($bomEntry === null) { - $name = isset($entry['name']) && $entry['name'] !== null ? trim($entry['name']) : ''; - $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $name]); + if (isset($entry['name']) && $entry['name'] !== '') { + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $entry['name']]); + } if ($bomEntry === null) { $bomEntry = new AssemblyBOMEntry(); @@ -423,9 +575,22 @@ private function parseJson(string $data, array $options = [], string $objectType $bomEntry->setName($entry['name'] ?? ''); $bomEntry->setPart($part); - } - $result[] = $bomEntry; + $result->addBomEntry($bomEntry); + } else { + //Eintrag ohne Part-Relation in die Bauteilliste aufnehmen + + if ($objectType === AssemblyBOMEntry::class) { + $bomEntry = new AssemblyBOMEntry(); + } else { + $bomEntry = new ProjectBOMEntry(); + } + + $bomEntry->setQuantity($entry['quantity']); + $bomEntry->setName($entry['name'] ?? ''); + + $result->addBomEntry($bomEntry); + } } return $result; @@ -931,4 +1096,16 @@ public function detectFields(string $data, ?string $delimiter = null): array return array_values($headers); } + + private function buildJsonViolation(string $message, string $propertyPath, mixed $invalidValue = null, array $parameters = []): ConstraintViolation + { + return new ConstraintViolation( + message: $this->translator->trans($message, $parameters, 'validators'), + messageTemplate: $message, + parameters: $parameters, + root: $this->jsonRoot, + propertyPath: $propertyPath, + invalidValue: $invalidValue + ); + } } diff --git a/src/Services/ImportExportSystem/ImporterResult.php b/src/Services/ImportExportSystem/ImporterResult.php new file mode 100644 index 000000000..4e289d133 --- /dev/null +++ b/src/Services/ImportExportSystem/ImporterResult.php @@ -0,0 +1,60 @@ +bomEntries = $bomEntries; + $this->violations = new ConstraintViolationList(); + } + + /** + * Fügt einen neuen BOM-Eintrag hinzu. + */ + public function addBomEntry(object $bomEntry): void + { + $this->bomEntries[] = $bomEntry; + } + + /** + * Gibt alle BOM-Einträge zurück. + */ + public function getBomEntries(): array + { + return $this->bomEntries; + } + + /** + * Gibt die Liste der Violation zurück. + */ + public function getViolations(): ConstraintViolationList + { + return $this->violations; + } + + /** + * Fügt eine neue `ConstraintViolation` zur Liste hinzu. + */ + public function addViolation(ConstraintViolation $violation): void + { + $this->violations->add($violation); + } + + /** + * Prüft, ob die Liste der Violationen leer ist. + */ + public function hasViolations(): bool + { + return count($this->violations) > 0; + } +} \ No newline at end of file diff --git a/templates/assemblies/import_bom.html.twig b/templates/assemblies/import_bom.html.twig index dc9430422..04ff328a3 100644 --- a/templates/assemblies/import_bom.html.twig +++ b/templates/assemblies/import_bom.html.twig @@ -3,16 +3,27 @@ {% block title %}{% trans %}assembly.import_bom{% endtrans %}{% endblock %} {% block before_card %} - {% if errors %} + {% if validationErrors or importerErrors %}

{% trans %}parts.import.errors.title{% endtrans %}

    - {% for violation in errors %} -
  • - {{ violation.propertyPath }}: - {{ violation.message|trans(violation.parameters, 'validators') }} -
  • - {% endfor %} + {% if validationErrors %} + {% for violation in validationErrors %} +
  • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators') }} +
  • + {% endfor %} + {% endif %} + + {% if importerErrors %} + {% for violation in importerErrors %} +
  • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators')|raw }} +
  • + {% endfor %} + {% endif %}
{% endif %} diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 06354533e..46471d02d 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -389,5 +389,59 @@ Musíte vybrat součást nebo nastavit název pro nesoučást!
+ + + validator.bom_importer.json.quantity.required + Musíte zadat množství > 0! + + + + + validator.bom_importer.json.quantity.float + očekává se jako float větší než 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + očekává se jako neprázdný řetězec + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + očekává se jako neprázdný řetězec nebo null + + + + + validator.bom_importer.json.parameter.array + očekává se jako pole (array) + + + + + validator.bom_importer.json.parameter.subproperties + musí mít alespoň jeden z následujících pod-parametrů: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + nenalezeno pro %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + se přesně neshoduje. Pro import zadáno: %importValue%, nalezeno (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + musí obsahovat jako pod-parametr buď: "id" jako celé číslo větší než 0 nebo "name" jako neprázdný řetězec + + diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 9a9dea4cd..88ec6784b 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -365,5 +365,59 @@ Du skal vælge en del eller sætte et navn for en ikke-del!
+ + + validator.bom_importer.json.quantity.required + Du skal angive en mængde > 0! + + + + + validator.bom_importer.json.quantity.float + forventet som en float større end 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + forventet som en ikke-tom streng + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + forventet som en ikke-tom streng eller null + + + + + validator.bom_importer.json.parameter.array + forventet som en array + + + + + validator.bom_importer.json.parameter.subproperties + skal have mindst én af følgende underparametre: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + ikke fundet for %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + stemmer ikke helt overens. Givet til import: %importValue%, fundet (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + skal indeholde som en underparameter enten: "id" som et heltal større end 0 eller "name" som en ikke-tom streng + + diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 6fde3419a..cb710ac97 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -386,7 +386,61 @@ validator.assembly.bom_entry.name_or_part_needed - Sie müssen ein Bauteil auswählen, oder einen Namen für ein nicht-Bauteil setzen! + Sie müssen ein Bauteil auswählen, oder einen Namen für den Eintrag setzen! + + + + + validator.bom_importer.json.quantity.required + Sie müssen eine Stückzahl > 0 angeben! + + + + + validator.bom_importer.json.quantity.float + wird als float größer als 0.0 erwartet + + + + + validator.bom_importer.json.parameter.string.notEmpty + als nicht leere Zeichenkette erwartet + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + als nicht leere Zeichenkette oder null erwartet + + + + + validator.bom_importer.json.parameter.array + als array erwartet + + + + + validator.bom_importer.json.parameter.subproperties + muss mindestens eines der folgenden Unter-Parameter haben: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + nicht gefunden für %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + stimmt nicht genau überein. Für den Import gegeben: %importValue%, gefunden (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + muss entweder als Unter-Parameter zugewiesen haben: "id" als Ganzzahl größer als 0 oder "name" als nicht leere Zeichenfolge diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index ee27863c0..318ba8928 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -31,5 +31,59 @@ Πρέπει να επιλέξετε ένα εξάρτημα ή να βάλετε ένα όνομα για ένα μη εξάρτημα!
+ + + validator.bom_importer.json.quantity.required + Πρέπει να εισαγάγετε ποσότητα > 0! + + + + + validator.bom_importer.json.quantity.float + αναμένεται ως δεκαδικός αριθμός (float) μεγαλύτερος από 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + αναμένεται ως μη κενή συμβολοσειρά + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + αναμένεται ως μη κενή συμβολοσειρά ή null + + + + + validator.bom_importer.json.parameter.array + αναμένεται ως array + + + + + validator.bom_importer.json.parameter.subproperties + πρέπει να έχει τουλάχιστον μία από τις ακόλουθες υπο-παραμέτρους: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + δεν βρέθηκε για %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + δεν ταιριάζει απόλυτα. Δόθηκε για εισαγωγή: %importValue%, βρέθηκε (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + πρέπει να περιέχει ως υπο-παράμετρο είτε: "id" ως ακέραιο αριθμό μεγαλύτερο από 0 είτε "name" ως μη κενή συμβολοσειρά + + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 86525b6a8..93640fff7 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -389,5 +389,59 @@ __validator.assembly.bom_entry.name_or_part_needed
+ + + validator.bom_importer.json.quantity.required + you must specify a quantity > 0! + + + + + validator.bom_importer.json.quantity.float + expected as float greater than 0.0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + expected as non-empty string + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + als nicht leere Zeichenkette oder null erwartet + + + + + validator.bom_importer.json.parameter.array + expectd as array + + + + + validator.bom_importer.json.parameter.subproperties + must have at least one of the following sub-properties: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + not found for %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + does not match exactly. Given for import: %importValue%, found (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + must have either assigned as sub-property: "id" as an integer greater than 0, or "name" as a non-empty string + + diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index e9bf32597..cde3672f6 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -224,7 +224,61 @@ validator.assembly.bom_entry.name_or_part_needed - Vous devez sélectionner une pièce ou attribuer un nom pour un non-élément ! + Vous devez sélectionner une pièce ou attribuer un nom pour un non-élément! + + + + + validator.bom_importer.json.quantity.required + Vous devez entrer une quantité > 0 ! + + + + + validator.bom_importer.json.quantity.float + attendu comme un nombre décimal (float) supérieur à 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + attendu comme une chaîne de caractères non vide + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + attendu comme une chaîne de caractères non vide ou null + + + + + validator.bom_importer.json.parameter.array + attendu comme un tableau (array) + + + + + validator.bom_importer.json.parameter.subproperties + doit contenir au moins l'un des sous-paramètres suivants : %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + non trouvé pour %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + ne correspond pas exactement. Donné pour l'importation : %importValue%, trouvé (%foundId%) : %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + doit contenir comme sous-paramètre soit : "id" comme entier supérieur à 0 ou "name" comme chaîne de caractères non vide diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 9c9c3960c..988724201 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -383,5 +383,59 @@ Morate odabrati dio ili unijeti naziv za nedio!
+ + + validator.bom_importer.json.quantity.required + Morate unijeti količinu > 0! + + + + + validator.bom_importer.json.quantity.float + očekuje se decimalni broj (float) veći od 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + očekuje se kao neprazan niz znakova + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + očekuje se kao neprazan niz znakova ili null + + + + + validator.bom_importer.json.parameter.array + očekuje se kao niz + + + + + validator.bom_importer.json.parameter.subproperties + mora sadržavati barem jedan od sljedećih pod-parametara: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + nije pronađeno za %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + ne podudara se točno. Uneseno za uvoz: %importValue%, pronađeno (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + mora sadržavati kao pod-parametar bilo: "id" kao cijeli broj veći od 0 ili "name" kao neprazan niz znakova + + diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index 2f747bc5c..63ca86d77 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -383,5 +383,59 @@ È necessario selezionare una parte o inserire un nome per un non-parte!
+ + + validator.bom_importer.json.quantity.required + Devi inserire una quantità > 0! + + + + + validator.bom_importer.json.quantity.float + atteso come numero decimale (float) maggiore di 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + atteso come stringa non vuota + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + atteso come stringa non vuota o null + + + + + validator.bom_importer.json.parameter.array + atteso come array + + + + + validator.bom_importer.json.parameter.subproperties + deve avere almeno uno dei seguenti sotto-parametri: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + non trovato per %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + non corrisponde esattamente. Valore dato per l'importazione: %importValue%, trovato (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + deve contenere come sotto-parametro: "id" come intero maggiore di 0 o "name" come stringa non vuota + + diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index 0156ffefc..c0b541173 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -227,5 +227,59 @@ 部品を選択するか、非部品の名前を入力する必要があります!
+ + + validator.bom_importer.json.quantity.required + 数量 > 0 を入力する必要があります! + + + + + validator.bom_importer.json.quantity.float + 0.0 より大きい小数 (float) である必要があります + + + + + validator.bom_importer.json.parameter.string.notEmpty + 空でない文字列が期待されます + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + 空でない文字列または null が期待されます + + + + + validator.bom_importer.json.parameter.array + 配列として期待されます + + + + + validator.bom_importer.json.parameter.subproperties + 以下のサブパラメーターのいずれかを含む必要があります:%propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + %value% に対する項目が見つかりません + + + + + validator.bom_importer.json.parameter.noExactMatch + 完全には一致しません。インポートされた値:%importValue%、見つかった値 (%foundId%):%foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + サブパラメーターとして次のいずれかを含む必要があります:"id" は 0 より大きい整数、または "name" は空でない文字列 + + diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 2cc4aef43..d417757dd 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -383,5 +383,59 @@ Musisz wybrać element lub przypisać nazwę dla elementu niestandardowego!
+ + + validator.bom_importer.json.quantity.required + Musisz wprowadzić ilość > 0! + + + + + validator.bom_importer.json.quantity.float + oczekiwano liczby zmiennoprzecinkowej (float) większej od 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + oczekiwano jako niepusty ciąg znaków + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + oczekiwano jako niepusty ciąg znaków lub null + + + + + validator.bom_importer.json.parameter.array + oczekiwano jako tablicę + + + + + validator.bom_importer.json.parameter.subproperties + musi zawierać co najmniej jeden z następujących podparametrów: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + nie znaleziono dla %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + brak dokładnego dopasowania. Wprowadzone do importu: %importValue%, znalezione (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + musi zawierać jako podparametr: "id" jako liczbę całkowitą większą od 0 lub "name" jako niepusty ciąg znaków + + diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 4049b453e..625aea240 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -383,5 +383,59 @@ Необходимо выбрать деталь или ввести название для недетали!
+ + + validator.bom_importer.json.quantity.required + Необходимо указать количество > 0! + + + + + validator.bom_importer.json.quantity.float + ожидается число с плавающей запятой (float), большее 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + ожидается непустая строка + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + ожидается непустая строка или null + + + + + validator.bom_importer.json.parameter.array + ожидается массив + + + + + validator.bom_importer.json.parameter.subproperties + должен содержать хотя бы один из следующих под-параметров: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + не найдено для %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + точное совпадение отсутствует. Указано для импорта: %importValue%, найдено (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + должен содержать под-параметр: "id" как целое число больше 0 или "name" как непустая строка + + diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 3eab5c4e3..5093ce989 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -371,5 +371,59 @@ 必须选择零件或为非零件指定名称!
+ + + validator.bom_importer.json.quantity.required + 必须输入数量 > 0! + + + + + validator.bom_importer.json.quantity.float + 应为大于 0.0 的浮点数 (float) + + + + + validator.bom_importer.json.parameter.string.notEmpty + 应为非空字符串 + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + 应为非空字符串或 null + + + + + validator.bom_importer.json.parameter.array + 应为数组 + + + + + validator.bom_importer.json.parameter.subproperties + 必须包含以下子参数之一:%propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + 未找到对应值 %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + 未精确匹配。用于导入的值:%importValue%,找到的值 (%foundId%):%foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + 必须包含子参数:"id" 为大于 0 的整数,或 "name" 为非空字符串 + + From 725c73980db2bb6b1e399a8296360f0c5cb584ea Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 16 Apr 2025 10:21:44 +0200 Subject: [PATCH 09/83] =?UTF-8?q?Anpassungen=20zu=20JSON=20Importer=20vorn?= =?UTF-8?q?ehmen.=20CSV=20Importer=20implementieren.=20=C3=9Cbersetzungsar?= =?UTF-8?q?beiten=20vornehmen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Controller/AssemblyController.php | 1 + .../ImportExportSystem/BOMImporter.php | 726 ++++++++++++------ templates/assemblies/import_bom.html.twig | 42 + translations/messages.cs.xlf | 187 ++++- translations/messages.da.xlf | 217 +++++- translations/messages.de.xlf | 163 +++- translations/messages.el.xlf | 189 ++++- translations/messages.en.xlf | 185 ++++- translations/messages.es.xlf | 193 ++++- translations/messages.fr.xlf | 307 ++++++-- translations/messages.it.xlf | 305 ++++++-- translations/messages.ja.xlf | 313 +++++--- translations/messages.nl.xlf | 317 +++++--- translations/messages.pl.xlf | 323 +++++--- translations/messages.ru.xlf | 343 ++++++--- translations/messages.zh.xlf | 195 ++++- translations/validators.cs.xlf | 48 +- translations/validators.da.xlf | 48 +- translations/validators.de.xlf | 48 +- translations/validators.el.xlf | 48 +- translations/validators.en.xlf | 48 +- translations/validators.fr.xlf | 48 +- translations/validators.hr.xlf | 48 +- translations/validators.it.xlf | 48 +- translations/validators.ja.xlf | 48 +- translations/validators.pl.xlf | 48 +- translations/validators.ru.xlf | 48 +- translations/validators.zh.xlf | 48 +- 28 files changed, 3410 insertions(+), 1172 deletions(-) diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php index 54cc1abb0..9106f6775 100644 --- a/src/Controller/AssemblyController.php +++ b/src/Controller/AssemblyController.php @@ -139,6 +139,7 @@ public function importBOM(Request $request, EntityManagerInterface $entityManage 'required' => true, 'choices' => [ 'assembly.bom_import.type.json' => 'json', + 'assembly.bom_import.type.csv' => 'csv', 'assembly.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew', ] ]); diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index e76726233..2f6236afc 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -38,6 +38,7 @@ use League\Csv\Reader; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\Translation\TranslatorInterface; use UnexpectedValueException; @@ -48,6 +49,9 @@ */ class BOMImporter { + private const IMPORT_TYPE_JSON = 'json'; + private const IMPORT_TYPE_CSV = 'csv'; + private const IMPORT_TYPE_KICAD_PCB = 'kicad_pcbnew'; private const MAP_KICAD_PCB_FIELDS = [ 0 => 'Id', @@ -67,6 +71,7 @@ public function __construct( private readonly PartRepository $partRepository, private readonly ManufacturerRepository $manufacturerRepository, private readonly CategoryRepository $categoryRepository, + private readonly DBElementRepository $projectBOMEntryRepository, private readonly DBElementRepository $assemblyBOMEntryRepositor, private readonly TranslatorInterface $translator ) { @@ -109,7 +114,7 @@ public function importFileIntoProject(File $file, Project $project, array $optio * Converts the given file into an ImporterResult with an array of BOM entries using the given options and save them into the given assembly. * The changes are not saved into the database yet. */ - public function importFileIntoAssembly(File $file, Assembly $assembly, array $options): ImporterResult + public function importFileIntoAssembly(UploadedFile $file, Assembly $assembly, array $options): ImporterResult { $importerResult = $this->fileToImporterResult($file, $options, AssemblyBOMEntry::class); @@ -135,8 +140,44 @@ public function fileToBOMEntries(File $file, array $options, string $objectType /** * Converts the given file into an ImporterResult with an array of BOM entries using the given options. */ - public function fileToImporterResult(File $file, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult + public function fileToImporterResult(UploadedFile $file, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult { + $result = new ImporterResult(); + + //Available file endings depending on the import type + $validExtensions = match ($options['type']) { + self::IMPORT_TYPE_KICAD_PCB => ['kicad_pcb'], + self::IMPORT_TYPE_JSON => ['json'], + self::IMPORT_TYPE_CSV => ['csv'], + default => [], + }; + + //Get the file extension of the uploaded file + $fileExtension = pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION); + + //Check whether the file extension is valid + if ($validExtensions === []) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.invalid_import_type', + 'import.type' + )); + + return $result; + } else if (!in_array(strtolower($fileExtension), $validExtensions, true)) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.invalid_file_extension', + 'file.extension', + $fileExtension, + [ + '%extension%' => $fileExtension, + '%importType%' => $this->translator->trans('assembly.bom_import.type.'.$options['type']), + '%allowedExtensions%' => implode(', ', $validExtensions), + ] + )); + + return $result; + } + return $this->stringToImporterResult($file->getContent(), $options, $objectType); } @@ -180,7 +221,7 @@ public function stringToBOMEntries(string $data, array $options, string $objectT * Import string data into an array of BOM entries, which are not yet assigned to a project. * @param string $data The data to import * @param array $options An array of options - * @return ProjectBOMEntry[]|AssemblyBOMEntry[] An array of imported entries + * @return ImporterResult An result of imported entries or a violation list */ public function stringToImporterResult(string $data, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult { @@ -188,10 +229,17 @@ public function stringToImporterResult(string $data, array $options, string $obj $resolver = $this->configureOptions($resolver); $options = $resolver->resolve($options); + $defaultImporterResult = new ImporterResult(); + $defaultImporterResult->addViolation($this->buildJsonViolation( + 'validator.bom_importer.invalid_import_type', + 'import.type' + )); + return match ($options['type']) { - 'kicad_pcbnew' => $this->parseKiCADPCB($data, $objectType), - 'json' => $this->parseJson($data, $options, $objectType), - default => throw new InvalidArgumentException('Invalid import type!'), + self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $objectType), + self::IMPORT_TYPE_JSON => $this->parseJson($data, $objectType), + self::IMPORT_TYPE_CSV => $this->parseCsv($data, $objectType), + default => $defaultImporterResult, }; } @@ -228,7 +276,7 @@ private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntr $bom_entry->setName($entry['Designation']); } - $bom_entry->setMountnames($entry['Designator'] ?? ''); + $bom_entry->setMountnames($entry['Designator']); $bom_entry->setComment($entry['Supplier and ref'] ?? ''); $bom_entry->setQuantity((float) ($entry['Quantity'] ?? 1)); @@ -295,7 +343,37 @@ private function validateKiCADSchematicData(string $data, array $options): array return $this->validationService->validateBOMEntries($mapped_entries, $options); } - private function parseJson(string $data, array $options = [], string $objectType = ProjectBOMEntry::class): ImporterResult + /** + * Parses the given JSON data into an ImporterResult while validating and transforming entries according to the + * specified options and object type. If violations are encountered during parsing, they are added to the result. + * + * The structure of each entry in the JSON data is validated to ensure that required fields (e.g., quantity, and name) + * are present, and optional composite fields, like `part` and its sub-properties, meet specific criteria. Various + * conditions are checked, including whether the provided values are the correct types, and if relationships (like + * matching parts or manufacturers) are resolved successfully. + * + * Violations are added for: + * - Missing or invalid `quantity` values. + * - Non-string `name` values. + * - Invalid structure or missing sub-properties in `part`. + * - Incorrect or unresolved references to parts and their information, such as `id`, `name`, `manufacturer_product_number` + * (mpnr), `internal_part_number` (ipn), or `description`. + * - Inconsistent or absent manufacturer information. + * + * If a match for a part or manufacturer cannot be resolved, a violation is added alongside an indication of the + * imported value and any partially matched information. Warnings for no exact matches are also added for parts + * using specific identifying properties like name, manufacturer product number, or internal part numbers. + * + * Additional validations include: + * - Checking for empty or invalid descriptions. + * - Ensuring manufacturers, if specified, have valid `name` or `id` values. + * + * @param string $data JSON encoded string containing BOM entries data. + * @param string $objectType The type of entries expected during import (e.g., `ProjectBOMEntry` or `AssemblyBOMEntry`). + * + * @return ImporterResult The result containing parsed data and any violations encountered during the parsing process. + */ + private function parseJson(string $data, string $objectType = ProjectBOMEntry::class): ImporterResult { $result = new ImporterResult(); $this->jsonRoot = 'JSON Import for '.$objectType === ProjectBOMEntry::class ? 'Project' : 'Assembly'; @@ -303,297 +381,434 @@ private function parseJson(string $data, array $options = [], string $objectType $data = json_decode($data, true); foreach ($data as $key => $entry) { - // Check quantity if (!isset($entry['quantity'])) { $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.quantity.required', + 'validator.bom_importer.json_csv.quantity.required', "entry[$key].quantity" )); } if (isset($entry['quantity']) && (!is_float($entry['quantity']) || $entry['quantity'] <= 0)) { $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.quantity.float', + 'validator.bom_importer.json_csv.quantity.float', "entry[$key].quantity", $entry['quantity'] )); } - // Check name if (isset($entry['name']) && !is_string($entry['name'])) { $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.string.notEmpty', + 'validator.bom_importer.json_csv.parameter.string.notEmpty', "entry[$key].name", $entry['name'] )); } - // Check if part is assigned with relevant information if (isset($entry['part'])) { - if (!is_array($entry['part'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.array', - "entry[$key].part", - $entry['part'] - )); - } + $this->processPart($entry, $result, $key, $objectType,self::IMPORT_TYPE_JSON); + } else { + $bomEntry = $this->getOrCreateBomEntry($objectType, $entry['name'] ?? null); + $bomEntry->setQuantity((float) $entry['quantity']); - $partIdValid = isset($entry['part']['id']) && is_int($entry['part']['id']) && $entry['part']['id'] > 0; - $partNameValid = isset($entry['part']['name']) && is_string($entry['part']['name']) && trim($entry['part']['name']) !== ''; - $partMpnrValid = isset($entry['part']['mpnr']) && is_string($entry['part']['mpnr']) && trim($entry['part']['mpnr']) !== ''; - $partIpnValid = isset($entry['part']['ipn']) && is_string($entry['part']['ipn']) && trim($entry['part']['ipn']) !== ''; - - if (!$partIdValid && !$partNameValid && !$partMpnrValid && !$partIpnValid) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.subproperties', - "entry[$key].part", - $entry['part'], - ['%propertyString%' => '"id", "name", "mpnr", or "ipn"'] - )); - } + $result->addBomEntry($bomEntry); + } + } - $part = $partIdValid ? $this->partRepository->findOneBy(['id' => $entry['part']['id']]) : null; - $part = $part ?? ($partMpnrValid ? $this->partRepository->findOneBy(['manufacturer_product_number' => trim($entry['part']['mpnr'])]) : null); - $part = $part ?? ($partIpnValid ? $this->partRepository->findOneBy(['ipn' => trim($entry['part']['ipn'])]) : null); - $part = $part ?? ($partNameValid ? $this->partRepository->findOneBy(['name' => trim($entry['part']['name'])]) : null); - - if ($part === null) { - $value = sprintf('part.id: %s, part.mpnr: %s, part.ipn: %s, part.name: %s', - isset($entry['part']['id']) ? '' . $entry['part']['id'] . '' : '-', - isset($entry['part']['mpnr']) ? '' . $entry['part']['mpnr'] . '' : '-', - isset($entry['part']['ipn']) ? '' . $entry['part']['ipn'] . '' : '-', - isset($entry['part']['name']) ? '' . $entry['part']['name'] . '' : '-', - ); - - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.notFoundFor', - "entry[$key].part", - $entry['part'], - ['%value%' => $value] - )); - } + return $result; + } - if ($partNameValid && $part !== null && isset($entry['part']['name']) && $part->getName() !== trim($entry['part']['name'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.noExactMatch', - "entry[$key].part.name", - $entry['part']['name'], - [ - '%importValue%' => '' . $entry['part']['name'] . '', - '%foundId%' => $part->getID(), - '%foundValue%' => '' . $part->getName() . '' - ] - )); - } - if ($partMpnrValid && $part !== null && isset($entry['part']['mpnr']) && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.noExactMatch', - "entry[$key].part.mpnr", - $entry['part']['mpnr'], - [ - '%importValue%' => '' . $entry['part']['mpnr'] . '', - '%foundId%' => $part->getID(), - '%foundValue%' => '' . $part->getManufacturerProductNumber() . '' - ] - )); - } + /** + * Parses a CSV string and processes its rows into hierarchical data structures, + * performing validations and converting data based on the provided headers. + * Handles potential violations and manages the creation of BOM entries based on the given type. + * + * @param string $csvData The raw CSV data to parse, with rows separated by newlines. + * @param string $objectType The class type to instantiate for BOM entries, defaults to ProjectBOMEntry. + * + * @return ImporterResult Returns an ImporterResult instance containing BOM entries and any validation violations encountered. + */ + function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): ImporterResult + { + $result = new ImporterResult(); + $rows = explode("\n", trim($csvData)); + $headers = str_getcsv(array_shift($rows), ';'); - if ($partIpnValid && $part !== null && isset($entry['part']['ipn']) && $part->getIpn() !== trim($entry['part']['ipn'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.noExactMatch', - "entry[$key].part.ipn", - $entry['part']['ipn'], - [ - '%importValue%' => '' . $entry['part']['ipn'] . '', - '%foundId%' => $part->getID(), - '%foundValue%' => '' . $part->getIpn() . '' - ] - )); - } + foreach ($rows as $key => $row) { + $entry = []; + $values = str_getcsv($row, ';'); - // Part: Description check - if (isset($entry['part']['description'])) { - if (!is_string($entry['part']['description']) || trim($entry['part']['description']) === '') { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.string.notEmpty', - 'entry[$key].part.description', - $entry['part']['description'] - )); - } - } + foreach ($headers as $index => $column) { + //Convert column name into hierarchy + $path = explode('_', $column); + $temp = &$entry; - $partDescription = $entry['part']['description'] ?? ''; - - // Part: Manufacturer check - $manufacturerIdValid = false; - $manufacturerNameValid = false; - if (array_key_exists('manufacturer', $entry['part'])) { - if (!is_array($entry['part']['manufacturer'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.array', - 'entry[$key].part.manufacturer', - $entry['part']['manufacturer']) ?? null - ); + foreach ($path as $step) { + if (!isset($temp[$step])) { + $temp[$step] = []; } - $manufacturerIdValid = isset($entry['part']['manufacturer']['id']) && is_int($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] > 0; - $manufacturerNameValid = isset($entry['part']['manufacturer']['name']) && is_string($entry['part']['manufacturer']['name']) && trim($entry['part']['manufacturer']['name']) !== ''; + $temp = &$temp[$step]; + } - // Stellen sicher, dass mindestens eine Bedingung für manufacturer erfüllt sein muss - if (!$manufacturerIdValid && !$manufacturerNameValid) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties', - "entry[$key].part.manufacturer", - $entry['part']['manufacturer'], - )); + //If there is no value, skip + if (isset($values[$index]) && $values[$index] !== '') { + //Check whether the value is numerical + if (is_numeric($values[$index])) { + //Convert to integer or float + $temp = (strpos($values[$index], '.') !== false) + ? floatval($values[$index]) + : intval($values[$index]); + } else { + //Leave other data types untouched + $temp = $values[$index]; } } + } - $manufacturer = $manufacturerIdValid ? $this->manufacturerRepository->findOneBy(['id' => $entry['part']['manufacturer']['id']]) : null; - $manufacturer = $manufacturer ?? ($manufacturerNameValid ? $this->manufacturerRepository->findOneBy(['name' => trim($entry['part']['manufacturer']['name'])]) : null); - - if (($manufacturerIdValid || $manufacturerNameValid) && $manufacturer === null) { - $value = sprintf( - 'manufacturer.id: %s, manufacturer.name: %s', - isset($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] !== null ? '' . $entry['part']['manufacturer']['id'] . '' : '-', - isset($entry['part']['manufacturer']['name']) && $entry['part']['manufacturer']['name'] !== null ? '' . $entry['part']['manufacturer']['name'] . '' : '-' - ); - - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.notFoundFor', - "entry[$key].part.manufacturer", - $entry['part']['manufacturer'], - ['%value%' => $value] - )); - } + $entry = $this->removeEmptyProperties($entry); - if ($manufacturerNameValid && $manufacturer !== null && isset($entry['part']['manufacturer']['name']) && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.noExactMatch', - "entry[$key].part.manufacturer.name", - $entry['part']['manufacturer']['name'], - [ - '%importValue%' => '' . $entry['part']['manufacturer']['name'] . '', - '%foundId%' => $manufacturer->getID(), - '%foundValue%' => '' . $manufacturer->getName() . '' - ] - )); - } + if (!isset($entry['quantity'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.csv.quantity.required', + "row[$key].quantity" + )); + } - // Part: Category check - $categoryIdValid = false; - $categoryNameValid = false; - if (array_key_exists('category', $entry['part'])) { - if (!is_array($entry['part']['category'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.array', - 'entry[$key].part.category', - $entry['part']['category']) ?? null - ); - } + if (isset($entry['quantity']) && (!is_numeric($entry['quantity']) || $entry['quantity'] <= 0)) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.csv.quantity.float', + "row[$key].quantity", + $entry['quantity'] + )); + } - $categoryIdValid = isset($entry['part']['category']['id']) && is_int($entry['part']['category']['id']) && $entry['part']['category']['id'] > 0; - $categoryNameValid = isset($entry['part']['category']['name']) && is_string($entry['part']['category']['name']) && trim($entry['part']['category']['name']) !== ''; + if (isset($entry['name']) && !is_string($entry['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.csv.parameter.string.notEmpty', + "row[$key].name", + $entry['name'] + )); + } - if (!$categoryIdValid && !$categoryNameValid) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties', - "entry[$key].part.category", - $entry['part']['category'] - )); - } - } + if (isset($entry['part'])) { + $this->processPart($entry, $result, $key, $objectType, self::IMPORT_TYPE_CSV); + } else { + $bomEntry = $this->getOrCreateBomEntry($objectType, $entry['name'] ?? null); + $bomEntry->setQuantity((float) $entry['quantity'] ?? 0); - $category = $categoryIdValid ? $this->categoryRepository->findOneBy(['id' => $entry['part']['category']['id']]) : null; - $category = $category ?? ($categoryNameValid ? $this->categoryRepository->findOneBy(['name' => trim($entry['part']['category']['name'])]) : null); - - if (($categoryIdValid || $categoryNameValid) && $category === null) { - $value = sprintf( - 'category.id: %s, category.name: %s', - isset($entry['part']['category']['id']) && $entry['part']['category']['id'] !== null ? '' . $entry['part']['category']['id'] . '' : '-', - isset($entry['part']['category']['name']) && $entry['part']['category']['name'] !== null ? '' . $entry['part']['category']['name'] . '' : '-' - ); - - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.notFoundFor', - "entry[$key].part.category", - $entry['part']['category'], - ['%value%' => $value] - )); - } + $result->addBomEntry($bomEntry); + } + } - if ($categoryNameValid && $category !== null && isset($entry['part']['category']['name']) && $category->getName() !== trim($entry['part']['category']['name'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.noExactMatch', - "entry[$key].part.category.name", - $entry['part']['category']['name'], - [ - '%importValue%' => '' . $entry['part']['category']['name'] . '', - '%foundId%' => $category->getID(), - '%foundValue%' => '' . $category->getName() . '' - ] - )); - } + return $result; + } - if ($result->getViolations()->count() > 0) { - continue; - } + /** + * Processes an individual part entry in the import data. + * + * This method validates the structure and content of the provided part entry and uses the findings + * to identify corresponding objects in the database. The result is recorded, and violations are + * logged if issues or discrepancies exist in the validation or database matching process. + * + * @param array $entry The array representation of the part entry. + * @param ImporterResult $result The result object used for recording validation violations. + * @param int $key The index of the entry in the data array. + * @param string $objectType The type of object being processed. + * @param string $importType The type of import being performed. + * + * @return void + */ + private function processPart(array $entry, ImporterResult $result, int $key, string $objectType, string $importType): void + { + $prefix = $importType === self::IMPORT_TYPE_JSON ? 'entry' : 'row'; + + if (!is_array($entry['part'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.array', + $prefix."[$key].part", + $entry['part'] + )); + } - if ($partDescription !== '') { - //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe die Beschreibung des Bauteils mit übernehmen. - $part->setDescription($partDescription); - } + $partIdValid = isset($entry['part']['id']) && is_int($entry['part']['id']) && $entry['part']['id'] > 0; + $partMpnrValid = isset($entry['part']['mpnr']) && is_string($entry['part']['mpnr']) && trim($entry['part']['mpnr']) !== ''; + $partIpnValid = isset($entry['part']['ipn']) && is_string($entry['part']['ipn']) && trim($entry['part']['ipn']) !== ''; + $partNameValid = isset($entry['part']['name']) && is_string($entry['part']['name']) && trim($entry['part']['name']) !== ''; + + if (!$partIdValid && !$partNameValid && !$partMpnrValid && !$partIpnValid) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.subproperties', + $prefix."[$key].part", + $entry['part'], + ['%propertyString%' => '"id", "name", "mpnr", or "ipn"'] + )); + } - if ($manufacturer !== null && $manufacturer->getID() !== $part->getManufacturerID()) { - //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe des Hersteller des Bauteils mit übernehmen. - $part->setManufacturer($manufacturer); - } + $part = $partIdValid ? $this->partRepository->findOneBy(['id' => $entry['part']['id']]) : null; + $part = $part ?? ($partMpnrValid ? $this->partRepository->findOneBy(['manufacturer_product_number' => trim($entry['part']['mpnr'])]) : null); + $part = $part ?? ($partIpnValid ? $this->partRepository->findOneBy(['ipn' => trim($entry['part']['ipn'])]) : null); + $part = $part ?? ($partNameValid ? $this->partRepository->findOneBy(['name' => trim($entry['part']['name'])]) : null); + + if ($part === null) { + $value = sprintf('part.id: %s, part.mpnr: %s, part.ipn: %s, part.name: %s', + isset($entry['part']['id']) ? '' . $entry['part']['id'] . '' : '-', + isset($entry['part']['mpnr']) ? '' . $entry['part']['mpnr'] . '' : '-', + isset($entry['part']['ipn']) ? '' . $entry['part']['ipn'] . '' : '-', + isset($entry['part']['name']) ? '' . $entry['part']['name'] . '' : '-', + ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.notFoundFor', + $prefix."[$key].part", + $entry['part'], + ['%value%' => $value] + )); + } - if ($category !== null && $category->getID() !== $part->getCategoryID()) { - //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe die Kategorie des Bauteils mit übernehmen. - $part->setCategory($category); - } + if ($partNameValid && $part !== null && isset($entry['part']['name']) && $part->getName() !== trim($entry['part']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.name", + $entry['part']['name'], + [ + '%importValue%' => '' . $entry['part']['name'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getName() . '' + ] + )); + } + + if ($partMpnrValid && $part !== null && isset($entry['part']['mpnr']) && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.mpnr", + $entry['part']['mpnr'], + [ + '%importValue%' => '' . $entry['part']['mpnr'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getManufacturerProductNumber() . '' + ] + )); + } - if ($objectType === AssemblyBOMEntry::class) { - $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['part' => $part]); + if ($partIpnValid && $part !== null && isset($entry['part']['ipn']) && $part->getIpn() !== trim($entry['part']['ipn'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.ipn", + $entry['part']['ipn'], + [ + '%importValue%' => '' . $entry['part']['ipn'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getIpn() . '' + ] + )); + } - if ($bomEntry === null) { - if (isset($entry['name']) && $entry['name'] !== '') { - $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $entry['name']]); - } + if (isset($entry['part']['description'])) { + if (!is_string($entry['part']['description']) || trim($entry['part']['description']) === '') { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.string.notEmpty', + 'entry[$key].part.description', + $entry['part']['description'] + )); + } + } - if ($bomEntry === null) { - $bomEntry = new AssemblyBOMEntry(); - } - } - } else { - $bomEntry = new ProjectBOMEntry(); - } + $partDescription = $entry['part']['description'] ?? ''; - $bomEntry->setQuantity($entry['quantity']); - $bomEntry->setName($entry['name'] ?? ''); + $manufacturerIdValid = false; + $manufacturerNameValid = false; + if (array_key_exists('manufacturer', $entry['part'])) { + if (!is_array($entry['part']['manufacturer'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.array', + 'entry[$key].part.manufacturer', + $entry['part']['manufacturer']) ?? null + ); + } - $bomEntry->setPart($part); + $manufacturerIdValid = isset($entry['part']['manufacturer']['id']) && is_int($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] > 0; + $manufacturerNameValid = isset($entry['part']['manufacturer']['name']) && is_string($entry['part']['manufacturer']['name']) && trim($entry['part']['manufacturer']['name']) !== ''; - $result->addBomEntry($bomEntry); - } else { - //Eintrag ohne Part-Relation in die Bauteilliste aufnehmen + if (!$manufacturerIdValid && !$manufacturerNameValid) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties', + $prefix."[$key].part.manufacturer", + $entry['part']['manufacturer'], + )); + } + } + + $manufacturer = $manufacturerIdValid ? $this->manufacturerRepository->findOneBy(['id' => $entry['part']['manufacturer']['id']]) : null; + $manufacturer = $manufacturer ?? ($manufacturerNameValid ? $this->manufacturerRepository->findOneBy(['name' => trim($entry['part']['manufacturer']['name'])]) : null); + + if (($manufacturerIdValid || $manufacturerNameValid) && $manufacturer === null) { + $value = sprintf( + 'manufacturer.id: %s, manufacturer.name: %s', + isset($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] !== null ? '' . $entry['part']['manufacturer']['id'] . '' : '-', + isset($entry['part']['manufacturer']['name']) && $entry['part']['manufacturer']['name'] !== null ? '' . $entry['part']['manufacturer']['name'] . '' : '-' + ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.notFoundFor', + $prefix."[$key].part.manufacturer", + $entry['part']['manufacturer'], + ['%value%' => $value] + )); + } + + if ($manufacturerNameValid && $manufacturer !== null && isset($entry['part']['manufacturer']['name']) && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.manufacturer.name", + $entry['part']['manufacturer']['name'], + [ + '%importValue%' => '' . $entry['part']['manufacturer']['name'] . '', + '%foundId%' => $manufacturer->getID(), + '%foundValue%' => '' . $manufacturer->getName() . '' + ] + )); + } + + $categoryIdValid = false; + $categoryNameValid = false; + if (array_key_exists('category', $entry['part'])) { + if (!is_array($entry['part']['category'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.array', + 'entry[$key].part.category', + $entry['part']['category']) ?? null + ); + } + + $categoryIdValid = isset($entry['part']['category']['id']) && is_int($entry['part']['category']['id']) && $entry['part']['category']['id'] > 0; + $categoryNameValid = isset($entry['part']['category']['name']) && is_string($entry['part']['category']['name']) && trim($entry['part']['category']['name']) !== ''; + + if (!$categoryIdValid && !$categoryNameValid) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties', + $prefix."[$key].part.category", + $entry['part']['category'] + )); + } + } + + $category = $categoryIdValid ? $this->categoryRepository->findOneBy(['id' => $entry['part']['category']['id']]) : null; + $category = $category ?? ($categoryNameValid ? $this->categoryRepository->findOneBy(['name' => trim($entry['part']['category']['name'])]) : null); + + if (($categoryIdValid || $categoryNameValid) && $category === null) { + $value = sprintf( + 'category.id: %s, category.name: %s', + isset($entry['part']['category']['id']) && $entry['part']['category']['id'] !== null ? '' . $entry['part']['category']['id'] . '' : '-', + isset($entry['part']['category']['name']) && $entry['part']['category']['name'] !== null ? '' . $entry['part']['category']['name'] . '' : '-' + ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.notFoundFor', + $prefix."[$key].part.category", + $entry['part']['category'], + ['%value%' => $value] + )); + } + + if ($categoryNameValid && $category !== null && isset($entry['part']['category']['name']) && $category->getName() !== trim($entry['part']['category']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.category.name", + $entry['part']['category']['name'], + [ + '%importValue%' => '' . $entry['part']['category']['name'] . '', + '%foundId%' => $category->getID(), + '%foundValue%' => '' . $category->getName() . '' + ] + )); + } + + if ($result->getViolations()->count() > 0) { + return; + } - if ($objectType === AssemblyBOMEntry::class) { + if ($partDescription !== '') { + //When updating the associated parts to a assembly, take over the description of the part. + $part->setDescription($partDescription); + } + + if ($manufacturer !== null && $manufacturer->getID() !== $part->getManufacturerID()) { + //When updating the associated parts, take over to a assembly of the manufacturer of the part. + $part->setManufacturer($manufacturer); + } + + if ($category !== null && $category->getID() !== $part->getCategoryID()) { + //When updating the associated parts to a assembly, take over the category of the part. + $part->setCategory($category); + } + + if ($objectType === AssemblyBOMEntry::class) { + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['part' => $part]); + + if ($bomEntry === null) { + if (isset($entry['name']) && $entry['name'] !== '') { + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $entry['name']]); + } + + if ($bomEntry === null) { $bomEntry = new AssemblyBOMEntry(); - } else { - $bomEntry = new ProjectBOMEntry(); } + } + } else { + $bomEntry = new ProjectBOMEntry(); + } - $bomEntry->setQuantity($entry['quantity']); - $bomEntry->setName($entry['name'] ?? ''); + $bomEntry->setQuantity((float) $entry['quantity']); + $bomEntry->setName($entry['name'] ?? ''); - $result->addBomEntry($bomEntry); + $bomEntry->setPart($part); + + $result->addBomEntry($bomEntry); + } + + private function removeEmptyProperties(array $data): array + { + foreach ($data as $key => &$value) { + //Recursive check when the value is an array + if (is_array($value)) { + $value = $this->removeEmptyProperties($value); + + //Remove the array when it is empty after cleaning + if (empty($value)) { + unset($data[$key]); + } + } elseif ($value === null || $value === '') { + //Remove values that are explicitly zero or empty + unset($data[$key]); } } - return $result; + return $data; + } + + private function getOrCreateBomEntry(string $objectType, ?string $name) + { + $bomEntry = null; + + //Check whether there is a name + if (!empty($name)) { + if ($objectType === ProjectBOMEntry::class) { + $bomEntry = $this->projectBOMEntryRepository->findOneBy(['name' => $name]); + } elseif ($objectType === AssemblyBOMEntry::class) { + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $name]); + } + } + + //If no bom enttry was found, a new object create + if ($bomEntry === null) { + $bomEntry = new $objectType(); + } + + $bomEntry->setName($name); + + return $bomEntry; } /** @@ -619,6 +834,21 @@ private function normalizeColumnNames(array $entry): array return $out; } + + /** + * Builds a JSON-based constraint violation. + * + * This method creates a `ConstraintViolation` object that represents a validation error. + * The violation includes a message, property path, invalid value, and other contextual information. + * Translations for the violation message can be applied through the translator service. + * + * @param string $message The translation key for the validation message. + * @param string $propertyPath The property path where the violation occurred. + * @param mixed|null $invalidValue The value that caused the violation (optional). + * @param array $parameters Additional parameters for message placeholders (default is an empty array). + * + * @return ConstraintViolation The created constraint violation object. + */ /** * Parse KiCad schematic BOM with flexible field mapping */ @@ -1097,6 +1327,20 @@ public function detectFields(string $data, ?string $delimiter = null): array return array_values($headers); } + /** + * Builds a JSON-based constraint violation. + * + * This method creates a `ConstraintViolation` object that represents a validation error. + * The violation includes a message, property path, invalid value, and other contextual information. + * Translations for the violation message can be applied through the translator service. + * + * @param string $message The translation key for the validation message. + * @param string $propertyPath The property path where the violation occurred. + * @param mixed|null $invalidValue The value that caused the violation (optional). + * @param array $parameters Additional parameters for message placeholders (default is an empty array). + * + * @return ConstraintViolation The created constraint violation object. + */ private function buildJsonViolation(string $message, string $propertyPath, mixed $invalidValue = null, array $parameters = []): ConstraintViolation { return new ConstraintViolation( diff --git a/templates/assemblies/import_bom.html.twig b/templates/assemblies/import_bom.html.twig index 04ff328a3..9e99c5417 100644 --- a/templates/assemblies/import_bom.html.twig +++ b/templates/assemblies/import_bom.html.twig @@ -53,6 +53,34 @@
+
+
+
+ {% trans %}assembly.import_bom.template.header.csv{% endtrans %} +
+
+ {{ 'assembly.bom_import.template.csv.exptected_columns'|trans }} + +
quantity;name;part_id;part_mpnr;part_ipn;part_name;part_description;part_manufacturer_id;part_manufacturer_name;part_category_id;part_category_name
+ +
    +
  • quantity
  • +
  • name
  • +
  • part_id
  • +
  • part_mpnr
  • +
  • part_ipn
  • +
  • part_name
  • +
  • part_description
  • +
  • part_manufacturer_id
  • +
  • part_manufacturer_name
  • +
  • part_category_id
  • +
  • part_category_name
  • +
+ + {{ 'assembly.bom_import.template.csv.table'|trans|raw }} +
+
+
@@ -61,6 +89,20 @@
{{ 'assembly.bom_import.template.kicad_pcbnew.exptected_columns'|trans }}
Id;Designator;Package;Quantity;Designation;Supplier and ref
+ +
    +
  • Id
  • +
  • Designator
  • +
  • Package
  • +
  • Quantity
  • +
  • Designation
  • +
  • Supplier and ref
  • +
  • Note
  • +
  • Footprint
  • +
  • Value
  • +
  • Footprint
  • +
+ {{ 'assembly.bom_import.template.kicad_pcbnew.exptected_columns.note'|trans|raw }} {{ 'assembly.bom_import.template.kicad_pcbnew.table'|trans|raw }} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index cbe8833ea..b0ec93321 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -13437,6 +13437,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz JSON pro sestavu + + + assembly.bom_import.type.csv + CSV pro sestavu + + assembly.bom_import.type.kicad_pcbnew @@ -13461,6 +13467,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Šablona importu JSON pro sestavu + + + assembly.import_bom.template.header.csv + Importní šablona CSV pro sestavu + + assembly.import_bom.template.header.kicad_pcbnew @@ -13520,24 +13532,26 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz quantity - Povinné + Povinné pole Číslo s plovoucí desetinnou čárkou (Float) - Musí být uvedeno a obsahovat hodnotu s plovoucí desetinnou čárkou (Float) větší než 0,0. + Musí být vyplněno a obsahovat číselnou hodnotu (Float) větší než 0.0. name Volitelné - Řetězec (String) - Pokud je přítomen, musí být neprázdný řetězec. + Řetězec + Pokud je uvedeno, musí být neprázdný text. Název položky ve skupině. part Volitelné Objekt/Array - Pokud je uvedeno, musí to být objekt/array a minimálně jedno pole musí být vyplněno: + Pokud má být přiřazena součástka, musí být objektem/arrayem a alespoň jedno z následujících polí musí být vyplněno:
  • part.id
  • +
  • part.mpnr
  • +
  • part.ipn
  • part.name
@@ -13546,38 +13560,38 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz part.id Volitelné Celé číslo (Integer) - Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID součástky v Part-DB. + Celé číslo > 0. Odpovídá internímu číselnému ID součástky v databázi. - part.name + part.mpnr Volitelné - Řetězec (String) - Neprázdný řetězec, pokud není zadáno part.mpnr nebo part.ipn. + Řetězec + Neprázdný text, pokud není vyplněno part.id, part.ipn ani part.name. - part.mpnr + part.ipn Volitelné - Řetězec (String) - Neprázdný řetězec, pokud není zadáno part.name nebo part.ipn. + Řetězec + Neprázdný text, pokud není vyplněno part.id, part.mpnr ani part.name. - part.ipn + part.name Volitelné - Řetězec (String) - Neprázdný řetězec, pokud není zadáno part.name nebo part.mpnr. + Řetězec + Neprázdný text, pokud není vyplněno part.id, part.mpnr ani part.ipn. part.description Volitelné Řetězec nebo null - Pokud je přítomen, musí být neprázdný řetězec nebo null. + Pokud je uvedeno, musí být neprázdný řetězec nebo null. Přepíše stávající hodnotu v součástce. part.manufacturer Volitelné Objekt/Array - Pokud je přítomen, musí to být objekt/array a minimálně jedno pole musí být vyplněno: + Pokud má být výrobce součástky upraven nebo má být součástka jednoznačně identifikována pomocí hodnoty part.mpnr, musí být objektem/arrayem a alespoň jedno z následujících polí musí být vyplněno:
  • manufacturer.id
  • manufacturer.name
  • @@ -13588,20 +13602,20 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz manufacturer.id Volitelné Celé číslo (Integer) - Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID výrobce. + Celé číslo > 0. Odpovídá internímu číselnému ID výrobce. manufacturer.name Volitelné - Řetězec (String) - Neprázdný řetězec, pokud není uvedeno manufacturer.id. + Řetězec + Neprázdný text, pokud není uveden manufacturer.id. part.category Volitelné Objekt/Array - Pokud je přítomen, musí to být objekt/array a minimálně jedno pole musí být vyplněno: + Pokud má být kategorie součástky upravena, musí být objektem/arrayem a alespoň jedno z následujících polí musí být vyplněno:
    • category.id
    • category.name
    • @@ -13612,13 +13626,138 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz category.id Volitelné Celé číslo (Integer) - Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID kategorie součástky. + Celé číslo > 0. Odpovídá internímu číselnému ID kategorie součástky. category.name Volitelné - Řetězec (String) - Neprázdný řetězec, pokud není uvedeno category.id. + Řetězec + Neprázdný text, pokud není uvedeno category.id. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Možné sloupce: + + + + + assembly.bom_import.template.csv.table + + + + + Sloupec + Podmínka + Datový typ + Popis + + + + + quantity + Povinné pole + Číslo s plovoucí desetinnou čárkou (Float) + Musí být vyplněno a obsahovat číselnou hodnotu (Float) větší než 0.0. + + + name + Volitelné + Řetězec + Název položky ve skupině. + + + Sloupce začínající part_ + + Pokud má být přiřazena součástka, jeden z následujících sloupců musí být uveden a vyplněn: +
        +
      • part_id
      • +
      • part_mpnr
      • +
      • part_ipn
      • +
      • part_name
      • +
      + + + + part_id + Volitelné + Celé číslo (Integer) + Celé číslo > 0. Odpovídá internímu číselnému ID součástky v databázi. + + + part_mpnr + Volitelné + Řetězec + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_ipn nebo part_name. + + + part_ipn + Volitelné + Řetězec + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_mpnr nebo part_name. + + + part_name + Volitelné + Řetězec + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_mpnr nebo part_ipn. + + + part_description + Volitelné + Řetězec + Bude přeneseno do součástky a přepíše aktuální hodnotu, pokud je uveden neprázdný text. + + + Sloupce začínající part_manufacturer_ + + Pokud má být výrobce upraven nebo součástka jednoznačně identifikována pomocí part_mpnr, jeden z následujících sloupců musí být uveden a vyplněn: +
        +
      • part_manufacturer_id
      • +
      • part_manufacturer_name
      • +
      + + + + part_manufacturer_id + Volitelné + Celé číslo (Integer) + Celé číslo > 0. Odpovídá internímu číselnému ID výrobce. + + + part_manufacturer_name + Volitelné + Řetězec + Musí být uvedeno, pokud není vyplněn sloupec part_manufacturer_id. + + + Sloupce začínající part.category_ + + Pokud má být kategorie upravena, jeden z následujících sloupců musí být uveden a vyplněn: +
        +
      • part_category_id
      • +
      • part_category_name
      • +
      + + + + part_category_id + Volitelné + Celé číslo (Integer) + Celé číslo > 0. Odpovídá internímu číselnému ID kategorie součástky. + + + part_category_name + Volitelné + Řetězec + Musí být uvedeno, pokud není vyplněn sloupec part_category_id. diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 2461f8c77..4322fd9f8 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -12598,6 +12598,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver JSON for en samling
      + + + assembly.bom_import.type.csv + CSV til en samling + + assembly.bom_import.type.kicad_pcbnew @@ -12622,6 +12628,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver JSON-importskabelon til en samling + + + assembly.import_bom.template.header.csv + Importskabelon CSV til en samling + + assembly.import_bom.template.header.kicad_pcbnew @@ -12681,64 +12693,66 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver quantity - Påkrævet - Flydende tal (Float) - Skal være til stede og indeholde en flydende værdi (Float), der er større end 0,0. + Påkrævet felt + Flydende punkt-tal (Float) + Skal være udfyldt og indeholde en flydende værdi (Float), der er større end 0.0. name - Valgfri - String - Hvis til stede, skal det være en ikke-tom streng. + Valgfrit + Streng + Hvis det er angivet, skal det være en ikke-tom streng. Navn på posten inden for samlingen. part - Valgfri + Valgfrit Objekt/Array - Hvis angivet, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: + Hvis en del skal tildeles, skal det være et objekt/array, og mindst et af følgende felter skal være udfyldt:
      • part.id
      • +
      • part.mpnr
      • +
      • part.ipn
      • part.name
      part.id - Valgfri + Valgfrit Heltal (Integer) - Heltal (Integer) > 0. Matcher Part-DB's interne numeriske ID for komponenten. - - - part.name - Valgfri - String - Ikke-tom streng, hvis part.mpnr eller part.ipn ikke er givet. + Heltal > 0. Tilsvarer den interne nummer-ID for delen i database. part.mpnr - Valgfri - String - Ikke-tom streng, hvis part.name eller part.ipn ikke er givet. + Valgfrit + Streng + Ikke-tom streng, hvis ingen part.id-, part.ipn- eller part.name-værdi er angivet. part.ipn - Valgfri - String - Ikke-tom streng, hvis part.name eller part.mpnr ikke er givet. + Valgfrit + Streng + Ikke-tom streng, hvis ingen part.id-, part.mpnr- eller part.name-værdi er angivet. + + + part.name + Valgfrit + Streng + Ikke-tom streng, hvis ingen part.id-, part.mpnr- eller part.ipn-værdi er angivet. part.description - Valgfri - String eller null - Hvis til stede, skal det være en ikke-tom streng eller null. + Valgfrit + Streng eller null + Hvis angivet, skal det være en ikke-tom streng eller null. Værdien bliver overskrevet i delen. part.manufacturer - Valgfri + Valgfrit Objekt/Array - Hvis til stede, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: + Hvis producenten af en del skal ændres eller entydigt søges ved hjælp af part.mpnr-værdien, skal det være et objekt/array og mindst et af følgende felter skal være udfyldt:
      • manufacturer.id
      • manufacturer.name
      • @@ -12747,22 +12761,22 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver manufacturer.id - Valgfri + Valgfrit Heltal (Integer) - Heltal (Integer) > 0. Matcher producentens interne numeriske ID. + Heltal > 0. Tilsvarer den interne nummer-ID for producenten. manufacturer.name - Valgfri - String - Ikke-tom streng, hvis manufacturer.id ikke er givet. + Valgfrit + Streng + Ikke-tom streng, hvis ingen manufacturer.id er angivet. part.category - Valgfri + Valgfrit Objekt/Array - Hvis til stede, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: + Hvis en delens kategori skal ændres, skal det være et objekt/array, og mindst et af følgende felter skal være udfyldt:
        • category.id
        • category.name
        • @@ -12771,15 +12785,140 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver category.id - Valgfri + Valgfrit Heltal (Integer) - Heltal (Integer) > 0. Matcher komponentkategoriens interne numeriske ID. + Heltal > 0. Tilsvarer den interne nummer-ID for delens kategori. category.name - Valgfri - String - Ikke-tom streng, hvis category.id ikke er givet. + Valgfrit + Streng + Ikke-tom streng, hvis ingen category.id er angivet. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Mulige kolonner: + + + + + assembly.bom_import.template.csv.table + + + + + Kolonne + Betingelse + Datatype + Beskrivelse + + + + + quantity + Påkrævet felt + Flydende punkt-tal (Float) + Skal være udfyldt og indeholde en flydende værdi (Float), der er større end 0.0. + + + name + Valgfrit + Streng + Navnet på posten inden for samlingen. + + + Kolonner, der starter med part_ + + Hvis en del skal tildeles, skal en af følgende kolonner være angivet og udfyldt: +
            +
          • part_id
          • +
          • part_mpnr
          • +
          • part_ipn
          • +
          • part_name
          • +
          + + + + part_id + Valgfrit + Heltal (Integer) + Heltal > 0. Tilsvarer den interne nummer-ID for delen i databasen. + + + part_mpnr + Valgfrit + Streng + Skal angives, hvis ingen af kolonnerne part_id, part_ipn, eller part_name er udfyldt. + + + part_ipn + Valgfrit + Streng + Skal angives, hvis ingen af kolonnerne part_id, part_mpnr eller part_name er udfyldt. + + + part_name + Valgfrit + Streng + Skal angives, hvis ingen af kolonnerne part_id, part_mpnr eller part_ipn er udfyldt. + + + part_description + Valgfrit + Streng + Vil blive overført og overskrive værdien for delen, hvis en ikke-tom streng er angivet. + + + Kolonner, der starter med part_manufacturer_ + + Hvis producenten for en del skal ændres eller søges entydigt ved hjælp af part_mpnr, skal en af følgende kolonner være angivet og udfyldt: +
            +
          • part_manufacturer_id
          • +
          • part_manufacturer_name
          • +
          + + + + part_manufacturer_id + Valgfrit + Heltal (Integer) + Heltal > 0. Tilsvarer den interne nummer-ID for producenten. + + + part_manufacturer_name + Valgfrit + Streng + Skal angives, hvis ingen part_manufacturer_id er udfyldt. + + + Kolonner, der starter med part.category_ + + Hvis en dels kategori skal ændres, skal en af følgende kolonner være angivet og udfyldt: +
            +
          • part_category_id
          • +
          • part_category_name
          • +
          + + + + part_category_id + Valgfrit + Heltal (Integer) + Heltal > 0. Tilsvarer den interne nummer-ID for delens kategori. + + + part_category_name + Valgfrit + Streng + Skal angives, hvis ingen part_category_id er udfyldt. diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index bc0cac33f..8377facc9 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -12986,7 +12986,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön assembly.edit.bom.import_bom - Bauteile importieren + Importiere Bauteil-Liste @@ -13241,6 +13241,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön JSON für eine Baugruppe
          + + + assembly.bom_import.type.csv + CSV für eine Baugruppe + + assembly.bom_import.type.kicad_pcbnew @@ -13265,6 +13271,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Import-Vorlage JSON für eine Baugruppe + + + assembly.import_bom.template.header.csv + Import-Vorlage CSV für eine Baugruppe + + assembly.import_bom.template.header.kicad_pcbnew @@ -13332,16 +13344,18 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön name Optional String - Falls vorhanden, muss es ein nicht-leerer String sein. + Falls vorhanden, muss es ein nicht-leerer String sein. Name des Eintrags innerhalb der Baugruppe. part Optional Objekt/Array - Falls angegeben, muss es ein Objekt/Array sein und mindestens eines der Felder ausgefüllt sein: + Falls ein Bauteil zugeordnet werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein:
          • part.id
          • +
          • part.mpnr
          • +
          • part.ipn
          • part.name
          @@ -13353,35 +13367,35 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. - part.name + part.mpnr Optional String - Nicht-leerer String, falls keine part.mpnr- bzw. part.ipn-Angabe gegeben ist. + Nicht-leerer String, falls keine part.id-, part-ipn- bzw. part.name-Angabe gegeben ist. - part.mpnr + part.ipn Optional String - Nicht-leerer String, falls keine part.name- bzw. part-ipn-Angabe gegeben ist. + Nicht-leerer String, falls keine part.id-, part.mpnr bzw. part.name-Angabe gegeben ist. - part.ipn + part.name Optional String - Nicht-leerer String, falls keine part.name- bzw. part.mpnr-Angabe gegeben ist. + Nicht-leerer String, falls keine part.id-, part.mpnr- bzw. part.ipn-Angabe gegeben ist. part.description Optional String oder null - Falls vorhanden, muss es ein nicht-leerer String sein oder null. + Falls vorhanden, muss es ein nicht-leerer String sein oder null. Wird in das Bauteil übernommen, d.h. der dortige Wert überschrieben. part.manufacturer Optional Objekt/Array - Falls vorhanden, muss es ein Objekt/Array sein und mindestens eines der Felder ausgefüllt sein: + Falls der Hersteller eines Bauteils mit angepasst werden oder das Bauteil anhand der part.mpnr-Angabe eindeutig gesucht werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein:
          • manufacturer.id
          • manufacturer.name
          • @@ -13405,7 +13419,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Optional Objekt/Array - Falls vorhanden, muss es ein Objekt/Array sein und mindestens eines der Felder ausgefüllt sein: + Falls die Kategorie eine Bauteils mit angepasst werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein:
            • category.id
            • category.name
            • @@ -13430,6 +13444,131 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön + + + assembly.bom_import.template.csv.exptected_columns + Mögliche Spalten: + + + + + assembly.bom_import.template.csv.table + + + + + Spalte + Bedingung + Datentyp + Beschreibung + + + + + quantity + Pflichtfeld + Gleitkommazahl (Float) + Muss gegeben sein und enthält einen Gleitkommawert (Float), der größer als 0.0 ist. + + + name + Optional + String + Name des Eintrags innerhalb der Baugruppe. + + + Spalten beginnend mit part_ + + Falls ein Bauteil zugeordnet werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
                +
              • part_id
              • +
              • part_mpnr
              • +
              • part_ipn
              • +
              • part_name
              • +
              + + + + part_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. + + + part_mpnr + Optional + String + Anzugeben, falls keine part_id-, part_ipn- bzw. part_name-Spalte ausgefüllt gegeben ist. + + + part_ipn + Optional + String + Anzugeben, falls keine part_id-, part_mpnr- bzw. part_name-Spalte ausgefüllt gegeben ist. + + + part_name + Optional + String + Anzugeben, falls keine part_id-, part_mpnr- bzw. part_ipn-Spalte ausgefüllt gegeben ist. + + + part_description + Optional + String + Wird in das Bauteil übernommen, d.h. der dortige Wert überschrieben sofern ein nicht-leerer String gegeben ist. + + + Spalten beginnend mit part_manufacturer_ + + Falls der Hersteller eines Bauteils mit angepasst werden oder das Bauteil anhand der part_mpnr-Angabe eindeutig gesucht werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
                +
              • part_manufacturer_id
              • +
              • part_manufacturer_name
              • +
              + + + + part_manufacturer_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID des Herstellers. + + + part_manufacturer_name + Optional + String + Anzugeben, falls keine part_manufacturer_id-Spalte ausgefüllt gegeben ist. + + + Spalten beginnend mit part.category_ + + Falls die Kategorie eines Bauteils mit angepasst werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
                +
              • part_category_id
              • +
              • part_category_name
              • +
              + + + + part_category_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID der Kategorie des Bauteils. + + + part_category_name + Optional + String + Anzugeben, falls keine part_category_id-Spalte ausgefüllt gegeben ist. + + + + ]]> +
              +
              +
              assembly.bom_import.template.kicad_pcbnew.exptected_columns diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index b54131896..c97f06304 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1925,6 +1925,12 @@ JSON για συναρμολόγηση + + + assembly.bom_import.type.csv + CSV για μια συναρμολόγηση + + assembly.bom_import.type.kicad_pcbnew @@ -1949,6 +1955,12 @@ Πρότυπο εισαγωγής JSON για συναρμολόγηση + + + assembly.import_bom.template.header.csv + Πρότυπο CSV εισαγωγής για μια συναρμολόγηση + + assembly.import_bom.template.header.kicad_pcbnew @@ -2001,7 +2013,7 @@ Πεδίο Προϋπόθεση - Τύπος Δεδομένων + Τύπος δεδομένων Περιγραφή @@ -2010,22 +2022,24 @@ quantity Υποχρεωτικό πεδίο Αριθμός κινητής υποδιαστολής (Float) - Πρέπει να παρέχεται και να περιέχει τιμή κινητής υποδιαστολής (Float) μεγαλύτερη από 0.0. + Πρέπει να είναι συμπληρωμένος και να περιέχει μια αριθμητική τιμή (Float) μεγαλύτερη από 0.0. name Προαιρετικό - Κείμενο (String) - Εάν υπάρχει, πρέπει να είναι μη κενό κείμενο. + Χαρακτηριστική ακολουθία (String) + Αν υπάρχει, πρέπει να είναι μη κενό κείμενο. Το όνομα του είδους μέσα στη συλλογή. part Προαιρετικό Αντικείμενο/Πίνακας - Εάν παρέχεται, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα πεδία του να είναι συμπληρωμένο: + Αν πρόκειται να ανατεθεί ένα εξάρτημα, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα παρακάτω πεδία πρέπει να έχει συμπληρωθεί:
              • part.id
              • +
              • part.mpnr
              • +
              • part.ipn
              • part.name
              @@ -2034,38 +2048,38 @@ part.id Προαιρετικό Ακέραιος αριθμός (Integer) - Ακέραιος (Integer) > 0. Αντιστοιχεί στην εσωτερική αριθμητική ταυτότητα (ID) του εξαρτήματος στη βάση δεδομένων. + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID του εξαρτήματος στη βάση δεδομένων. - part.name + part.mpnr Προαιρετικό - Κείμενο (String) - Μη κενό κείμενο, εάν δεν παρέχονται οι ενδείξεις part.mpnr ή part.ipn. + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το part.id, part.ipn ή part.name. - part.mpnr + part.ipn Προαιρετικό - Κείμενο (String) - Μη κενό κείμενο, εάν δεν παρέχονται οι ενδείξεις part.name ή part.ipn. + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το part.id, part.mpnr ή part.name. - part.ipn + part.name Προαιρετικό - Κείμενο (String) - Μη κενό κείμενο, εάν δεν παρέχονται οι ενδείξεις part.name ή part.mpnr. + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το part.id, part.mpnr ή part.ipn. part.description Προαιρετικό - Κείμενο ή null - Εάν υπάρχει, πρέπει να είναι μη κενό κείμενο, ή null. + Χαρακτηριστική ακολουθία ή null + Αν υπάρχει, πρέπει να είναι μη κενό κείμενο ή null. Υπερισχύει της υπάρχουσας τιμής στο εξάρτημα. part.manufacturer Προαιρετικό Αντικείμενο/Πίνακας - Εάν υπάρχει, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα πεδία του να είναι συμπληρωμένο: + Αν ο κατασκευαστής ενός εξαρτήματος χρειάζεται να αλλάξει ή να αναζητηθεί μονοσήμαντα μέσω της τιμής part.mpnr, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα παρακάτω πεδία να είναι συμπληρωμένα:
              • manufacturer.id
              • manufacturer.name
              • @@ -2076,20 +2090,20 @@ manufacturer.id Προαιρετικό Ακέραιος αριθμός (Integer) - Ακέραιος (Integer) > 0. Αντιστοιχεί στην εσωτερική αριθμητική ταυτότητα (ID) του κατασκευαστή. + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID του κατασκευαστή. manufacturer.name Προαιρετικό - Κείμενο (String) - Μη κενό κείμενο, εάν δεν παρέχεται η ένδειξη manufacturer.id. + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το manufacturer.id. part.category Προαιρετικό Αντικείμενο/Πίνακας - Εάν υπάρχει, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα πεδία του να είναι συμπληρωμένο: + Αν χρειάζεται να τροποποιηθεί η κατηγορία του εξαρτήματος, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα παρακάτω πεδία να είναι συμπληρωμένα:
                • category.id
                • category.name
                • @@ -2100,13 +2114,138 @@ category.id Προαιρετικό Ακέραιος αριθμός (Integer) - Ακέραιος (Integer) > 0. Αντιστοιχεί στην εσωτερική αριθμητική ταυτότητα (ID) της κατηγορίας του εξαρτήματος. + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID της κατηγορίας εξαρτήματος. category.name Προαιρετικό - Κείμενο (String) - Μη κενό κείμενο, εάν δεν παρέχεται η ένδειξη category.id. + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το category.id. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Δυνατές στήλες: + + + + + assembly.bom_import.template.csv.table + + + + + Στήλη + Προϋπόθεση + Τύπος δεδομένων + Περιγραφή + + + + + quantity + Υποχρεωτικό πεδίο + Αριθμός κινητής υποδιαστολής (Float) + Πρέπει να είναι συμπληρωμένος και να περιέχει μια αριθμητική τιμή (Float) μεγαλύτερη από 0.0. + + + name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Το όνομα του είδους μέσα στη συλλογή. + + + Στήλες που ξεκινούν με part_ + + Αν χρειάζεται να αποδοθεί εξάρτημα, πρέπει να συμπληρωθεί μία από τις παρακάτω στήλες: +
                    +
                  • part_id
                  • +
                  • part_mpnr
                  • +
                  • part_ipn
                  • +
                  • part_name
                  • +
                  + + + + part_id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID του εξαρτήματος στη βάση δεδομένων. + + + part_mpnr + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσουν οι part_id, part_ipn ή part_name. + + + part_ipn + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσουν οι part_id, part_mpnr ή part_name. + + + part_name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσουν οι part_id, part_mpnr ή part_ipn. + + + part_description + Προαιρετικό + Χαρακτηριστική ακολουθία + Θα μεταφερθεί και θα αντικαταστήσει την τιμή στο εξάρτημα, αν δοθεί μια μη κενή ακολουθία. + + + Στήλες που ξεκινούν με part_manufacturer_ + + Αν ο κατασκευαστής του εξαρτήματος πρέπει να αλλάξει ή να αναζητηθεί μονοσήμαντα μέσω της part_mpnr, πρέπει να συμπληρωθεί μία από τις παρακάτω στήλες: +
                    +
                  • part_manufacturer_id
                  • +
                  • part_manufacturer_name
                  • +
                  + + + + part_manufacturer_id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID του κατασκευαστή. + + + part_manufacturer_name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσει το πεδίο part_manufacturer_id. + + + Στήλες που ξεκινούν με part.category_ + + Αν η κατηγορία του εξαρτήματος πρέπει να αλλάξει, πρέπει να συμπληρωθεί μία από τις παρακάτω στήλες: +
                    +
                  • part_category_id
                  • +
                  • part_category_name
                  • +
                  + + + + part_category_id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID της κατηγορίας του εξαρτήματος. + + + part_category_name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσει το πεδίο part_category_id. diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 8ac704001..f8d762f2f 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12987,7 +12987,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g assembly.edit.bom.import_bom - Import BOM + Import part list @@ -13191,7 +13191,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g assembly.import_bom - Import BOM for project + Import part list for assembly @@ -13239,7 +13239,13 @@ Please note, that you can not impersonate a disabled user. If you try you will g assembly.bom_import.type.json - JSON for one assembly + JSON for an assembly + + + + + assembly.bom_import.type.csv + CSV for an assembly @@ -13266,6 +13272,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Import template JSON format for one assembly
                  + + + assembly.import_bom.template.header.csv + Import template CSV format for one assembly + + assembly.import_bom.template.header.kicad_pcbnew @@ -13318,31 +13330,33 @@ Please note, that you can not impersonate a disabled user. If you try you will g Field Condition - Data type + Data Type Description quantity - Required - Floating point (Float) - Must be provided and contains a floating-point value (Float) greater than 0.0. + Mandatory + Floating point number (Float) + Must be provided and contains a floating point value (Float), which is greater than 0.0. name Optional String - If present, it must be a non-empty string. + If present, it must be a non-empty string. Name of the entry within the assembly. part Optional Object/Array - If provided, it must be an object/array and at least one of the fields must be filled: + If a part is to be associated, it must be an object/array with at least one of the following fields filled out:
                  • part.id
                  • +
                  • part.mpnr
                  • +
                  • part.ipn
                  • part.name
                  @@ -13351,38 +13365,38 @@ Please note, that you can not impersonate a disabled user. If you try you will g part.id Optional Integer - Integer > 0. Matches the Part-DB internal numeric ID of the component. + Integer > 0. Corresponds to the part database internal numeric ID of the part. - part.name + part.mpnr Optional String - Non-empty string if no part.mpnr or part.ipn is provided. + Non-empty string, if no part.id, part.ipn, or part.name is provided. - part.mpnr + part.ipn Optional String - Non-empty string if no part.name or part.ipn is provided. + Non-empty string, if no part.id, part.mpnr, or part.name is provided. - part.ipn + part.name Optional String - Non-empty string if no part.name or part.mpnr is provided. + Non-empty string, if no part.id, part.mpnr, or part.ipn is provided. part.description Optional String or null - If present, it must be a non-empty string or null. + If provided, it must be a non-empty string or null. It will be transferred to the part, overwriting its current value. part.manufacturer Optional Object/Array - If present, it must be an object/array and at least one of the fields must be filled: + If the manufacturer of a part is to be adjusted or the part is to be uniquely identified using the part.mpnr, it must be an object/array with at least one of the following fields filled out:
                  • manufacturer.id
                  • manufacturer.name
                  • @@ -13393,20 +13407,20 @@ Please note, that you can not impersonate a disabled user. If you try you will g manufacturer.id Optional Integer - Integer > 0. Matches the internal numeric ID of the manufacturer. + Integer > 0. Corresponds to the internal numeric ID of the manufacturer. manufacturer.name Optional String - Non-empty string if no manufacturer.id is provided. + Non-empty string, if no manufacturer.id is provided. part.category Optional Object/Array - If present, it must be an object/array and at least one of the fields must be filled: + If the category of a part is to be adjusted, it must be an object/array with at least one of the following fields filled out:
                    • category.id
                    • category.name
                    • @@ -13417,13 +13431,138 @@ Please note, that you can not impersonate a disabled user. If you try you will g category.id Optional Integer - Integer > 0. Matches the internal numeric ID of the component's category. + Integer > 0. Corresponds to the internal numeric ID of the part category. category.name Optional String - Non-empty string if no category.id is provided. + Non-empty string, if no category.id is provided. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Possible columns: + + + + + assembly.bom_import.template.csv.table + + + + + Column + Condition + Data Type + Description + + + + + quantity + Mandatory + Floating point number (Float) + Must be provided and contains a floating point value (Float), which is greater than 0.0. + + + name + Optional + String + Name of the entry within the assembly. + + + Columns starting with part_ + + If a part is to be associated, one of the following columns must be provided and filled out: +
                        +
                      • part_id
                      • +
                      • part_mpnr
                      • +
                      • part_ipn
                      • +
                      • part_name
                      • +
                      + + + + part_id + Optional + Integer + Integer > 0. Corresponds to the part database internal numeric ID of the part. + + + part_mpnr + Optional + String + Must be provided if no part_id, part_ipn, or part_name column is filled out. + + + part_ipn + Optional + String + Must be provided if no part_id, part_mpnr, or part_name column is filled out. + + + part_name + Optional + String + Must be provided if no part_id, part_mpnr, or part_ipn column is filled out. + + + part_description + Optional + String + Will be transferred to the part, overwriting its current value if a non-empty string is provided. + + + Columns starting with part_manufacturer_ + + If the manufacturer of a part is to be adjusted or the part is to be uniquely identified using the part_mpnr, one of the following columns must be provided and filled out: +
                        +
                      • part_manufacturer_id
                      • +
                      • part_manufacturer_name
                      • +
                      + + + + part_manufacturer_id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the manufacturer. + + + part_manufacturer_name + Optional + String + Must be provided if no part_manufacturer_id column is filled out. + + + Columns starting with part.category_ + + If the category of a part is to be adjusted, one of the following columns must be provided and filled out: +
                        +
                      • part_category_id
                      • +
                      • part_category_name
                      • +
                      + + + + part_category_id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the part category. + + + part_category_name + Optional + String + Must be provided if no part_category_id column is filled out. diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 0383f964f..ff1183f02 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -12734,6 +12734,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S JSON para un ensamblaje
                      + + + assembly.bom_import.type.csv + CSV para un ensamblaje + + assembly.bom_import.type.kicad_pcbnew @@ -12758,6 +12764,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Plantilla de importación JSON para un ensamblaje + + + assembly.import_bom.template.header.csv + Plantilla de importación CSV para un ensamblaje + + assembly.import_bom.template.header.kicad_pcbnew @@ -12810,31 +12822,33 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Campo Condición - Tipo de dato + Tipo de datos Descripción quantity - Obligatorio - Número decimal (Float) - Debe estar presente y contener un valor decimal (Float) mayor que 0.0. + Campo obligatorio + Número de punto flotante (Float) + Debe completarse y contener un valor flotante (Float) mayor a 0.0. name Opcional - Cadena de texto (String) - Si está presente, debe ser una cadena de texto no vacía. + Cadena + Si se especifica, debe ser una cadena no vacía. El nombre del elemento dentro del conjunto. part Opcional Objeto/Array - Si se proporciona, debe ser un objeto/array y al menos uno de los campos debe estar completado: + Si se debe asignar una pieza, debe ser un objeto/array, y al menos uno de los siguientes campos debe completarse:
                      • part.id
                      • +
                      • part.mpnr
                      • +
                      • part.ipn
                      • part.name
                      @@ -12843,38 +12857,38 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S part.id Opcional Entero (Integer) - Entero (Integer) > 0. Corresponde al ID numérico interno del componente en la base de datos. + Entero > 0. Corresponde al ID interno numérico de la pieza en la base de datos. - part.name + part.mpnr Opcional - Cadena de texto (String) - Cadena de texto no vacía, si no se proporciona part.mpnr o part.ipn. + Cadena + Cadena no vacía si no se rellenan los campos part.id, part.ipn o part.name. - part.mpnr + part.ipn Opcional - Cadena de texto (String) - Cadena de texto no vacía, si no se proporciona part.name o part.ipn. + Cadena + Cadena no vacía si no se rellenan los campos part.id, part.mpnr o part.name. - part.ipn + part.name Opcional - Cadena de texto (String) - Cadena de texto no vacía, si no se proporciona part.name o part.mpnr. + Cadena + Cadena no vacía si no se rellenan los campos part.id, part.mpnr o part.ipn. part.description Opcional - Cadena de texto (String) o null - Si está presente, debe ser una cadena de texto no vacía o null. + Cadena o null + Si se especifica, debe ser una cadena no vacía o null. Este valor sobrescribirá el existente en la pieza. part.manufacturer Opcional Objeto/Array - Si está presente, debe ser un objeto/array y al menos uno de los campos debe estar completado: + Si se desea cambiar el fabricante de la pieza o buscarlo de forma única utilizando el valor part.mpnr, debe ser un objeto/array y al menos uno de los siguientes campos debe completarse:
                      • manufacturer.id
                      • manufacturer.name
                      • @@ -12885,20 +12899,20 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S manufacturer.id Opcional Entero (Integer) - Entero (Integer) > 0. Corresponde al ID numérico interno del fabricante. + Entero > 0. Corresponde al ID interno numérico del fabricante. manufacturer.name Opcional - Cadena de texto (String) - Cadena de texto no vacía, si no se proporciona manufacturer.id. + Cadena + Cadena no vacía si no se proporciona el campo manufacturer.id. part.category Opcional Objeto/Array - Si está presente, debe ser un objeto/array y al menos uno de los campos debe estar completado: + Si se desea cambiar la categoría de la pieza, debe ser un objeto/array y al menos uno de los siguientes campos debe completarse:
                        • category.id
                        • category.name
                        • @@ -12909,13 +12923,138 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S category.id Opcional Entero (Integer) - Entero (Integer) > 0. Corresponde al ID numérico interno de la categoría del componente. + Entero > 0. Corresponde al ID interno numérico de la categoría de la pieza. category.name Opcional - Cadena de texto (String) - Cadena de texto no vacía, si no se proporciona category.id. + Cadena + Cadena no vacía si no se proporciona el campo category.id. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Columnas posibles: + + + + + assembly.bom_import.template.csv.table + + + + + Columna + Condición + Tipo de datos + Descripción + + + + + quantity + Campo obligatorio + Número de punto flotante (Float) + Debe completarse y contener un valor flotante (Float) mayor a 0.0. + + + name + Opcional + Cadena + El nombre del elemento dentro del conjunto. + + + Columnas que comienzan con part_ + + Si se debe asignar una pieza, al menos una de las siguientes columnas debe completarse: +
                            +
                          • part_id
                          • +
                          • part_mpnr
                          • +
                          • part_ipn
                          • +
                          • part_name
                          • +
                          + + + + part_id + Opcional + Entero (Integer) + Entero > 0. Corresponde al ID interno numérico de la pieza en la base de datos. + + + part_mpnr + Opcional + Cadena + Debe completarse si no se han rellenado las columnas part_id, part_ipn o part_name. + + + part_ipn + Opcional + Cadena + Debe completarse si no se han rellenado las columnas part_id, part_mpnr o part_name. + + + part_name + Opcional + Cadena + Debe completarse si no se han rellenado las columnas part_id, part_mpnr o part_ipn. + + + part_description + Opcional + Cadena + Se transferirá y reemplazará el valor de la descripción de la pieza si se proporciona una cadena no vacía. + + + Columnas que comienzan con part_manufacturer_ + + Si el fabricante de la pieza debe cambiarse o buscarse exclusivamente mediante part_mpnr, al menos una de las siguientes columnas debe completarse: +
                            +
                          • part_manufacturer_id
                          • +
                          • part_manufacturer_name
                          • +
                          + + + + part_manufacturer_id + Opcional + Entero (Integer) + Entero > 0. Corresponde al ID interno numérico del fabricante. + + + part_manufacturer_name + Opcional + Cadena + Debe completarse si no se ha proporcionado el campo part_manufacturer_id. + + + Columnas que comienzan con part.category_ + + Si se necesita modificar la categoría de la pieza, al menos una de las siguientes columnas debe completarse: +
                            +
                          • part_category_id
                          • +
                          • part_category_name
                          • +
                          + + + + part_category_id + Opcional + Entero (Integer) + Entero > 0. Corresponde al ID interno numérico de la categoría de la pieza. + + + part_category_name + Opcional + Cadena + Debe completarse si no se ha proporcionado el campo part_category_id. diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 320121909..9cd293219 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -9487,6 +9487,12 @@ exemple de ville
                          JSON pour un assemblage
                          + + + assembly.bom_import.type.csv + CSV pour un assemblage + + assembly.bom_import.type.kicad_pcbnew @@ -9511,6 +9517,12 @@ exemple de ville Modèle d’importation JSON pour un assemblage + + + assembly.import_bom.template.header.csv + Modèle d'importation CSV pour un assemblage + + assembly.import_bom.template.header.kicad_pcbnew @@ -9570,24 +9582,26 @@ exemple de ville quantity - Obligatoire - Nombre décimal (Float) - Doit être présent et contenir une valeur décimale (Float) supérieure à 0,0. + Champ obligatoire + Nombre à virgule flottante (Float) + Doit être rempli et contenir une valeur décimale (Float) supérieure à 0.0. name Optionnel - Chaîne (String) - Si présent, doit être une chaîne non vide. + Chaîne + Si renseigné, doit être une chaîne non vide. Le nom de l'élément dans l'assemblage. part Optionnel Objet/Tableau - Si fourni, doit être un objet/un tableau et au moins un des champs doit être rempli : + Si une pièce doit être assignée, cela doit être un objet/tableau et au moins un des champs suivants doit être renseigné :
                          • part.id
                          • +
                          • part.mpnr
                          • +
                          • part.ipn
                          • part.name
                          @@ -9596,38 +9610,38 @@ exemple de ville part.id Optionnel Entier (Integer) - Entier (Integer) > 0. Correspond à l'ID numérique interne dans Part-DB du composant. + Nombre entier > 0. Correspond à l'ID interne numérique de la pièce dans la base de données. - part.name + part.mpnr Optionnel - Chaîne (String) - Chaîne non vide si part.mpnr ou part.ipn ne sont pas fournis. + Chaîne + Chaîne non vide si part.id, part.ipn ou part.name ne sont pas renseignés. - part.mpnr + part.ipn Optionnel - Chaîne (String) - Chaîne non vide si part.name ou part.ipn ne sont pas fournis. + Chaîne + Chaîne non vide si part.id, part.mpnr ou part.name ne sont pas renseignés. - part.ipn + part.name Optionnel - Chaîne (String) - Chaîne non vide si part.name ou part.mpnr ne sont pas fournis. + Chaîne + Chaîne non vide si part.id, part.mpnr ou part.ipn ne sont pas renseignés. part.description Optionnel Chaîne ou null - Si présent, doit être une chaîne non vide ou null. + Si renseignée, doit être une chaîne non vide ou null. Ce champ remplacera la valeur existante de la pièce. part.manufacturer Optionnel Objet/Tableau - Si présent, doit être un objet/un tableau et au moins un des champs doit être rempli : + Si le fabricant de la pièce doit être modifié ou recherché de manière unique à l'aide de la valeur part.mpnr, cela doit être un objet/tableau et au moins un des champs suivants doit être renseigné :
                          • manufacturer.id
                          • manufacturer.name
                          • @@ -9638,20 +9652,20 @@ exemple de ville manufacturer.id Optionnel Entier (Integer) - Entier (Integer) > 0. Correspond à l'ID numérique interne du fabricant. + Nombre entier > 0. Correspond à l'ID interne numérique du fabricant. manufacturer.name Optionnel - Chaîne (String) - Chaîne non vide si manufacturer.id n'est pas fourni. + Chaîne + Chaîne non vide si manufacturer.id n'est pas renseigné. part.category Optionnel Objet/Tableau - Si présent, doit être un objet/un tableau et au moins un des champs doit être rempli : + Si la catégorie de la pièce doit être modifiée, cela doit être un objet/tableau et au moins un des champs suivants doit être renseigné :
                            • category.id
                            • category.name
                            • @@ -9662,13 +9676,138 @@ exemple de ville category.id Optionnel Entier (Integer) - Entier (Integer) > 0. Correspond à l'ID numérique interne de la catégorie du composant. + Nombre entier > 0. Correspond à l'ID interne numérique de la catégorie de la pièce. category.name Optionnel - Chaîne (String) - Chaîne non vide si category.id n'est pas fourni. + Chaîne + Chaîne non vide si category.id n'est pas renseigné. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Colonnes possibles : + + + + + assembly.bom_import.template.csv.table + + + + + Colonne + Condition + Type de données + Description + + + + + quantity + Champ obligatoire + Nombre à virgule flottante (Float) + Doit être rempli et contenir une valeur décimale (Float) supérieure à 0.0. + + + name + Optionnel + Chaîne + Le nom de l'élément dans l'assemblage. + + + Colonnes commençant par part_ + + Si une pièce doit être assignée, au moins une des colonnes suivantes doit être renseignée : +
                                +
                              • part_id
                              • +
                              • part_mpnr
                              • +
                              • part_ipn
                              • +
                              • part_name
                              • +
                              + + + + part_id + Optionnel + Entier (Integer) + Nombre entier > 0. Correspond à l'ID interne numérique de la pièce dans la base de données. + + + part_mpnr + Optionnel + Chaîne + Doit être renseignée si les colonnes part_id, part_ipn ou part_name ne sont pas remplies. + + + part_ipn + Optionnel + Chaîne + Doit être renseignée si les colonnes part_id, part_mpnr ou part_name ne sont pas remplies. + + + part_name + Optionnel + Chaîne + Doit être renseignée si les colonnes part_id, part_mpnr ou part_ipn ne sont pas remplies. + + + part_description + Optionnel + Chaîne + Sera transférée et remplacera la valeur existante de la description si une chaîne non vide est fournie. + + + Colonnes commençant par part_manufacturer_ + + Si le fabricant de la pièce doit être modifié ou recherché uniquement via part_mpnr, au moins une des colonnes suivantes doit être renseignée : +
                                +
                              • part_manufacturer_id
                              • +
                              • part_manufacturer_name
                              • +
                              + + + + part_manufacturer_id + Optionnel + Entier (Integer) + Nombre entier > 0. Correspond à l'ID interne numérique du fabricant. + + + part_manufacturer_name + Optionnel + Chaîne + Doit être renseignée si part_manufacturer_id n'est pas fourni. + + + Colonnes commençant par part.category_ + + Si la catégorie de la pièce doit être modifiée, au moins une des colonnes suivantes doit être renseignée : +
                                +
                              • part_category_id
                              • +
                              • part_category_name
                              • +
                              + + + + part_category_id + Optionnel + Entier (Integer) + Nombre entier > 0. Correspond à l'ID interne numérique de la catégorie de la pièce. + + + part_category_name + Optionnel + Chaîne + Doit être renseignée si part_category_id n'est pas fourni. @@ -9679,75 +9818,75 @@ exemple de ville
                              assembly.bom_import.template.kicad_pcbnew.exptected_columns - Colonnes attendues : + Colonnes attendues: assembly.bom_import.template.kicad_pcbnew.exptected_columns.note - Remarque : Aucun mappage n'est effectué avec des composants spécifiques issus de la gestion des catégories.

                              - ]]> + Remarque: Aucun mappage n'est effectué avec des composants spécifiques issus de la gestion des catégories.

                              + ]]>
                              - - assembly.bom_import.template.kicad_pcbnew.table - - - - - Champ - Condition - Type de Données - Description - - - - - Id - Optionnel - Entier - Champ libre. Un numéro d'identification unique pour chaque composant. - - - Designator - Optionnel - Chaîne - Champ libre. Une désignation de référence unique pour le composant sur le PCB, par exemple, "R1" pour la résistance 1.
                              Utilisé comme le nom de l'emplacement de l'entrée composant dans l'assemblage. - - - Package - Optionnel - Chaîne - Champ libre. Le boîtier ou le format du composant, par exemple, "0805" pour les résistances CMS.
                              Non inclus dans l'entrée composant pour l'assemblage. - - - Quantity - Obligatoire - Entier - Le nombre de composants identiques nécessaires pour créer une instance de l'assemblage.
                              Utilisé comme la quantité dans l'entrée composant de l'assemblage. - - - Designation - Obligatoire - Chaîne - La description ou la fonction du composant, par exemple, valeur de résistance "10kΩ" ou valeur de condensateur "100nF".
                              Utilisé comme le nom dans l'entrée composant pour l'assemblage. - - - Supplier and ref - Optionnel - Chaîne - Champ libre. Peut contenir par exemple des informations spécifiques sur le fournisseur.
                              Utilisé comme une note dans l'entrée composant pour l'assemblage. - - - - ]]> -
                              -
                              + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Champ + Condition + Type de Données + Description + + + + + Id + Optionnel + Entier + Champ libre. Un numéro d'identification unique pour chaque composant. + + + Designator + Optionnel + Chaîne + Champ libre. Une désignation de référence unique pour le composant sur le PCB, par exemple, "R1" pour la résistance 1.
                              Utilisé comme le nom de l'emplacement de l'entrée composant dans l'assemblage. + + + Package + Optionnel + Chaîne + Champ libre. Le boîtier ou le format du composant, par exemple, "0805" pour les résistances CMS.
                              Non inclus dans l'entrée composant pour l'assemblage. + + + Quantity + Obligatoire + Entier + Le nombre de composants identiques nécessaires pour créer une instance de l'assemblage.
                              Utilisé comme la quantité dans l'entrée composant de l'assemblage. + + + Designation + Obligatoire + Chaîne + La description ou la fonction du composant, par exemple, valeur de résistance "10kΩ" ou valeur de condensateur "100nF".
                              Utilisé comme le nom dans l'entrée composant pour l'assemblage. + + + Supplier and ref + Optionnel + Chaîne + Champ libre. Peut contenir par exemple des informations spécifiques sur le fournisseur.
                              Utilisé comme une note dans l'entrée composant pour l'assemblage. + + + + ]]> +
                              +
                              diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index f6e3c9446..8e39c0313 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -12736,6 +12736,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a JSON per un gruppo + + + assembly.bom_import.type.csv + CSV per un'assemblaggio + + assembly.bom_import.type.kicad_pcbnew @@ -12760,6 +12766,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Template di importazione JSON per un gruppo + + + assembly.import_bom.template.header.csv + Modello di importazione CSV per un assemblaggio + + assembly.import_bom.template.header.kicad_pcbnew @@ -12812,31 +12824,33 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Campo Condizione - Tipo di dato + Tipo di dati Descrizione quantity - Obbligatorio + Campo obbligatorio Numero decimale (Float) - Deve essere presente e contenere un valore decimale (Float) maggiore di 0,0. + Deve essere compilato e contenere un valore decimale (Float) maggiore di 0.0. name Opzionale - Stringa (String) - Se presente, deve essere una stringa non vuota. + Stringa + Se specificato, deve essere una stringa non vuota. Il nome del componente all'interno dell'assemblaggio. part Opzionale Oggetto/Array - Se fornito, deve essere un oggetto/un array e almeno uno dei campi deve essere compilato: + Se è necessario assegnare una parte, deve essere un Oggetto/Array e almeno uno dei seguenti campi deve essere compilato:
                              • part.id
                              • +
                              • part.mpnr
                              • +
                              • part.ipn
                              • part.name
                              @@ -12844,39 +12858,39 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a part.id Opzionale - Intero (Integer) - Intero (Integer) > 0. Corrisponde all'ID numerico interno di Part-DB per il componente. + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico del componente nel database. - part.name + part.mpnr Opzionale - Stringa (String) - Stringa non vuota se part.mpnr o part.ipn non sono forniti. + Stringa + Stringa non vuota se i campi part.id, part.ipn o part.name non sono compilati. - part.mpnr + part.ipn Opzionale - Stringa (String) - Stringa non vuota se part.name o part.ipn non sono forniti. + Stringa + Stringa non vuota se i campi part.id, part.mpnr o part.name non sono compilati. - part.ipn + part.name Opzionale - Stringa (String) - Stringa non vuota se part.name o part.mpnr non sono forniti. + Stringa + Stringa non vuota se i campi part.id, part.mpnr o part.ipn non sono compilati. part.description Opzionale Stringa o null - Se presente, deve essere una stringa non vuota o null. + Se specificato, deve essere una stringa non vuota o null. Questo valore sovrascriverà quello esistente nella parte. part.manufacturer Opzionale Oggetto/Array - Se presente, deve essere un oggetto/un array e almeno uno dei campi deve essere compilato: + Se il produttore della parte deve essere cambiato o ricercato esclusivamente utilizzando il valore part.mpnr, deve essere un Oggetto/Array e almeno uno dei seguenti campi deve essere compilato:
                              • manufacturer.id
                              • manufacturer.name
                              • @@ -12886,21 +12900,21 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a manufacturer.id Opzionale - Intero (Integer) - Intero (Integer) > 0. Corrisponde all'ID numerico interno del produttore. + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico del produttore. manufacturer.name Opzionale - Stringa (String) - Stringa non vuota se manufacturer.id non è fornito. + Stringa + Stringa non vuota se il campo manufacturer.id non è compilato. part.category Opzionale Oggetto/Array - Se presente, deve essere un oggetto/un array e almeno uno dei campi deve essere compilato: + Se è necessario modificare la categoria della parte, deve essere un Oggetto/Array e almeno uno dei seguenti campi deve essere compilato:
                                • category.id
                                • category.name
                                • @@ -12910,14 +12924,139 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a category.id Opzionale - Intero (Integer) - Intero (Integer) > 0. Corrisponde all'ID numerico interno della categoria del componente. + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico della categoria della parte. category.name Opzionale - Stringa (String) - Stringa non vuota se category.id non è fornito. + Stringa + Stringa non vuota se il campo category.id non è compilato. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Colonne possibili: + + + + + assembly.bom_import.template.csv.table + + + + + Colonna + Condizione + Tipo di dati + Descrizione + + + + + quantity + Campo obbligatorio + Numero decimale (Float) + Deve essere compilato e contenere un valore decimale (Float) maggiore di 0.0. + + + name + Opzionale + Stringa + Il nome dell'elemento all'interno dell'assemblaggio. + + + Colonne che iniziano con part_ + + Se è necessario assegnare una parte, almeno una delle colonne seguenti deve essere compilata: +
                                    +
                                  • part_id
                                  • +
                                  • part_mpnr
                                  • +
                                  • part_ipn
                                  • +
                                  • part_name
                                  • +
                                  + + + + part_id + Opzionale + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico del componente nel database. + + + part_mpnr + Opzionale + Stringa + Deve essere compilato se le colonne part_id, part_ipn o part_name non sono compilate. + + + part_ipn + Opzionale + Stringa + Deve essere compilato se le colonne part_id, part_mpnr o part_name non sono compilate. + + + part_name + Opzionale + Stringa + Deve essere compilato se le colonne part_id, part_mpnr o part_ipn non sono compilate. + + + part_description + Opzionale + Stringa + Sarà trasferita e sostituirà il valore esistente della descrizione se viene fornita una stringa non vuota. + + + Colonne che iniziano con part_manufacturer_ + + Se il produttore del componente deve essere modificato o ricercato esclusivamente tramite part_mpnr, almeno una delle seguenti colonne deve essere compilata: +
                                    +
                                  • part_manufacturer_id
                                  • +
                                  • part_manufacturer_name
                                  • +
                                  + + + + part_manufacturer_id + Opzionale + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico del produttore. + + + part_manufacturer_name + Opzionale + Stringa + Deve essere compilata se il campo part_manufacturer_id non è fornito. + + + Colonne che iniziano con part_category_ + + Se è necessario modificare la categoria della parte, almeno una delle seguenti colonne deve essere compilata: +
                                    +
                                  • part_category_id
                                  • +
                                  • part_category_name
                                  • +
                                  + + + + part_category_id + Opzionale + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico della categoria del componente. + + + part_category_name + Opzionale + Stringa + Deve essere compilata se il campo part_category_id non è fornito. @@ -12942,61 +13081,61 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a
                                  - - assembly.bom_import.template.kicad_pcbnew.table - - - - - Campo - Condizione - Tipo di Dati - Descrizione - - - - - Id - Opzionale - Intero - Campo libero. Un numero identificativo univoco per ogni componente. - - - Designator - Opzionale - Stringa - Campo libero. Un riferimento univoco al componente su PCB, ad esempio "R1" per il resistore 1.
                                  Utilizzato come nome della posizione nella voce componenti all'interno dell'assemblaggio. - - - Package - Opzionale - Stringa - Campo libero. L'involucro o il fattore di forma del componente, ad esempio "0805" per i resistori SMD.
                                  Non incluso nelle informazioni del componente nell'assemblaggio. - - - Quantity - Obbligatorio - Intero - Il numero di componenti identici richiesti per creare un'istanza dell'assemblaggio.
                                  Utilizzato come quantità nella voce componenti dell'assemblaggio. - - - Designation - Obbligatorio - Stringa - Descrizione o funzione del componente, ad esempio valore resistore "10kΩ" o valore condensatore "100nF".
                                  Utilizzato come nome nella voce componenti dell'assemblaggio. - - - Supplier and ref - Opzionale - Stringa - Campo libero. Può contenere, ad esempio, informazioni specifiche del fornitore.
                                  Utilizzato come nota nelle informazioni del componente nell'assemblaggio. - - - - ]]> -
                                  -
                                  + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Campo + Condizione + Tipo di Dati + Descrizione + + + + + Id + Opzionale + Intero + Campo libero. Un numero identificativo univoco per ogni componente. + + + Designator + Opzionale + Stringa + Campo libero. Un riferimento univoco al componente su PCB, ad esempio "R1" per il resistore 1.
                                  Utilizzato come nome della posizione nella voce componenti all'interno dell'assemblaggio. + + + Package + Opzionale + Stringa + Campo libero. L'involucro o il fattore di forma del componente, ad esempio "0805" per i resistori SMD.
                                  Non incluso nelle informazioni del componente nell'assemblaggio. + + + Quantity + Obbligatorio + Intero + Il numero di componenti identici richiesti per creare un'istanza dell'assemblaggio.
                                  Utilizzato come quantità nella voce componenti dell'assemblaggio. + + + Designation + Obbligatorio + Stringa + Descrizione o funzione del componente, ad esempio valore resistore "10kΩ" o valore condensatore "100nF".
                                  Utilizzato come nome nella voce componenti dell'assemblaggio. + + + Supplier and ref + Opzionale + Stringa + Campo libero. Può contenere, ad esempio, informazioni specifiche del fornitore.
                                  Utilizzato come nota nelle informazioni del componente nell'assemblaggio. + + + + ]]> +
                                  +
                                  diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index a8f5f9151..fb64a0f04 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -9188,6 +9188,12 @@ Exampletown アセンブリ用 JSON + + + assembly.bom_import.type.csv + アセンブリ用のCSV + + assembly.bom_import.type.kicad_pcbnew @@ -9212,6 +9218,12 @@ Exampletown アセンブリ用 JSON テンプレート + + + assembly.import_bom.template.header.csv + アセンブリ用のCSVインポートテンプレート + + assembly.import_bom.template.header.kicad_pcbnew @@ -9271,24 +9283,26 @@ Exampletown quantity - 必須 + 必須項目 浮動小数点数 (Float) - 指定され、0.0より大きい浮動小数点値 (Float) を含む必要があります。 + 入力必須で、0.0よりも大きい浮動小数点数 (Float) を含む必要があります。 name 任意 - 文字列 (String) - 指定されている場合、空でない文字列でなければなりません。 + 文字列 + 指定されている場合、空でない文字列でなければなりません。アセンブリ内のアイテムの名前。 part 任意 オブジェクト/配列 - 指定された場合、オブジェクト/配列であり、以下のフィールドのうち少なくとも1つが入力されている必要があります: + 部品を割り当てる必要がある場合、これはオブジェクト/配列であり、次のフィールドのいずれかを少なくとも1つ入力する必要があります:
                                  • part.id
                                  • +
                                  • part.mpnr
                                  • +
                                  • part.ipn
                                  • part.name
                                  @@ -9297,38 +9311,38 @@ Exampletown part.id 任意 整数 (Integer) - 整数 (Integer) > 0。部品の Part-DB 内部数値 ID に対応します。 + 0より大きい整数。データベース内の部品の内部数値IDに対応します。 - part.name + part.mpnr 任意 - 文字列 (String) - part.mpnr または part.ipn が指定されていない場合、空でない文字列でなければなりません。 + 文字列 + part.id、part.ipn、または part.name が入力されていない場合、空でない文字列。 - part.mpnr + part.ipn 任意 - 文字列 (String) - part.name または part.ipn が指定されていない場合、空でない文字列でなければなりません。 + 文字列 + part.id、part.mpnr、または part.name が入力されていない場合、空でない文字列。 - part.ipn + part.name 任意 - 文字列 (String) - part.name または part.mpnr が指定されていない場合、空でない文字列でなければなりません。 + 文字列 + part.id、part.mpnr、または part.ipn が入力されていない場合、空でない文字列。 part.description 任意 文字列または null - 指定されている場合、空でない文字列または null でなければなりません。 + 指定されている場合、空でない文字列または null である必要があります。この値は部品の既存の値を上書きします。 part.manufacturer 任意 オブジェクト/配列 - 指定されている場合、オブジェクト/配列であり、以下のフィールドのうち少なくとも1つが入力されている必要があります: + 部品のメーカーを変更する場合、または part.mpnr の値を利用して一意に検索する場合、これはオブジェクト/配列であり、次のフィールドのいずれかを少なくとも1つ入力する必要があります:
                                  • manufacturer.id
                                  • manufacturer.name
                                  • @@ -9339,20 +9353,20 @@ Exampletown manufacturer.id 任意 整数 (Integer) - 整数 (Integer) > 0。製造元の内部数値 ID に対応します。 + 0より大きい整数。メーカーの内部数値IDに対応します。 manufacturer.name 任意 - 文字列 (String) - manufacturer.id が指定されていない場合、空でない文字列でなければなりません。 + 文字列 + manufacturer.id が提供されていない場合、空でない文字列。 part.category 任意 オブジェクト/配列 - 指定されている場合、オブジェクト/配列であり、以下のフィールドのうち少なくとも1つが入力されている必要があります: + 部品のカテゴリーを変更する場合、これはオブジェクト/配列であり、次のフィールドのいずれかを少なくとも1つ入力する必要があります:
                                    • category.id
                                    • category.name
                                    • @@ -9363,13 +9377,138 @@ Exampletown category.id 任意 整数 (Integer) - 整数 (Integer) > 0。コンポーネントカテゴリの内部数値 ID に対応します。 + 0より大きい整数。部品のカテゴリーに対応する内部数値ID。 category.name 任意 - 文字列 (String) - category.id が指定されていない場合、空でない文字列でなければなりません。 + 文字列 + category.id が提供されていない場合、空でない文字列。 + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + 可能なカラム: + + + + + assembly.bom_import.template.csv.table + + + + + カラム + 条件 + データ型 + 説明 + + + + + quantity + 必須項目 + 浮動小数点数 (Float) + 入力必須で、0.0よりも大きい浮動小数点数 (Float) を含む必要があります。 + + + name + 任意 + 文字列 + アセンブリ内のアイテムの名前。 + + + part_ で始まるカラム + + 部品を割り当てる必要がある場合、次のカラムのいずれかが少なくとも1つ入力されなければなりません: +
                                        +
                                      • part_id
                                      • +
                                      • part_mpnr
                                      • +
                                      • part_ipn
                                      • +
                                      • part_name
                                      • +
                                      + + + + part_id + 任意 + 整数 (Integer) + 0より大きい整数。データベース内の部品の内部数値ID。 + + + part_mpnr + 任意 + 文字列 + part_id、part_ipn、または part_name が入力されていない場合に入力される必要があります。 + + + part_ipn + 任意 + 文字列 + part_id、part_mpnr、または part_name が入力されていない場合に入力される必要があります。 + + + part_name + 任意 + 文字列 + part_id、part_mpnr、または part_ipn が入力されていない場合に入力される必要があります。 + + + part_description + 任意 + 文字列 + 指定されている場合、部品の説明既存の値を上書きする非空の文字列。 + + + part_manufacturer_ で始まるカラム + + 部品の製造元を変更する場合、または part_mpnr を利用して一意に検索する場合、次のカラムのいずれかを少なくとも1つ入力する必要があります: +
                                        +
                                      • part_manufacturer_id
                                      • +
                                      • part_manufacturer_name
                                      • +
                                      + + + + part_manufacturer_id + 任意 + 整数 (Integer) + 0より大きい整数。製造元の内部数値ID。 + + + part_manufacturer_name + 任意 + 文字列 + part_manufacturer_id が入力されていない場合、入力される必要があります。 + + + part_category_ で始まるカラム + + 部品のカテゴリーを変更する場合、次のカラムのいずれかを少なくとも1つ入力する必要があります: +
                                        +
                                      • part_category_id
                                      • +
                                      • part_category_name
                                      • +
                                      + + + + part_category_id + 任意 + 整数 (Integer) + 0より大きい整数。部品のカテゴリーに対応する内部数値ID。 + + + part_category_name + 任意 + 文字列 + part_category_id が提供されていない場合、入力される必要があります。 @@ -9384,71 +9523,71 @@ Exampletown
                                      - - assembly.bom_import.template.kicad_pcbnew.exptected_columns.note - - 注意: カテゴリ管理から特定のコンポーネントへのマッピングは行われません。

                                      - ]]> -
                                      -
                                      + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + 注意: カテゴリ管理から特定のコンポーネントへのマッピングは行われません。

                                      + ]]> +
                                      +
                                      - - assembly.bom_import.template.kicad_pcbnew.table - - - - - フィールド - 条件 - データ型 - 説明 - - - - - Id - 任意 - 整数 - 自由形式のフィールド。各コンポーネントのユニークな識別番号。 - - - Designator - 任意 - 文字列 - 自由形式のフィールド。PCB上のコンポーネントごとの一意のリファレンス識別子。例: 抵抗 "R1"。
                                      アセンブリ内の部品エントリの配置名として使用。 - - - Package - 任意 - 文字列 - 自由形式のフィールド。コンポーネントのパッケージまたはフォームファクタ。例: 表面実装抵抗器 "0805"。
                                      アセンブリ内の部品エントリ情報には含まれません。 - - - Quantity - 必須 - 整数 - アセンブリの一つのインスタンスを作るために必要な同一部品の数。
                                      アセンブリの部品情報で数量として使用。 - - - Designation - 必須 - 文字列 - コンポーネントの説明や機能。例: 抵抗の値 "10kΩ" やコンデンサの値 "100nF"。
                                      アセンブリの部品情報で名称として使用。 - - - Supplier and ref - 任意 - 文字列 - 自由形式フィールド。例: 供給元に関する特定の情報を含む場合がある。
                                      アセンブリの部品情報の注記として使用。 - - - - ]]> -
                                      -
                                      + + assembly.bom_import.template.kicad_pcbnew.table + + + + + フィールド + 条件 + データ型 + 説明 + + + + + Id + 任意 + 整数 + 自由形式のフィールド。各コンポーネントのユニークな識別番号。 + + + Designator + 任意 + 文字列 + 自由形式のフィールド。PCB上のコンポーネントごとの一意のリファレンス識別子。例: 抵抗 "R1"。
                                      アセンブリ内の部品エントリの配置名として使用。 + + + Package + 任意 + 文字列 + 自由形式のフィールド。コンポーネントのパッケージまたはフォームファクタ。例: 表面実装抵抗器 "0805"。
                                      アセンブリ内の部品エントリ情報には含まれません。 + + + Quantity + 必須 + 整数 + アセンブリの一つのインスタンスを作るために必要な同一部品の数。
                                      アセンブリの部品情報で数量として使用。 + + + Designation + 必須 + 文字列 + コンポーネントの説明や機能。例: 抵抗の値 "10kΩ" やコンデンサの値 "100nF"。
                                      アセンブリの部品情報で名称として使用。 + + + Supplier and ref + 任意 + 文字列 + 自由形式フィールド。例: 供給元に関する特定の情報を含む場合がある。
                                      アセンブリの部品情報の注記として使用。 + + + + ]]> +
                                      +
                                      diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index f98e9c784..7cca59753 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1150,6 +1150,12 @@ JSON voor assemblage + + + assembly.bom_import.type.csv + CSV voor een assemblage + + assembly.bom_import.type.kicad_pcbnew @@ -1174,6 +1180,12 @@ JSON-sjabloon voor assemblage + + + assembly.import_bom.template.header.csv + CSV-importsjabloon voor een assemblage + + assembly.import_bom.template.header.kicad_pcbnew @@ -1235,22 +1247,24 @@ quantity Verplicht veld Kommagetal (Float) - Moet opgegeven zijn en bevat een kommagetal (Float) dat groter is dan 0,0. + Moet worden ingevuld en een kommagetal (Float) bevatten dat groter is dan 0,0. name Optioneel - String - Indien aanwezig, moet het een niet-lege string zijn. + Tekst + Wanneer ingevuld, moet het een niet-lege tekst zijn. De naam van het item binnen de assemblage. part Optioneel Object/Array - Indien opgegeven, moet het een object/array zijn en ten minste één van de velden ingevuld zijn: + Als een onderdeel moet worden toegewezen, moet dit een object/array zijn en moet ten minste één van de volgende velden worden ingevuld:
                                      • part.id
                                      • +
                                      • part.mpnr
                                      • +
                                      • part.ipn
                                      • part.name
                                      @@ -1258,39 +1272,39 @@ part.id Optioneel - Geheel getal (Integer) - Geheel getal (Integer) > 0. Komt overeen met de interne numerieke ID van het onderdeel in de Part-DB. + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van het onderdeel in de database. - part.name + part.mpnr Optioneel - String - Niet-lege string, indien geen part.mpnr- of part.ipn-vermelding is gegeven. + Tekst + Niet-lege tekst, wanneer part.id, part.ipn of part.name niet zijn ingevuld. - part.mpnr + part.ipn Optioneel - String - Niet-lege string, indien geen part.name- of part.ipn-vermelding is gegeven. + Tekst + Niet-lege tekst wanneer part.id, part.mpnr of part.name niet zijn ingevuld. - part.ipn + part.name Optioneel - String - Niet-lege string, indien geen part.name- of part.mpnr-vermelding is gegeven. + Tekst + Niet-lege tekst, wanneer part.id, part.mpnr of part.ipn niet zijn ingevuld. part.description Optioneel - String of null - Indien aanwezig, moet het een niet-lege string zijn of null. + Tekst of null + Indien ingevuld, moet het een niet-lege tekst of null zijn. Deze waarde vervangt de bestaande waarde in het onderdeel. part.manufacturer Optioneel Object/Array - Indien aanwezig, moet het een object/array zijn en ten minste één van de velden ingevuld zijn: + Als de fabrikant van het onderdeel moet worden gewijzigd of uniek moet worden opgezocht met de waarde part.mpnr, moet dit een object/array zijn en moet ten minste één van de volgende velden worden ingevuld:
                                      • manufacturer.id
                                      • manufacturer.name
                                      • @@ -1300,21 +1314,21 @@ manufacturer.id Optioneel - Geheel getal (Integer) - Geheel getal (Integer) > 0. Komt overeen met de interne numerieke ID van de fabrikant. + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van de fabrikant. manufacturer.name Optioneel - String - Niet-lege string, indien geen manufacturer.id-vermelding is gegeven. + Tekst + Niet-lege tekst als manufacturer.id niet is ingevuld. part.category Optioneel Object/Array - Indien aanwezig, moet het een object/array zijn en ten minste één van de velden ingevuld zijn: + Als de categorie van het onderdeel moet worden gewijzigd, moet dit een object/array zijn en moet ten minste één van de volgende velden worden ingevuld:
                                        • category.id
                                        • category.name
                                        • @@ -1324,14 +1338,139 @@ category.id Optioneel - Geheel getal (Integer) - Geheel getal (Integer) > 0. Komt overeen met de interne numerieke ID van de categorie van het onderdeel. + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van de categorie van het onderdeel. category.name Optioneel - String - Niet-lege string, indien geen category.id-vermelding is gegeven. + Tekst + Niet-lege tekst als category.id niet is ingevuld. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Mogelijke kolommen: + + + + + assembly.bom_import.template.csv.table + + + + + Kolom + Voorwaarde + Gegevenstype + Beschrijving + + + + + quantity + Verplicht veld + Kommagetal (Float) + Moet worden ingevuld en moet een kommagetal (Float) bevatten dat groter is dan 0,0. + + + name + Optioneel + Tekst + De naam van het item binnen de assemblage. + + + Kolommen die beginnen met part_ + + Als een onderdeel moet worden toegewezen, moet ten minste één van de volgende kolommen worden ingevuld: +
                                            +
                                          • part_id
                                          • +
                                          • part_mpnr
                                          • +
                                          • part_ipn
                                          • +
                                          • part_name
                                          • +
                                          + + + + part_id + Optioneel + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van het onderdeel in de database. + + + part_mpnr + Optioneel + Tekst + Moet worden ingevuld als part_id, part_ipn of part_name niet zijn ingevuld. + + + part_ipn + Optioneel + Tekst + Moet worden ingevuld als part_id, part_mpnr of part_name niet zijn ingevuld. + + + part_name + Optioneel + Tekst + Moet worden ingevuld als part_id, part_mpnr of part_ipn niet zijn ingevuld. + + + part_description + Optioneel + Tekst + Wordt overgenomen en vervangt de bestaande waarde van de beschrijving als er een niet-lege tekst wordt opgegeven. + + + Kolommen die beginnen met part_manufacturer_ + + Als de fabrikant van een onderdeel moet worden gewijzigd of uniek moet worden gezocht via part_mpnr, moet ten minste één van de volgende kolommen worden ingevuld: +
                                            +
                                          • part_manufacturer_id
                                          • +
                                          • part_manufacturer_name
                                          • +
                                          + + + + part_manufacturer_id + Optioneel + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van de fabrikant. + + + part_manufacturer_name + Optioneel + Tekst + Moet worden ingevuld als part_manufacturer_id niet wordt opgegeven. + + + Kolommen die beginnen met part_category_ + + Als de categorie van een onderdeel moet worden gewijzigd, moet ten minste één van de volgende kolommen worden ingevuld: +
                                            +
                                          • part_category_id
                                          • +
                                          • part_category_name
                                          • +
                                          + + + + part_category_id + Optioneel + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van de categorie van het onderdeel. + + + part_category_name + Optioneel + Tekst + Moet worden ingevuld als part_category_id niet wordt opgegeven. @@ -1346,71 +1485,71 @@
                                          - - assembly.bom_import.template.kicad_pcbnew.exptected_columns.note - - Opmerking: Er wordt geen mapping uitgevoerd met specifieke componenten uit de categoriebeheer.

                                          - ]]> -
                                          + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Opmerking: Er wordt geen mapping uitgevoerd met specifieke componenten uit de categoriebeheer.

                                          + ]]> +
                                          - - assembly.bom_import.template.kicad_pcbnew.table - - - - - Veld - Voorwaarde - Gegevenstype - Beschrijving - - - - - Id - Optioneel - Integer - Vrij veld. Een unieke identificatienummer voor elk onderdeel. - - - Designator - Optioneel - String - Vrij veld. Een unieke referentienaam voor het onderdeel op de PCB, bijvoorbeeld "R1" voor weerstand 1.
                                          Wordt gebruikt als positioneringsnaam in de onderdelenlijst van de assemblage. - - - Package - Optioneel - String - Vrij veld. De behuizing of vormfactor van het onderdeel, bijvoorbeeld "0805" voor SMD-weerstanden.
                                          Wordt niet opgenomen in de onderdelenlijst binnen de assemblage. - - - Quantity - Vereist - Integer - Het aantal identieke onderdelen dat nodig is om een assemblage-instantie te creëren.
                                          Wordt gebruikt als hoeveelheid in de onderdelenlijst binnen de assemblage. - - - Designation - Vereist - String - De beschrijving of functie van het onderdeel, zoals weerstandwaarde "10kΩ" of condensatorwaarde "100nF".
                                          Wordt gebruikt als de naam in de onderdelenlijst binnen de assemblage. - - - Supplier and ref - Optioneel - String - Vrij veld. Kan bijvoorbeeld specifieke informatie over leveranciers bevatten.
                                          Wordt gebruikt als notitie in de onderdelenlijst binnen de assemblage. - - - - ]]> -
                                          -
                                          + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Veld + Voorwaarde + Gegevenstype + Beschrijving + + + + + Id + Optioneel + Integer + Vrij veld. Een unieke identificatienummer voor elk onderdeel. + + + Designator + Optioneel + String + Vrij veld. Een unieke referentienaam voor het onderdeel op de PCB, bijvoorbeeld "R1" voor weerstand 1.
                                          Wordt gebruikt als positioneringsnaam in de onderdelenlijst van de assemblage. + + + Package + Optioneel + String + Vrij veld. De behuizing of vormfactor van het onderdeel, bijvoorbeeld "0805" voor SMD-weerstanden.
                                          Wordt niet opgenomen in de onderdelenlijst binnen de assemblage. + + + Quantity + Vereist + Integer + Het aantal identieke onderdelen dat nodig is om een assemblage-instantie te creëren.
                                          Wordt gebruikt als hoeveelheid in de onderdelenlijst binnen de assemblage. + + + Designation + Vereist + String + De beschrijving of functie van het onderdeel, zoals weerstandwaarde "10kΩ" of condensatorwaarde "100nF".
                                          Wordt gebruikt als de naam in de onderdelenlijst binnen de assemblage. + + + Supplier and ref + Optioneel + String + Vrij veld. Kan bijvoorbeeld specifieke informatie over leveranciers bevatten.
                                          Wordt gebruikt als notitie in de onderdelenlijst binnen de assemblage. + + + + ]]> +
                                          +
                                          diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index d1a095072..86894d998 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -12613,6 +12613,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli JSON dla zespołu + + + assembly.bom_import.type.csv + CSV dla zestawienia + + assembly.bom_import.type.kicad_pcbnew @@ -12637,6 +12643,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Szablon importu JSON dla zespołu + + + assembly.import_bom.template.header.csv + Szablon importu CSV dla zespołu + + assembly.import_bom.template.header.kicad_pcbnew @@ -12696,24 +12708,26 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli quantity - Wymagane - Typ zmiennoprzecinkowy (Float) - Musi być obecne i zawierać wartość zmiennoprzecinkową (Float) większą niż 0,0. + Pole obowiązkowe + Liczba zmiennoprzecinkowa (Float) + Musi być wypełnione i zawierać liczbę zmiennoprzecinkową (Float) większą niż 0,0. name Opcjonalne - Ciąg znaków (String) - Jeśli obecne, musi być niepustym ciągiem znaków. + Tekst + Jeśli określono, musi to być niepusty tekst. Nazwa elementu w ramach montażu. part Opcjonalne Obiekt/Tablica - Jeśli podane, musi być obiektem/tablicą i co najmniej jedno z poniższych pól musi być wypełnione: + Jeśli konieczne jest przypisanie części, musi to być Obiekt/Tablica, a przynajmniej jedno z poniższych pól powinno być wypełnione:
                                          • part.id
                                          • +
                                          • part.mpnr
                                          • +
                                          • part.ipn
                                          • part.name
                                          @@ -12721,39 +12735,39 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli part.id Opcjonalne - Liczba całkowita (Integer) - Liczba całkowita (Integer) > 0. Odpowiada wewnętrznemu numerowi ID komponentu w Part-DB. + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID części w bazie danych. - part.name + part.mpnr Opcjonalne - Ciąg znaków (String) - Niepusty ciąg znaków, jeśli part.mpnr ani part.ipn nie są podane. + Tekst + Niepusty tekst, jeśli pola part.id, part.ipn lub part.name nie są wypełnione. - part.mpnr + part.ipn Opcjonalne - Ciąg znaków (String) - Niepusty ciąg znaków, jeśli part.name ani part.ipn nie są podane. + Tekst + Niepusty tekst, jeśli pola part.id, part.mpnr lub part.name nie są wypełnione. - part.ipn + part.name Opcjonalne - Ciąg znaków (String) - Niepusty ciąg znaków, jeśli part.name ani part.mpnr nie są podane. + Tekst + Niepusty tekst, jeśli pola part.id, part.mpnr lub part.ipn nie są wypełnione. part.description Opcjonalne - Ciąg znaków lub null - Jeśli obecne, musi być niepustym ciągiem znaków lub null. + Tekst lub null + Jeśli określono, musi to być niepusty tekst lub null. Ta wartość nadpisze istniejącą wartość w części. part.manufacturer Opcjonalne Obiekt/Tablica - Jeśli obecne, musi być obiektem/tablicą i co najmniej jedno z poniższych pól musi być wypełnione: + Jeśli producent części ma zostać zmieniony lub wyszukany unikalnie za pomocą wartości part.mpnr, musi to być Obiekt/Tablica i przynajmniej jedno z poniższych pól powinno być wypełnione:
                                          • manufacturer.id
                                          • manufacturer.name
                                          • @@ -12763,21 +12777,21 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli manufacturer.id Opcjonalne - Liczba całkowita (Integer) - Liczba całkowita (Integer) > 0. Odpowiada wewnętrznemu identyfikatorowi numerowemu producenta. + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID producenta. manufacturer.name Opcjonalne - Ciąg znaków (String) - Niepusty ciąg znaków, jeśli manufacturer.id nie jest podane. + Tekst + Niepusty tekst, jeśli pole manufacturer.id nie jest wypełnione. part.category Opcjonalne Obiekt/Tablica - Jeśli obecne, musi być obiektem/tablicą i co najmniej jedno z poniższych pól musi być wypełnione: + Jeśli konieczna jest zmiana kategorii części, musi to być Obiekt/Tablica i przynajmniej jedno z poniższych pól powinno być wypełnione:
                                            • category.id
                                            • category.name
                                            • @@ -12787,14 +12801,139 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli category.id Opcjonalne - Liczba całkowita (Integer) - Liczba całkowita (Integer) > 0. Odpowiada wewnętrznemu numerowi ID kategorii komponentu. + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID kategorii części. category.name Opcjonalne - Ciąg znaków (String) - Niepusty ciąg znaków, jeśli category.id nie jest podane. + Tekst + Niepusty tekst, jeśli pole category.id nie jest wypełnione. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Możliwe kolumny: + + + + + assembly.bom_import.template.csv.table + + + + + Kolumna + Warunek + Typ danych + Opis + + + + + quantity + Pole obowiązkowe + Liczba zmiennoprzecinkowa (Float) + Musi być wypełnione i zawierać liczbę zmiennoprzecinkową (Float) większą niż 0,0. + + + name + Opcjonalne + Tekst + Nazwa elementu w ramach montażu. + + + Kolumny zaczynające się od part_ + + Jeśli konieczne jest przypisanie części, przynajmniej jedna z poniższych kolumn powinna być wypełniona: +
                                                +
                                              • part_id
                                              • +
                                              • part_mpnr
                                              • +
                                              • part_ipn
                                              • +
                                              • part_name
                                              • +
                                              + + + + part_id + Opcjonalne + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID części w bazie danych. + + + part_mpnr + Opcjonalne + Tekst + Musi być wypełnione, gdy kolumny part_id, part_ipn lub part_name nie są wypełnione. + + + part_ipn + Opcjonalne + Tekst + Musi być wypełnione, gdy kolumny part_id, part_mpnr lub part_name nie są wypełnione. + + + part_name + Opcjonalne + Tekst + Musi być wypełnione, gdy kolumny part_id, part_mpnr lub part_ipn nie są wypełnione. + + + part_description + Opcjonalne + Tekst + Zostanie przeniesione i zastąpi istniejącą wartość opisu, jeśli określono niepusty tekst. + + + Kolumny zaczynające się od part_manufacturer_ + + Jeśli producent części musi zostać zmieniony lub wyszukany unikalnie za pomocą part_mpnr, przynajmniej jedna z poniższych kolumn powinna być wypełniona: +
                                                +
                                              • part_manufacturer_id
                                              • +
                                              • part_manufacturer_name
                                              • +
                                              + + + + part_manufacturer_id + Opcjonalne + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID producenta. + + + part_manufacturer_name + Opcjonalne + Tekst + Musi być wypełnione, jeśli part_manufacturer_id nie jest określony. + + + Kolumny zaczynające się od part_category_ + + Jeśli konieczna jest zmiana kategorii części, przynajmniej jedna z poniższych kolumn powinna być wypełniona: +
                                                +
                                              • part_category_id
                                              • +
                                              • part_category_name
                                              • +
                                              + + + + part_category_id + Opcjonalne + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID kategorii części. + + + part_category_name + Opcjonalne + Tekst + Musi być wypełnione, jeśli part_category_id nie jest określone. @@ -12809,71 +12948,71 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli
                                              - - assembly.bom_import.template.kicad_pcbnew.exptected_columns.note - - Uwaga: Nie wykonano mapowania z określonymi komponentami z zarządzania kategoriami.

                                              - ]]> -
                                              -
                                              + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Uwaga: Nie wykonano mapowania z określonymi komponentami z zarządzania kategoriami.

                                              + ]]> +
                                              +
                                              - - assembly.bom_import.template.kicad_pcbnew.table - - - - - Pole - Warunek - Typ Danych - Opis - - - - - Id - Opcjonalne - Liczba całkowita - Pole dowolne. Unikalny numer identyfikacyjny dla każdego komponentu. - - - Designator - Opcjonalne - Tekst - Pole dowolne. Jednoznaczny znacznik referencyjny komponentu na PCB, np. "R1" dla rezystora 1.
                                              Używane jako nazwa pozycji w pozycji komponentu w montażu. - - - Package - Opcjonalne - Tekst - Pole dowolne. Obudowa lub forma komponentu, np. "0805" dla rezystorów SMD.
                                              Niewykorzystywane w pozycji komponentu w montażu. - - - Quantity - Wymagane - Liczba całkowita - Liczba identycznych komponentów potrzebna do utworzenia jednej instancji montażu.
                                              Używane jako ilość w pozycji komponentu w montażu. - - - Designation - Wymagane - Tekst - Opis lub funkcja komponentu, np. wartość rezystora "10kΩ" lub wartość kondensatora "100nF".
                                              Używane jako nazwa w pozycji komponentu w montażu. - - - Supplier and ref - Opcjonalne - Tekst - Pole dowolne. Może zawierać np. specyficzne informacje o dostawcy.
                                              Używane jako notatka w pozycji komponentu w montażu. - - - - ]]> -
                                              -
                                              + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Pole + Warunek + Typ Danych + Opis + + + + + Id + Opcjonalne + Liczba całkowita + Pole dowolne. Unikalny numer identyfikacyjny dla każdego komponentu. + + + Designator + Opcjonalne + Tekst + Pole dowolne. Jednoznaczny znacznik referencyjny komponentu na PCB, np. "R1" dla rezystora 1.
                                              Używane jako nazwa pozycji w pozycji komponentu w montażu. + + + Package + Opcjonalne + Tekst + Pole dowolne. Obudowa lub forma komponentu, np. "0805" dla rezystorów SMD.
                                              Niewykorzystywane w pozycji komponentu w montażu. + + + Quantity + Wymagane + Liczba całkowita + Liczba identycznych komponentów potrzebna do utworzenia jednej instancji montażu.
                                              Używane jako ilość w pozycji komponentu w montażu. + + + Designation + Wymagane + Tekst + Opis lub funkcja komponentu, np. wartość rezystora "10kΩ" lub wartość kondensatora "100nF".
                                              Używane jako nazwa w pozycji komponentu w montażu. + + + Supplier and ref + Opcjonalne + Tekst + Pole dowolne. Może zawierać np. specyficzne informacje o dostawcy.
                                              Używane jako notatka w pozycji komponentu w montażu. + + + + ]]> +
                                              +
                                              diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 429a82eca..ac04ed290 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -12713,6 +12713,12 @@ JSON для сборки + + + assembly.bom_import.type.csv + CSV для сборки + + assembly.bom_import.type.kicad_pcbnew @@ -12737,6 +12743,12 @@ Шаблон импорта JSON для сборки + + + assembly.import_bom.template.header.csv + Шаблон импорта CSV для сборки + + assembly.import_bom.template.header.kicad_pcbnew @@ -12796,64 +12808,66 @@ quantity - Обязательное - Дробное число (Float) - Поле должно быть заполнено и содержать дробное значение (Float), большее 0,0. + Обязательное поле + Число с плавающей точкой (Float) + Должно быть заполнено и содержать число с плавающей точкой (Float) больше 0,0. name - Опциональное - Строка (String) - Если присутствует, должно быть непустой строкой. + Необязательное + Строка + Если указано, должно быть непустой строкой. Имя элемента внутри сборки. part - Опциональное + Необязательное Объект/Массив - Если указано, должно быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: + Если необходимо назначить деталь, это должен быть объект/массив, и должно быть заполнено хотя бы одно из следующих полей:
                                              • part.id
                                              • +
                                              • part.mpnr
                                              • +
                                              • part.ipn
                                              • part.name
                                              part.id - Опциональное - Целое число (Integer) - Целое число (Integer) > 0. Соответствует внутреннему цифровому идентификатору компонента в Part-DB. - - - part.name - Опциональное - Строка (String) - Непустая строка, если part.mpnr или part.ipn не указаны. + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому идентификатору детали в базе данных. part.mpnr - Опциональное - Строка (String) - Непустая строка, если part.name или part.ipn не указаны. + Необязательное + Строка + Непустая строка, если part.id, part.ipn или part.name не указаны. part.ipn - Опциональное - Строка (String) - Непустая строка, если part.name или part.mpnr не указаны. + Необязательное + Строка + Непустая строка, если part.id, part.mpnr или part.name не указаны. + + + part.name + Необязательное + Строка + Непустая строка, если part.id, part.mpnr или part.ipn не указаны. part.description - Опциональное + Необязательное Строка или null - Если присутствует, должно быть непустой строкой или null. + Если указано, должно быть непустой строкой или null. Это значение перезаписывает существующее значение в детали. part.manufacturer - Опциональное + Необязательное Объект/Массив - Если присутствует, должно быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: + Если необходимо изменить производителя детали или уникально найти по значению part.mpnr, это должен быть объект/массив, и должно быть заполнено хотя бы одно из следующих полей:
                                              • manufacturer.id
                                              • manufacturer.name
                                              • @@ -12862,22 +12876,22 @@ manufacturer.id - Опциональное - Целое число (Integer) - Целое число (Integer) > 0. Соответствует внутреннему идентификатору производителя. + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому идентификатору производителя. manufacturer.name - Опциональное - Строка (String) + Необязательное + Строка Непустая строка, если manufacturer.id не указано. part.category - Опциональное + Необязательное Объект/Массив - Если присутствует, должно быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: + Если необходимо изменить категорию детали, это должен быть объект/массив, и должно быть заполнено хотя бы одно из следующих полей:
                                                • category.id
                                                • category.name
                                                • @@ -12886,14 +12900,14 @@ category.id - Опциональное - Целое число (Integer) - Целое число (Integer) > 0. Соответствует внутреннему цифровому идентификатору категории компонента. + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому идентификатору категории детали. category.name - Опциональное - Строка (String) + Необязательное + Строка Непустая строка, если category.id не указано. @@ -12902,6 +12916,131 @@ + + + assembly.bom_import.template.csv.exptected_columns + Возможные столбцы: + + + + + assembly.bom_import.template.csv.table + + + + + Столбец + Условие + Тип данных + Описание + + + + + quantity + Обязательное поле + Число с плавающей точкой (Float) + Должно быть заполнено и содержать число с плавающей точкой (Float), больше 0,0. + + + name + Необязательное + Строка + Название элемента в рамках сборки. + + + Столбцы, начинающиеся с part_ + + Если необходимо назначить деталь, то хотя бы один из следующих столбцов должен быть заполнен: +
                                                    +
                                                  • part_id
                                                  • +
                                                  • part_mpnr
                                                  • +
                                                  • part_ipn
                                                  • +
                                                  • part_name
                                                  • +
                                                  + + + + part_id + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому ID детали в базе данных. + + + part_mpnr + Необязательное + Строка + Должно быть заполнено, если part_id, part_ipn или part_name не указаны. + + + part_ipn + Необязательное + Строка + Должно быть заполнено, если part_id, part_mpnr или part_name не указаны. + + + part_name + Необязательное + Строка + Должно быть заполнено, если part_id, part_mpnr или part_ipn не указаны. + + + part_description + Необязательное + Строка + Если указано, заменяет существующее значение описания деталя не пустой строкой. + + + Столбцы, начинающиеся с part_manufacturer_ + + Если необходимо указать производителя детали или найти деталь уникально по part_mpnr, должно быть заполнено хотя бы одно из следующих полей: +
                                                    +
                                                  • part_manufacturer_id
                                                  • +
                                                  • part_manufacturer_name
                                                  • +
                                                  + + + + part_manufacturer_id + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому ID производителя. + + + part_manufacturer_name + Необязательное + Строка + Должно быть заполнено, если part_manufacturer_id не указано. + + + Столбцы, начинающиеся с part_category_ + + Если необходимо изменить категорию детали, должно быть заполнено хотя бы одно из следующих полей: +
                                                    +
                                                  • part_category_id
                                                  • +
                                                  • part_category_name
                                                  • +
                                                  + + + + part_category_id + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому ID категории детали. + + + part_category_name + Необязательное + Строка + Должно быть заполнено, если part_category_id не указано. + + + + ]]> +
                                                  +
                                                  +
                                                  assembly.bom_import.template.kicad_pcbnew.exptected_columns @@ -12909,71 +13048,71 @@ - - assembly.bom_import.template.kicad_pcbnew.exptected_columns.note - - Примечание: Сопоставление с конкретными компонентами из управления категориями не выполняется.

                                                  - ]]> -
                                                  -
                                                  + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Примечание: Сопоставление с конкретными компонентами из управления категориями не выполняется.

                                                  + ]]> +
                                                  +
                                                  - - assembly.bom_import.template.kicad_pcbnew.table - - - - - Поле - Условие - Тип данных - Описание - - - - - Id - Опционально - Целое - Свободное поле. Уникальный идентификационный номер для каждого компонента. - - - Package - Designator - Строка - Свободное поле. Уникальная ссылочная метка компонента на печатной плате, например, "R1" для резистора 1.
                                                  Используется как наименование позиции в компоненте сборки. - - - Package - Опционально - Строка - Свободное поле. Тип корпуса или форм-фактор компонента, например, "0805" для SMD-резисторов.
                                                  Не включается в информацию о компоненте сборки. - - - Quantity - Обязательно - Целое - Количество одинаковых компонентов, необходимых для создания одной версии сборки.
                                                  Используется как количество в информации о компоненте сборки. - - - Designation - Обязательно - Строка - Описание или функция компонента, например, значение резистора "10kΩ" или значение конденсатора "100nF".
                                                  Используется как наименование в информации о компоненте сборки. - - - Supplier and ref - Опционально - Строка - Свободное поле. Может содержать, например, информацию о конкретных поставщиках.
                                                  Используется как примечание в информации о компоненте сборки. - - - - ]]> -
                                                  -
                                                  + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Поле + Условие + Тип данных + Описание + + + + + Id + Опционально + Целое + Свободное поле. Уникальный идентификационный номер для каждого компонента. + + + Package + Designator + Строка + Свободное поле. Уникальная ссылочная метка компонента на печатной плате, например, "R1" для резистора 1.
                                                  Используется как наименование позиции в компоненте сборки. + + + Package + Опционально + Строка + Свободное поле. Тип корпуса или форм-фактор компонента, например, "0805" для SMD-резисторов.
                                                  Не включается в информацию о компоненте сборки. + + + Quantity + Обязательно + Целое + Количество одинаковых компонентов, необходимых для создания одной версии сборки.
                                                  Используется как количество в информации о компоненте сборки. + + + Designation + Обязательно + Строка + Описание или функция компонента, например, значение резистора "10kΩ" или значение конденсатора "100nF".
                                                  Используется как наименование в информации о компоненте сборки. + + + Supplier and ref + Опционально + Строка + Свободное поле. Может содержать, например, информацию о конкретных поставщиках.
                                                  Используется как примечание в информации о компоненте сборки. + + + + ]]> +
                                                  +
                                                  diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 7e90daa28..426566225 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -12598,6 +12598,12 @@ Element 3 JSON 文件(组件) + + + assembly.bom_import.type.csv + 装配的CSV + + assembly.bom_import.type.kicad_pcbnew @@ -12622,6 +12628,12 @@ Element 3 装配 JSON 导入模板 + + + assembly.import_bom.template.header.csv + 用于装配的CSV导入模板 + + assembly.import_bom.template.header.kicad_pcbnew @@ -12681,24 +12693,26 @@ Element 3 quantity - 必填 + 必填字段 浮点数 (Float) - 必须存在,并包含大于 0.0 的浮点值 (Float)。 + 必须填写且包含大于 0.0 的浮点数 (Float)。 name 可选 - 字符串 (String) - 如果存在,必须是非空字符串。 + 文本 + 如果填写,必须是非空文本。表示装配中的项目名称。 part 可选 对象/数组 - 如果提供,则必须是对象/数组,并且以下字段中至少有一个被填写: + 如果需要分配零件,它必须是一个对象/数组,并且至少需要填写以下字段之一:
                                                  • part.id
                                                  • +
                                                  • part.mpnr
                                                  • +
                                                  • part.ipn
                                                  • part.name
                                                  @@ -12706,39 +12720,39 @@ Element 3 part.id 可选 - 整数 (Integer) - 整数 (Integer) > 0。表示组件在 Part-DB 中的内部数字 ID。 + 整数 + 一个大于 0 的整数。对应数据库中零件的内部数字ID。 - part.name + part.mpnr 可选 - 字符串 (String) - 如果未提供 part.mpnr 或 part.ipn,则必须是非空字符串。 + 文本 + 如果 part.id、part.ipn 或 part.name 未填写,则必须是非空文本。 - part.mpnr + part.ipn 可选 - 字符串 (String) - 如果未提供 part.name 或 part.ipn,则必须是非空字符串。 + 文本 + 如果 part.id、part.mpnr 或 part.name 未填写,则必须是非空文本。 - part.ipn + part.name 可选 - 字符串 (String) - 如果未提供 part.name 或 part.mpnr,则必须是非空字符串。 + 文本 + 如果 part.id、part.mpnr 或 part.ipn 未填写,则必须是非空文本。 part.description 可选 - 字符串或 null - 如果存在,必须是非空字符串或 null。 + 文本或 null + 如果填写,必须是非空文本或 null。该值将替换零件中的现有值。 part.manufacturer 可选 对象/数组 - 如果存在,则必须是对象/数组,并且以下字段中至少有一个被填写: + 如果需要更改零件制造商或通过值 part.mpnr 唯一查找零件,它必须是一个对象/数组,并且至少需要填写以下字段之一:
                                                  • manufacturer.id
                                                  • manufacturer.name
                                                  • @@ -12748,21 +12762,21 @@ Element 3 manufacturer.id 可选 - 整数 (Integer) - 整数 (Integer) > 0。表示制造商的内部数字 ID。 + 整数 + 一个大于 0 的整数。对应制造商的内部数字 ID。 manufacturer.name 可选 - 字符串 (String) - 如果未提供 manufacturer.id,则必须是非空字符串。 + 文本 + 如果 manufacturer.id 未填写,则必须是非空文本。 part.category 可选 对象/数组 - 如果存在,则必须是对象/数组,并且以下字段中至少有一个被填写: + 如果需要更改零件的类别,它必须是一个对象/数组,并且至少需要填写以下字段之一:
                                                    • category.id
                                                    • category.name
                                                    • @@ -12772,14 +12786,139 @@ Element 3 category.id 可选 - 整数 (Integer) - 整数 (Integer) > 0。表示组件类别的内部数字 ID。 + 整数 + 一个大于 0 的整数。对应零件类别的内部数字 ID。 category.name 可选 - 字符串 (String) - 如果未提供 category.id,则必须是非空字符串。 + 文本 + 如果 category.id 未填写,则必须是非空文本。 + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + 可用列: + + + + + assembly.bom_import.template.csv.table + + + + + 列名 + 条件 + 数据类型 + 描述 + + + + + quantity + 必填字段 + 浮点数 (Float) + 必须填写且包含大于 0.0 的浮点数 (Float)。 + + + name + 可选 + 文本 + 装配中的项目名称。 + + + 以 part_ 开头的列 + + 如果需要分配零件,则至少需要填写以下列之一: +
                                                        +
                                                      • part_id
                                                      • +
                                                      • part_mpnr
                                                      • +
                                                      • part_ipn
                                                      • +
                                                      • part_name
                                                      • +
                                                      + + + + part_id + 可选 + 整数 + 一个大于 0 的整数。对应数据库中零件的内部数字 ID。 + + + part_mpnr + 可选 + 文本 + 如果 part_id、part_ipn 或 part_name 未填写,则必须是非空文本。 + + + part_ipn + 可选 + 文本 + 如果 part_id、part_mpnr 或 part_name 未填写,则必须是非空文本。 + + + part_name + 可选 + 文本 + 如果 part_id、part_mpnr 或 part_ipn 未填写,则必须是非空文本。 + + + part_description + 可选 + 文本 + 如果指定,将取代现有描述值,且必须为非空文本。 + + + 以 part_manufacturer_ 开头的列 + + 如果需要更改零件的制造商或通过 part_mpnr 唯一查找,至少需要填写以下列之一: +
                                                        +
                                                      • part_manufacturer_id
                                                      • +
                                                      • part_manufacturer_name
                                                      • +
                                                      + + + + part_manufacturer_id + 可选 + 整数 + 一个大于 0 的整数。对应制造商的内部数字 ID。 + + + part_manufacturer_name + 可选 + 文本 + 如果 part_manufacturer_id 未填写,则必须是非空文本。 + + + 以 part_category_ 开头的列 + + 如果需要更改零件的类别,则至少需要填写以下列之一: +
                                                        +
                                                      • part_category_id
                                                      • +
                                                      • part_category_name
                                                      • +
                                                      + + + + part_category_id + 可选 + 整数 + 一个大于 0 的整数。对应零件类别的内部数字 ID。 + + + part_category_name + 可选 + 文本 + 如果 part_category_id 未填写,则必须是非空文本。 diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 46471d02d..699b5d2f3 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -371,6 +371,18 @@ Neplatný kód. Zkontrolujte, zda je vaše ověřovací aplikace správně nastavena a zda je čas správně nastaven jak na serveru, tak na ověřovacím zařízení.
                                                      + + + validator.bom_importer.invalid_import_type + Neplatný typ importu! + + + + + validator.bom_importer.invalid_file_extension + Neplatná přípona souboru "%extension%" pro typ importu "%importType%". Povolené přípony souborů: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -389,57 +401,57 @@ Musíte vybrat součást nebo nastavit název pro nesoučást! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Musíte zadat množství > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float očekává se jako float větší než 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty očekává se jako neprázdný řetězec - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null očekává se jako neprázdný řetězec nebo null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array očekává se jako pole (array) - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties musí mít alespoň jeden z následujících pod-parametrů: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor nenalezeno pro %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch se přesně neshoduje. Pro import zadáno: %importValue%, nalezeno (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties musí obsahovat jako pod-parametr buď: "id" jako celé číslo větší než 0 nebo "name" jako neprázdný řetězec diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 88ec6784b..056871bbf 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -347,6 +347,18 @@ Denne leverandørstregkodeværdi er allerede brugt til en anden beholdning. Stregkoden skal være unik! + + + validator.bom_importer.invalid_import_type + Ugyldig importtype! + + + + + validator.bom_importer.invalid_file_extension + Ugyldig filtypenavn "%extension%" for importtypen "%importType%". Tilladte filtypenavne: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -365,57 +377,57 @@ Du skal vælge en del eller sætte et navn for en ikke-del! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Du skal angive en mængde > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float forventet som en float større end 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty forventet som en ikke-tom streng - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null forventet som en ikke-tom streng eller null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array forventet som en array - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties skal have mindst én af følgende underparametre: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor ikke fundet for %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch stemmer ikke helt overens. Givet til import: %importValue%, fundet (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties skal indeholde som en underparameter enten: "id" som et heltal større end 0 eller "name" som en ikke-tom streng diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index cb710ac97..8771a0e65 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -371,6 +371,18 @@ Ungültiger Code. Überprüfen Sie, dass die Authenticator App korrekt eingerichtet ist und dass der Server und das Gerät beide die korrekte Uhrzeit eingestellt haben. + + + validator.bom_importer.invalid_import_type + Ungültiger Importtyp! + + + + + validator.bom_importer.invalid_file_extension + Ungültige Dateierweiterung "%extension%" für den Importtyp "%importType%". Erlaubte Dateierweiterungen: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -389,57 +401,57 @@ Sie müssen ein Bauteil auswählen, oder einen Namen für den Eintrag setzen! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Sie müssen eine Stückzahl > 0 angeben! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float wird als float größer als 0.0 erwartet - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty als nicht leere Zeichenkette erwartet - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null als nicht leere Zeichenkette oder null erwartet - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array als array erwartet - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties muss mindestens eines der folgenden Unter-Parameter haben: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor nicht gefunden für %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch stimmt nicht genau überein. Für den Import gegeben: %importValue%, gefunden (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties muss entweder als Unter-Parameter zugewiesen haben: "id" als Ganzzahl größer als 0 oder "name" als nicht leere Zeichenfolge diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index 318ba8928..bb78c7994 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -13,6 +13,18 @@ Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! + + + validator.bom_importer.invalid_import_type + Μη έγκυρος τύπος εισαγωγής! + + + + + validator.bom_importer.invalid_file_extension + Μη έγκυρη επέκταση αρχείου "%extension%" για τον τύπο εισαγωγής "%importType%". Επιτρεπόμενες επεκτάσεις αρχείων: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -31,57 +43,57 @@ Πρέπει να επιλέξετε ένα εξάρτημα ή να βάλετε ένα όνομα για ένα μη εξάρτημα! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Πρέπει να εισαγάγετε ποσότητα > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float αναμένεται ως δεκαδικός αριθμός (float) μεγαλύτερος από 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty αναμένεται ως μη κενή συμβολοσειρά - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null αναμένεται ως μη κενή συμβολοσειρά ή null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array αναμένεται ως array - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties πρέπει να έχει τουλάχιστον μία από τις ακόλουθες υπο-παραμέτρους: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor δεν βρέθηκε για %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch δεν ταιριάζει απόλυτα. Δόθηκε για εισαγωγή: %importValue%, βρέθηκε (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties πρέπει να περιέχει ως υπο-παράμετρο είτε: "id" ως ακέραιο αριθμό μεγαλύτερο από 0 είτε "name" ως μη κενή συμβολοσειρά diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 93640fff7..4c53ed187 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -371,6 +371,18 @@ Invalid code. Check that your authenticator app is set up correctly and that both the server and authentication device has the time set correctly. + + + validator.bom_importer.invalid_import_type + Invalid import type! + + + + + validator.bom_importer.invalid_file_extension + Invalid file extension "%extension%" for import type %importType%". Allowed file extensions: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -389,57 +401,57 @@ __validator.assembly.bom_entry.name_or_part_needed - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required you must specify a quantity > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float expected as float greater than 0.0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty expected as non-empty string - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null als nicht leere Zeichenkette oder null erwartet - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array expectd as array - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties must have at least one of the following sub-properties: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor not found for %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch does not match exactly. Given for import: %importValue%, found (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties must have either assigned as sub-property: "id" as an integer greater than 0, or "name" as a non-empty string diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index cde3672f6..957a47916 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -209,6 +209,18 @@ Seule une pièce ou un assemblage peut être sélectionné. Veuillez ajuster votre sélection! + + + validator.bom_importer.invalid_import_type + Type d'importation invalide ! + + + + + validator.bom_importer.invalid_file_extension + Extension de fichier "%extension%" invalide pour le type d'importation "%importType%". Extensions de fichier autorisées : %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -227,57 +239,57 @@ Vous devez sélectionner une pièce ou attribuer un nom pour un non-élément! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Vous devez entrer une quantité > 0 ! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float attendu comme un nombre décimal (float) supérieur à 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty attendu comme une chaîne de caractères non vide - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null attendu comme une chaîne de caractères non vide ou null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array attendu comme un tableau (array) - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties doit contenir au moins l'un des sous-paramètres suivants : %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor non trouvé pour %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch ne correspond pas exactement. Donné pour l'importation : %importValue%, trouvé (%foundId%) : %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties doit contenir comme sous-paramètre soit : "id" comme entier supérieur à 0 ou "name" comme chaîne de caractères non vide diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 988724201..639dff8bc 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -365,6 +365,18 @@ Neispravan kod. Provjerite je li vaša aplikacija za autentifikaciju ispravno postavljena i jesu li poslužitelj i uređaj za autentifikaciju ispravno postavili vrijeme. + + + validator.bom_importer.invalid_import_type + Nevažeći tip uvoza! + + + + + validator.bom_importer.invalid_file_extension + Nevažeća ekstenzija datoteke "%extension%" za tip uvoza "%importType%". Dopuštene ekstenzije datoteka: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -383,57 +395,57 @@ Morate odabrati dio ili unijeti naziv za nedio! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Morate unijeti količinu > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float očekuje se decimalni broj (float) veći od 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty očekuje se kao neprazan niz znakova - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null očekuje se kao neprazan niz znakova ili null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array očekuje se kao niz - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties mora sadržavati barem jedan od sljedećih pod-parametara: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor nije pronađeno za %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch ne podudara se točno. Uneseno za uvoz: %importValue%, pronađeno (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties mora sadržavati kao pod-parametar bilo: "id" kao cijeli broj veći od 0 ili "name" kao neprazan niz znakova diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index 63ca86d77..a1b9b2f09 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -365,6 +365,18 @@ Codice non valido. Controlla che la tua app di autenticazione sia impostata correttamente e che sia il server che il dispositivo di autenticazione abbiano l'ora impostata correttamente. + + + validator.bom_importer.invalid_import_type + Tipo di importazione non valido! + + + + + validator.bom_importer.invalid_file_extension + Estensione del file "%extension%" non valida per il tipo di importazione "%importType%". Estensioni consentite: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -383,57 +395,57 @@ È necessario selezionare una parte o inserire un nome per un non-parte! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Devi inserire una quantità > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float atteso come numero decimale (float) maggiore di 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty atteso come stringa non vuota - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null atteso come stringa non vuota o null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array atteso come array - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties deve avere almeno uno dei seguenti sotto-parametri: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor non trovato per %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch non corrisponde esattamente. Valore dato per l'importazione: %importValue%, trovato (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties deve contenere come sotto-parametro: "id" come intero maggiore di 0 o "name" come stringa non vuota diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index c0b541173..4a36a79ac 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -209,6 +209,18 @@ 部品またはアセンブリのみ選択可能です。選択内容を調整してください! + + + validator.bom_importer.invalid_import_type + 無効なインポートタイプです! + + + + + validator.bom_importer.invalid_file_extension + インポートタイプ "%importType%" に対して無効なファイル拡張子 "%extension%"。許可されているファイル拡張子: %allowedExtensions%。 + + assembly.bom_entry.part_already_in_bom @@ -227,57 +239,57 @@ 部品を選択するか、非部品の名前を入力する必要があります! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required 数量 > 0 を入力する必要があります! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float 0.0 より大きい小数 (float) である必要があります - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty 空でない文字列が期待されます - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null 空でない文字列または null が期待されます - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array 配列として期待されます - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties 以下のサブパラメーターのいずれかを含む必要があります:%propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor %value% に対する項目が見つかりません - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch 完全には一致しません。インポートされた値:%importValue%、見つかった値 (%foundId%):%foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties サブパラメーターとして次のいずれかを含む必要があります:"id" は 0 より大きい整数、または "name" は空でない文字列 diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index d417757dd..e80dd23bb 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -365,6 +365,18 @@ Nieprawidłowy kod. Sprawdź, czy aplikacja uwierzytelniająca jest poprawnie skonfigurowana i czy zarówno serwer, jak i urządzenie uwierzytelniające mają poprawnie ustawiony czas. + + + validator.bom_importer.invalid_import_type + Nieprawidłowy typ importu! + + + + + validator.bom_importer.invalid_file_extension + Nieprawidłowe rozszerzenie pliku "%extension%" dla typu importu "%importType%". Dozwolone rozszerzenia plików: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -383,57 +395,57 @@ Musisz wybrać element lub przypisać nazwę dla elementu niestandardowego! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Musisz wprowadzić ilość > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float oczekiwano liczby zmiennoprzecinkowej (float) większej od 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty oczekiwano jako niepusty ciąg znaków - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null oczekiwano jako niepusty ciąg znaków lub null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array oczekiwano jako tablicę - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties musi zawierać co najmniej jeden z następujących podparametrów: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor nie znaleziono dla %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch brak dokładnego dopasowania. Wprowadzone do importu: %importValue%, znalezione (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties musi zawierać jako podparametr: "id" jako liczbę całkowitą większą od 0 lub "name" jako niepusty ciąg znaków diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 625aea240..48f0737eb 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -365,6 +365,18 @@ Неверный код. Проверьте, что приложение аутентификации настроено правильно и что на сервере и устройстве аутентификации установлено правильное время. + + + validator.bom_importer.invalid_import_type + Недопустимый тип импорта! + + + + + validator.bom_importer.invalid_file_extension + Недопустимое расширение файла "%extension%" для типа импорта "%importType%". Допустимые расширения файлов: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -383,57 +395,57 @@ Необходимо выбрать деталь или ввести название для недетали! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Необходимо указать количество > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float ожидается число с плавающей запятой (float), большее 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty ожидается непустая строка - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null ожидается непустая строка или null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array ожидается массив - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties должен содержать хотя бы один из следующих под-параметров: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor не найдено для %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch точное совпадение отсутствует. Указано для импорта: %importValue%, найдено (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties должен содержать под-параметр: "id" как целое число больше 0 или "name" как непустая строка diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 5093ce989..dea45ccc9 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -353,6 +353,18 @@ 由于技术限制,在32位系统中无法选择2038年1月19日之后的日期! + + + validator.bom_importer.invalid_import_type + 无效的导入类型! + + + + + validator.bom_importer.invalid_file_extension + 导入类型“%importType%”的文件扩展名“%extension%”无效。允许的扩展名: %allowedExtensions%。 + + assembly.bom_entry.part_already_in_bom @@ -371,57 +383,57 @@ 必须选择零件或为非零件指定名称! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required 必须输入数量 > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float 应为大于 0.0 的浮点数 (float) - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty 应为非空字符串 - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null 应为非空字符串或 null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array 应为数组 - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties 必须包含以下子参数之一:%propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor 未找到对应值 %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch 未精确匹配。用于导入的值:%importValue%,找到的值 (%foundId%):%foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties 必须包含子参数:"id" 为大于 0 的整数,或 "name" 为非空字符串 From ca2110afe7ecc1a886a184d08f9e4e59ceed16f3 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 22 Apr 2025 16:31:22 +0200 Subject: [PATCH 10/83] =?UTF-8?q?Umstellung=20Migrationen=20bzgl.=20Multi-?= =?UTF-8?q?Plattform-Support.=20Zun=C3=A4chst=20MySQL,=20SQLite=20Statemen?= =?UTF-8?q?ts=20integrieren.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/Version20250304081039.php | 89 ++++++- migrations/Version20250304154507.php | 377 ++++++++++++++++++++++++++- migrations/Version20250310160354.php | 167 +++++++++++- 3 files changed, 624 insertions(+), 9 deletions(-) diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php index 4e521ade9..789cf3281 100644 --- a/migrations/Version20250304081039.php +++ b/migrations/Version20250304081039.php @@ -4,12 +4,18 @@ namespace DoctrineMigrations; +use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; -final class Version20250304081039 extends AbstractMigration +final class Version20250304081039 extends AbstractMultiPlatformMigration { - public function up(Schema $schema): void + public function getDescription(): string + { + return 'Add assemblies and assembly BOM entries'; + } + + public function mySQLUp(Schema $schema): void { $this->addSql('CREATE TABLE assemblies (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, order_quantity INT NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts TINYINT(1) NOT NULL, description LONGTEXT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, INDEX IDX_5F3832C0727ACA70 (parent_id), INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); $this->addSql('CREATE TABLE assembly_bom_entries (id INT AUTO_INCREMENT NOT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames LONGTEXT NOT NULL, name VARCHAR(255) DEFAULT NULL, comment LONGTEXT NOT NULL, price NUMERIC(11, 5) DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_assembly INT DEFAULT NULL, id_part INT DEFAULT NULL, price_currency_id INT DEFAULT NULL, INDEX IDX_8C74887E2F180363 (id_assembly), INDEX IDX_8C74887EC22F6CC4 (id_part), INDEX IDX_8C74887E3FFDCD60 (price_currency_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); @@ -20,7 +26,7 @@ public function up(Schema $schema): void $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id)'); } - public function down(Schema $schema): void + public function mySQLDown(Schema $schema): void { $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0727ACA70'); $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0EA7100A1'); @@ -30,4 +36,81 @@ public function down(Schema $schema): void $this->addSql('DROP TABLE assemblies'); $this->addSql('DROP TABLE assembly_bom_entries'); } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE assemblies ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + order_quantity INTEGER NOT NULL, + order_only_missing_parts BOOLEAN NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + status VARCHAR(64) DEFAULT NULL, + description CLOB NOT NULL, + alternative_names CLOB DEFAULT NULL, + CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_5F3832C0727ACA70 ON assemblies (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_5F3832C0EA7100A1 ON assemblies (id_preview_attachment) + SQL); + + $this->addSql(<<<'SQL' + CREATE TABLE assembly_bom_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_assembly INTEGER DEFAULT NULL, + id_part INTEGER DEFAULT NULL, + price_currency_id INTEGER DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames CLOB NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment CLOB NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT FK_8C74887E4AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E4AD2039E ON assembly_bom_entries (id_assembly) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887EC22F6CC4 ON assembly_bom_entries (id_part) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E3FFDCD60 ON assembly_bom_entries (price_currency_id) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP TABLE assembly_bom_entries + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assemblies + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + //Not needed + } + + public function postgreSQLDown(Schema $schema): void + { + //Not needed + } } diff --git a/migrations/Version20250304154507.php b/migrations/Version20250304154507.php index 62dcc43c0..37cf2fec3 100644 --- a/migrations/Version20250304154507.php +++ b/migrations/Version20250304154507.php @@ -4,22 +4,393 @@ namespace DoctrineMigrations; +use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; -final class Version20250304154507 extends AbstractMigration +final class Version20250304154507 extends AbstractMultiPlatformMigration { - public function up(Schema $schema): void + public function getDescription(): string + { + return 'Add built_assembly_id to parts table'; + } + + public function mySQLUp(Schema $schema): void { $this->addSql('ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL'); $this->addSql('ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id)'); $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON parts (built_assembly_id)'); } - public function down(Schema $schema): void + public function mySQLDown(Schema $schema): void { $this->addSql('ALTER TABLE `parts` DROP FOREIGN KEY FK_6940A7FECC660B3C'); $this->addSql('DROP INDEX UNIQ_6940A7FECC660B3C ON `parts`'); $this->addSql('ALTER TABLE `parts` DROP built_assembly_id'); } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__parts AS + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM parts + SQL); + $this->addSql('DROP TABLE parts'); + + $this->addSql(<<<'SQL' + CREATE TABLE "parts" + ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + id_category INTEGER NOT NULL, + id_footprint INTEGER DEFAULT NULL, + id_part_unit INTEGER DEFAULT NULL, + id_manufacturer INTEGER DEFAULT NULL, + order_orderdetails_id INTEGER DEFAULT NULL, + built_project_id INTEGER DEFAULT NULL, + built_assembly_id INTEGER DEFAULT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + needs_review BOOLEAN NOT NULL, + tags CLOB NOT NULL, + mass DOUBLE PRECISION DEFAULT NULL, + description CLOB NOT NULL, + comment CLOB NOT NULL, + visible BOOLEAN NOT NULL, + favorite BOOLEAN NOT NULL, + minamount DOUBLE PRECISION NOT NULL, + manufacturer_product_url CLOB NOT NULL, + manufacturer_product_number VARCHAR(255) NOT NULL, + manufacturing_status VARCHAR(255) DEFAULT NULL, + order_quantity INTEGER NOT NULL, + manual_order BOOLEAN NOT NULL, + ipn VARCHAR(100) DEFAULT NULL, + provider_reference_provider_key VARCHAR(255) DEFAULT NULL, + provider_reference_provider_id VARCHAR(255) DEFAULT NULL, + provider_reference_provider_url VARCHAR(255) DEFAULT NULL, + provider_reference_last_updated DATETIME DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_value VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO parts ( + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + ) SELECT * FROM __temp__parts + SQL); + $this->addSql('DROP TABLE __temp__parts'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON "parts" (built_assembly_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_ipn ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_name ON "parts" (name) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__parts AS + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM parts + SQL); + + $this->addSql('DROP TABLE parts'); + + $this->addSql(<<<'SQL' + CREATE TABLE "parts" + ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + id_category INTEGER NOT NULL, + id_footprint INTEGER DEFAULT NULL, + id_part_unit INTEGER DEFAULT NULL, + id_manufacturer INTEGER DEFAULT NULL, + order_orderdetails_id INTEGER DEFAULT NULL, + built_project_id INTEGER DEFAULT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + needs_review BOOLEAN NOT NULL, + tags CLOB NOT NULL, + mass DOUBLE PRECISION DEFAULT NULL, + description CLOB NOT NULL, + comment CLOB NOT NULL, + visible BOOLEAN NOT NULL, + favorite BOOLEAN NOT NULL, + minamount DOUBLE PRECISION NOT NULL, + manufacturer_product_url CLOB NOT NULL, + manufacturer_product_number VARCHAR(255) NOT NULL, + manufacturing_status VARCHAR(255) DEFAULT NULL, + order_quantity INTEGER NOT NULL, + manual_order BOOLEAN NOT NULL, + ipn VARCHAR(100) DEFAULT NULL, + provider_reference_provider_key VARCHAR(255) DEFAULT NULL, + provider_reference_provider_id VARCHAR(255) DEFAULT NULL, + provider_reference_provider_url VARCHAR(255) DEFAULT NULL, + provider_reference_last_updated DATETIME DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_value VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO parts ( + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + ) SELECT * FROM __temp__parts + SQL); + + $this->addSql('DROP TABLE __temp__parts'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_ipn ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_name ON "parts" (name) + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + //Not needed + } + + public function postgreSQLDown(Schema $schema): void + { + //Not needed + } } diff --git a/migrations/Version20250310160354.php b/migrations/Version20250310160354.php index 542fcac28..201e7e85a 100644 --- a/migrations/Version20250310160354.php +++ b/migrations/Version20250310160354.php @@ -4,12 +4,18 @@ namespace DoctrineMigrations; +use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; -final class Version20250310160354 extends AbstractMigration +final class Version20250310160354 extends AbstractMultiPlatformMigration { - public function up(Schema $schema): void + public function getDescription(): string + { + return 'Add assembly_id to project_bom_entries'; + } + + public function mySQLUp(Schema $schema): void { $this->addSql('ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e2f180363 TO IDX_8C74887E4AD2039E'); $this->addSql('ALTER TABLE project_bom_entries ADD id_assembly INT DEFAULT NULL AFTER id_part'); @@ -17,11 +23,166 @@ public function up(Schema $schema): void $this->addSql('CREATE INDEX IDX_1AA2DD314AD2039E ON project_bom_entries (id_assembly)'); } - public function down(Schema $schema): void + public function mySQLDown(Schema $schema): void { $this->addSql('ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e4ad2039e TO IDX_8C74887E2F180363'); $this->addSql('ALTER TABLE project_bom_entries DROP FOREIGN KEY FK_1AA2DD314AD2039E'); $this->addSql('DROP INDEX IDX_1AA2DD314AD2039E ON project_bom_entries'); $this->addSql('ALTER TABLE project_bom_entries DROP id_assembly'); } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__project_bom_entries AS + SELECT + id, + id_device, + id_part, + price_currency_id, + quantity, + mountnames, + name, + comment, + price, + last_modified, + datetime_added + FROM project_bom_entries + SQL); + + $this->addSql('DROP TABLE project_bom_entries'); + + $this->addSql(<<<'SQL' + CREATE TABLE project_bom_entries + ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_device INTEGER DEFAULT NULL, + id_assembly INTEGER DEFAULT NULL, + id_part INTEGER DEFAULT NULL, + price_currency_id INTEGER DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames CLOB NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment CLOB NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT FK_1AA2DD312F180363 FOREIGN KEY (id_device) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_1AA2DD314AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_1AA2DD31C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + + $this->addSql(<<<'SQL' + INSERT INTO project_bom_entries ( + id, + id_device, + id_part, + price_currency_id, + quantity, + mountnames, + name, + comment, + price, + last_modified, + datetime_added + ) SELECT * FROM __temp__project_bom_entries + SQL); + $this->addSql('DROP TABLE __temp__project_bom_entries'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD314AD2039E ON project_bom_entries (id_assembly) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__project_bom_entries AS + SELECT + id, + id_device, + id_part, + price_currency_id, + quantity, + mountnames, + name, + comment, + price, + last_modified, + datetime_added + FROM project_bom_entries + SQL); + + $this->addSql('DROP TABLE project_bom_entries'); + + $this->addSql(<<<'SQL' + CREATE TABLE project_bom_entries + ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_device INTEGER DEFAULT NULL, + id_part INTEGER DEFAULT NULL, + price_currency_id INTEGER DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames CLOB NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment CLOB NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT FK_1AA2DD312F180363 FOREIGN KEY (id_device) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_1AA2DD31C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO project_bom_entries ( + id, + id_device, + id_part, + price_currency_id, + quantity, + mountnames, + name, + comment, + price, + last_modified, + datetime_added + ) SELECT * FROM __temp__project_bom_entries + SQL); + + $this->addSql('DROP TABLE __temp__project_bom_entries'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id) + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + //Not needed + } + + public function postgreSQLDown(Schema $schema): void + { + //Not needed + } } From 35de4c43c1cf58610b7446b527ce9d485c521495 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 17 Jun 2025 11:28:42 +0200 Subject: [PATCH 11/83] Projekt BOM-Konfiguration um Assemblies bereinigen. Assembly BOM-Konfiguration um Projektauswahl erweitern (APS-3, APS-4) --- .../elements/project_select_controller.js | 70 +++++++ migrations/Version20250304081039.php | 109 +++++++++- migrations/Version20250304154507.php | 20 +- migrations/Version20250310160354.php | 188 ------------------ src/Controller/TypeaheadController.php | 36 ++-- .../AssemblyBomEntriesDataTable.php | 38 ++-- ...eHelper.php => ProjectDataTableHelper.php} | 6 +- src/DataTables/ProjectBomEntriesDataTable.php | 27 +-- src/Entity/AssemblySystem/Assembly.php | 4 +- .../AssemblySystem/AssemblyBOMEntry.php | 26 ++- src/Entity/ProjectSystem/Project.php | 1 - src/Entity/ProjectSystem/ProjectBOMEntry.php | 38 +--- .../AssemblySystem/AssemblyBOMEntryType.php | 13 +- src/Form/AssemblySystem/AssemblyBuildType.php | 19 +- .../ProjectSystem/ProjectAddPartsType.php | 1 - .../ProjectSystem/ProjectBOMEntryType.php | 5 - src/Form/ProjectSystem/ProjectBuildType.php | 23 +-- ...lySelectType.php => ProjectSelectType.php} | 26 +-- src/Helpers/Projects/ProjectBuildRequest.php | 98 +-------- src/Repository/AssemblyRepository.php | 69 ------- src/Repository/Parts/DeviceRepository.php | 18 ++ .../AssemblySystem/AssemblyBuildHelper.php | 36 ++-- ...erator.php => ProjectPreviewGenerator.php} | 26 +-- .../ProjectSystem/ProjectBuildHelper.php | 71 ++----- src/Twig/AssemblyTwigExtension.php | 6 +- .../form/collection_types_layout.html.twig | 18 +- ...collection_types_layout_assembly.html.twig | 18 +- templates/projects/build/_form.html.twig | 57 +----- translations/messages.cs.xlf | 62 ++---- translations/messages.da.xlf | 94 ++------- translations/messages.de.xlf | 84 ++------ translations/messages.el.xlf | 82 ++------ translations/messages.en.xlf | 82 ++------ translations/messages.es.xlf | 82 ++------ translations/messages.fr.xlf | 86 ++------ translations/messages.it.xlf | 86 ++------ translations/messages.ja.xlf | 106 ++++------ translations/messages.nl.xlf | 82 ++------ translations/messages.pl.xlf | 86 ++------ translations/messages.ru.xlf | 86 ++------ translations/messages.zh.xlf | 86 ++------ translations/validators.cs.xlf | 6 + translations/validators.da.xlf | 6 + translations/validators.de.xlf | 6 + translations/validators.el.xlf | 6 + translations/validators.en.xlf | 12 +- translations/validators.fr.xlf | 6 + translations/validators.hr.xlf | 6 + translations/validators.it.xlf | 6 + translations/validators.ja.xlf | 6 + translations/validators.pl.xlf | 6 + translations/validators.ru.xlf | 6 + translations/validators.zh.xlf | 6 + 53 files changed, 741 insertions(+), 1508 deletions(-) create mode 100644 assets/controllers/elements/project_select_controller.js delete mode 100644 migrations/Version20250310160354.php rename src/DataTables/Helpers/{AssemblyDataTableHelper.php => ProjectDataTableHelper.php} (91%) rename src/Form/Type/{AssemblySelectType.php => ProjectSelectType.php} (82%) delete mode 100644 src/Repository/AssemblyRepository.php rename src/Services/Attachments/{AssemblyPreviewGenerator.php => ProjectPreviewGenerator.php} (75%) diff --git a/assets/controllers/elements/project_select_controller.js b/assets/controllers/elements/project_select_controller.js new file mode 100644 index 000000000..98702d419 --- /dev/null +++ b/assets/controllers/elements/project_select_controller.js @@ -0,0 +1,70 @@ +import {Controller} from "@hotwired/stimulus"; + +import "tom-select/dist/css/tom-select.bootstrap5.css"; +import '../../css/components/tom-select_extensions.css'; +import TomSelect from "tom-select"; +import {marked} from "marked"; + +export default class extends Controller { + _tomSelect; + + connect() { + + let settings = { + allowEmptyOption: true, + plugins: ['dropdown_input', 'clear_button'], + searchField: ["name", "description", "category", "footprint"], + valueField: "id", + labelField: "name", + preload: "focus", + render: { + item: (data, escape) => { + return '' + (data.image ? "" : "") + escape(data.name) + ''; + }, + option: (data, escape) => { + if(data.text) { + return '' + escape(data.text) + ''; + } + + let tmp = '
                                                      ' + + "
                                                      " + + (data.image ? "" : "") + + "
                                                      " + + "
                                                      " + + '
                                                      ' + escape(data.name) + '
                                                      ' + + (data.description ? '

                                                      ' + marked.parseInline(data.description) + '

                                                      ' : "") + + (data.category ? '

                                                      ' + escape(data.category) : ""); + + return tmp + '

                                                      ' + + '
                                                      '; + } + } + }; + + + if (this.element.dataset.autocomplete) { + const base_url = this.element.dataset.autocomplete; + settings.valueField = "id"; + settings.load = (query, callback) => { + const url = base_url.replace('__QUERY__', encodeURIComponent(query)); + + fetch(url) + .then(response => response.json()) + .then(json => {callback(json);}) + .catch(() => { + callback() + }); + }; + + + this._tomSelect = new TomSelect(this.element, settings); + //this._tomSelect.clearOptions(); + } + } + + disconnect() { + super.disconnect(); + //Destroy the TomSelect instance + this._tomSelect.destroy(); + } +} \ No newline at end of file diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php index 789cf3281..755ae2360 100644 --- a/migrations/Version20250304081039.php +++ b/migrations/Version20250304081039.php @@ -6,7 +6,6 @@ use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; -use Doctrine\Migrations\AbstractMigration; final class Version20250304081039 extends AbstractMultiPlatformMigration { @@ -17,12 +16,13 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { - $this->addSql('CREATE TABLE assemblies (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, order_quantity INT NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts TINYINT(1) NOT NULL, description LONGTEXT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, INDEX IDX_5F3832C0727ACA70 (parent_id), INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('CREATE TABLE assembly_bom_entries (id INT AUTO_INCREMENT NOT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames LONGTEXT NOT NULL, name VARCHAR(255) DEFAULT NULL, comment LONGTEXT NOT NULL, price NUMERIC(11, 5) DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_assembly INT DEFAULT NULL, id_part INT DEFAULT NULL, price_currency_id INT DEFAULT NULL, INDEX IDX_8C74887E2F180363 (id_assembly), INDEX IDX_8C74887EC22F6CC4 (id_part), INDEX IDX_8C74887E3FFDCD60 (price_currency_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('CREATE TABLE assemblies (id INT AUTO_INCREMENT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, name VARCHAR(255) NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, order_quantity INT NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts TINYINT(1) NOT NULL, description LONGTEXT NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_5F3832C0727ACA70 (parent_id), INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('CREATE TABLE assembly_bom_entries (id INT AUTO_INCREMENT NOT NULL, id_assembly INT DEFAULT NULL, id_part INT DEFAULT NULL, id_project INT DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames LONGTEXT NOT NULL, name VARCHAR(255) DEFAULT NULL, comment LONGTEXT NOT NULL, price NUMERIC(11, 5) DEFAULT NULL, price_currency_id INT DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_8C74887E2F180363 (id_assembly), INDEX IDX_8C74887EC22F6CC4 (id_part), INDEX IDX_8C74887EF12E799E (id_project), INDEX IDX_8C74887E3FFDCD60 (price_currency_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); $this->addSql('ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id)'); $this->addSql('ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES `attachments` (id) ON DELETE SET NULL'); $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E2F180363 FOREIGN KEY (id_assembly) REFERENCES assemblies (id)'); $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES `parts` (id)'); + $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES `projects` (id)'); $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id)'); } @@ -32,6 +32,7 @@ public function mySQLDown(Schema $schema): void $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0EA7100A1'); $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E2F180363'); $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EC22F6CC4'); + $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EF12E799E'); $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E3FFDCD60'); $this->addSql('DROP TABLE assemblies'); $this->addSql('DROP TABLE assembly_bom_entries'); @@ -70,6 +71,7 @@ public function sqLiteUp(Schema $schema): void id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_assembly INTEGER DEFAULT NULL, id_part INTEGER DEFAULT NULL, + id_project INTEGER DEFAULT NULL, price_currency_id INTEGER DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames CLOB NOT NULL, @@ -80,6 +82,7 @@ public function sqLiteUp(Schema $schema): void datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_8C74887E4AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE ) SQL); @@ -89,6 +92,9 @@ public function sqLiteUp(Schema $schema): void $this->addSql(<<<'SQL' CREATE INDEX IDX_8C74887EC22F6CC4 ON assembly_bom_entries (id_part) SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887EF12E799E ON assembly_bom_entries (id_project) + SQL); $this->addSql(<<<'SQL' CREATE INDEX IDX_8C74887E3FFDCD60 ON assembly_bom_entries (price_currency_id) SQL); @@ -106,11 +112,104 @@ public function sqLiteDown(Schema $schema): void public function postgreSQLUp(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + CREATE TABLE assemblies ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + comment TEXT NOT NULL, + not_selectable BOOLEAN NOT NULL, + alternative_names TEXT DEFAULT NULL, + order_quantity INT NOT NULL, + status VARCHAR(64) DEFAULT NULL, + order_only_missing_parts BOOLEAN NOT NULL, + description TEXT NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, + PRIMARY KEY(id) + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_5F3832C0727ACA70 ON assemblies (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_5F3832C0EA7100A1 ON assemblies (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE assembly_bom_entries ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + id_assembly INT DEFAULT NULL, + id_part INT DEFAULT NULL, + id_project INT DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames TEXT NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment TEXT NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + price_currency_id INT DEFAULT NULL, + last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY(id) + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E4AD2039E ON assembly_bom_entries (id_assembly) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887EC22F6CC4 ON assembly_bom_entries (id_part) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887EF12E799E ON assembly_bom_entries (id_project) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E3FFDCD60 ON assembly_bom_entries (price_currency_id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E4AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES "projects" (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); } public function postgreSQLDown(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP CONSTRAINT FK_5F3832C0727ACA70 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP CONSTRAINT FK_5F3832C0EA7100A1 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887E4AD2039E + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887EC22F6CC4 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887EF12E799E + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887E3FFDCD60 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assemblies + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assembly_bom_entries + SQL); } } diff --git a/migrations/Version20250304154507.php b/migrations/Version20250304154507.php index 37cf2fec3..904a3b658 100644 --- a/migrations/Version20250304154507.php +++ b/migrations/Version20250304154507.php @@ -386,11 +386,27 @@ public function sqLiteDown(Schema $schema): void public function postgreSQLUp(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON parts (built_assembly_id) + SQL); } public function postgreSQLDown(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FECC660B3C + SQL); + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_6940A7FECC660B3C + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE "parts" DROP built_assembly_id + SQL); } } diff --git a/migrations/Version20250310160354.php b/migrations/Version20250310160354.php deleted file mode 100644 index 201e7e85a..000000000 --- a/migrations/Version20250310160354.php +++ /dev/null @@ -1,188 +0,0 @@ -addSql('ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e2f180363 TO IDX_8C74887E4AD2039E'); - $this->addSql('ALTER TABLE project_bom_entries ADD id_assembly INT DEFAULT NULL AFTER id_part'); - $this->addSql('ALTER TABLE project_bom_entries ADD CONSTRAINT FK_1AA2DD314AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id)'); - $this->addSql('CREATE INDEX IDX_1AA2DD314AD2039E ON project_bom_entries (id_assembly)'); - } - - public function mySQLDown(Schema $schema): void - { - $this->addSql('ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e4ad2039e TO IDX_8C74887E2F180363'); - $this->addSql('ALTER TABLE project_bom_entries DROP FOREIGN KEY FK_1AA2DD314AD2039E'); - $this->addSql('DROP INDEX IDX_1AA2DD314AD2039E ON project_bom_entries'); - $this->addSql('ALTER TABLE project_bom_entries DROP id_assembly'); - } - - public function sqLiteUp(Schema $schema): void - { - $this->addSql(<<<'SQL' - CREATE TEMPORARY TABLE __temp__project_bom_entries AS - SELECT - id, - id_device, - id_part, - price_currency_id, - quantity, - mountnames, - name, - comment, - price, - last_modified, - datetime_added - FROM project_bom_entries - SQL); - - $this->addSql('DROP TABLE project_bom_entries'); - - $this->addSql(<<<'SQL' - CREATE TABLE project_bom_entries - ( - id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - id_device INTEGER DEFAULT NULL, - id_assembly INTEGER DEFAULT NULL, - id_part INTEGER DEFAULT NULL, - price_currency_id INTEGER DEFAULT NULL, - quantity DOUBLE PRECISION NOT NULL, - mountnames CLOB NOT NULL, - name VARCHAR(255) DEFAULT NULL, - comment CLOB NOT NULL, - price NUMERIC(11, 5) DEFAULT NULL, - last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - CONSTRAINT FK_1AA2DD312F180363 FOREIGN KEY (id_device) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_1AA2DD314AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_1AA2DD31C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE - ) - SQL); - - - $this->addSql(<<<'SQL' - INSERT INTO project_bom_entries ( - id, - id_device, - id_part, - price_currency_id, - quantity, - mountnames, - name, - comment, - price, - last_modified, - datetime_added - ) SELECT * FROM __temp__project_bom_entries - SQL); - $this->addSql('DROP TABLE __temp__project_bom_entries'); - - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device) - SQL); - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part) - SQL); - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD314AD2039E ON project_bom_entries (id_assembly) - SQL); - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id) - SQL); - } - - public function sqLiteDown(Schema $schema): void - { - $this->addSql(<<<'SQL' - CREATE TEMPORARY TABLE __temp__project_bom_entries AS - SELECT - id, - id_device, - id_part, - price_currency_id, - quantity, - mountnames, - name, - comment, - price, - last_modified, - datetime_added - FROM project_bom_entries - SQL); - - $this->addSql('DROP TABLE project_bom_entries'); - - $this->addSql(<<<'SQL' - CREATE TABLE project_bom_entries - ( - id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - id_device INTEGER DEFAULT NULL, - id_part INTEGER DEFAULT NULL, - price_currency_id INTEGER DEFAULT NULL, - quantity DOUBLE PRECISION NOT NULL, - mountnames CLOB NOT NULL, - name VARCHAR(255) DEFAULT NULL, - comment CLOB NOT NULL, - price NUMERIC(11, 5) DEFAULT NULL, - last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - CONSTRAINT FK_1AA2DD312F180363 FOREIGN KEY (id_device) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_1AA2DD31C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE - ) - SQL); - - $this->addSql(<<<'SQL' - INSERT INTO project_bom_entries ( - id, - id_device, - id_part, - price_currency_id, - quantity, - mountnames, - name, - comment, - price, - last_modified, - datetime_added - ) SELECT * FROM __temp__project_bom_entries - SQL); - - $this->addSql('DROP TABLE __temp__project_bom_entries'); - - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device) - SQL); - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part) - SQL); - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id) - SQL); - } - - public function postgreSQLUp(Schema $schema): void - { - //Not needed - } - - public function postgreSQLDown(Schema $schema): void - { - //Not needed - } -} diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 09792951a..4335492e8 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -22,9 +22,9 @@ namespace App\Controller; -use App\Entity\AssemblySystem\Assembly; use App\Entity\Parameters\AbstractParameter; -use App\Services\Attachments\AssemblyPreviewGenerator; +use App\Entity\ProjectSystem\Project; +use App\Services\Attachments\ProjectPreviewGenerator; use Symfony\Component\HttpFoundation\Response; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Category; @@ -151,29 +151,29 @@ public function parts( 'footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '', 'description' => mb_strimwidth($part->getDescription(), 0, 127, '...'), 'image' => $preview_url, - ]; + ]; } return new JsonResponse($data); } - #[Route(path: '/assemblies/search/{query}', name: 'typeahead_assemblies')] - public function assemblies( - EntityManagerInterface $entityManager, - AssemblyPreviewGenerator $assemblyPreviewGenerator, - AttachmentURLGenerator $attachmentURLGenerator, - string $query = "" + #[Route(path: '/projects/search/{query}', name: 'typeahead_projects')] + public function projects( + EntityManagerInterface $entityManager, + ProjectPreviewGenerator $projectPreviewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" ): JsonResponse { - $this->denyAccessUnlessGranted('@assemblies.read'); + $this->denyAccessUnlessGranted('@projects.read'); $result = []; - $assemblyRepository = $entityManager->getRepository(Assembly::class); + $projectRepository = $entityManager->getRepository(Project::class); - $assemblies = $assemblyRepository->autocompleteSearch($query, 100); + $projects = $projectRepository->autocompleteSearch($query, 100); - foreach ($assemblies as $assembly) { - $preview_attachment = $assemblyPreviewGenerator->getTablePreviewAttachment($assembly); + foreach ($projects as $project) { + $preview_attachment = $projectPreviewGenerator->getTablePreviewAttachment($project); if($preview_attachment instanceof Attachment) { $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); @@ -181,13 +181,13 @@ public function assemblies( $preview_url = ''; } - /** @var Assembly $assembly */ + /** @var Project $project */ $result[] = [ - 'id' => $assembly->getID(), - 'name' => $this->translator->trans('typeahead.parts.assembly.name', ['%name%' => $assembly->getName()]), + 'id' => $project->getID(), + 'name' => $project->getName(), 'category' => '', 'footprint' => '', - 'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'), + 'description' => mb_strimwidth($project->getDescription(), 0, 127, '...'), 'image' => $preview_url, ]; } diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php index a953179a9..b31c72bc0 100644 --- a/src/DataTables/AssemblyBomEntriesDataTable.php +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -25,11 +25,13 @@ use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Helpers\ProjectDataTableHelper; use App\DataTables\Helpers\ColumnSortHelper; use App\DataTables\Helpers\PartDataTableHelper; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\ProjectSystem\Project; use App\Services\EntityURLGenerator; use App\Services\Formatters\AmountFormatter; use Doctrine\ORM\QueryBuilder; @@ -43,12 +45,13 @@ class AssemblyBomEntriesDataTable implements DataTableTypeInterface { public function __construct( - protected TranslatorInterface $translator, - protected PartDataTableHelper $partDataTableHelper, - protected EntityURLGenerator $entityURLGenerator, - protected AmountFormatter $amountFormatter, - private string $visible_columns, - private ColumnSortHelper $csh + protected TranslatorInterface $translator, + protected PartDataTableHelper $partDataTableHelper, + protected ProjectDataTableHelper $projectDataTableHelper, + protected EntityURLGenerator $entityURLGenerator, + protected AmountFormatter $amountFormatter, + private string $visible_columns, + private ColumnSortHelper $csh ){ } @@ -86,18 +89,29 @@ public function configure(DataTable $dataTable, array $options): void 'label' => $this->translator->trans('part.table.name'), 'orderField' => 'NATSORT(part.name)', 'render' => function ($value, AssemblyBOMEntry $context) { - if(!$context->getPart() instanceof Part) { + if(!$context->getPart() instanceof Part && !$context->getProject() instanceof Project) { return htmlspecialchars((string) $context->getName()); } - //Part exists if we reach this point - - $tmp = $this->partDataTableHelper->renderName($context->getPart()); - if($context->getName() !== null && $context->getName() !== '') { - $tmp .= '
                                                      '.htmlspecialchars($context->getName()).''; + if ($context->getPart() !== null) { + $tmp = $this->partDataTableHelper->renderName($context->getPart()); + $tmp = $this->translator->trans('part.table.name.value.for_part', ['%value%' => $tmp]); + + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
                                                      '.htmlspecialchars($context->getName()).''; + } + } elseif ($context->getProject() !== null) { + $tmp = $this->projectDataTableHelper->renderName($context->getProject()); + $tmp = $this->translator->trans('part.table.name.value.for_project', ['%value%' => $tmp]); + + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
                                                      '.htmlspecialchars($context->getName()).''; + } } + return $tmp; }, + ]) ->add('ipn', TextColumn::class, [ 'label' => $this->translator->trans('part.table.ipn'), diff --git a/src/DataTables/Helpers/AssemblyDataTableHelper.php b/src/DataTables/Helpers/ProjectDataTableHelper.php similarity index 91% rename from src/DataTables/Helpers/AssemblyDataTableHelper.php rename to src/DataTables/Helpers/ProjectDataTableHelper.php index 36f7836b7..0118d5d56 100644 --- a/src/DataTables/Helpers/AssemblyDataTableHelper.php +++ b/src/DataTables/Helpers/ProjectDataTableHelper.php @@ -23,18 +23,18 @@ namespace App\DataTables\Helpers; -use App\Entity\AssemblySystem\Assembly; +use App\Entity\ProjectSystem\Project; use App\Services\EntityURLGenerator; /** * A helper service which contains common code to render columns for assembly related tables */ -class AssemblyDataTableHelper +class ProjectDataTableHelper { public function __construct(private readonly EntityURLGenerator $entityURLGenerator) { } - public function renderName(Assembly $context): string + public function renderName(Project $context): string { $icon = ''; diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index 1c7a09e49..89572a8ab 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -25,9 +25,7 @@ use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; -use App\DataTables\Helpers\AssemblyDataTableHelper; use App\DataTables\Helpers\PartDataTableHelper; -use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\ProjectBOMEntry; @@ -46,7 +44,6 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface public function __construct( protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, - protected AssemblyDataTableHelper $assemblyDataTableHelper, protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter ) { @@ -90,26 +87,16 @@ public function configure(DataTable $dataTable, array $options): void 'label' => $this->translator->trans('part.table.name'), 'orderField' => 'NATSORT(part.name)', 'render' => function ($value, ProjectBOMEntry $context) { - if(!$context->getPart() instanceof Part && !$context->getAssembly() instanceof Assembly) { + if(!$context->getPart() instanceof Part) { return htmlspecialchars((string) $context->getName()); } - if ($context->getPart() !== null) { - $tmp = $this->partDataTableHelper->renderName($context->getPart()); - $tmp = $this->translator->trans('part.table.name.value.for_part', ['%value%' => $tmp]); - - if($context->getName() !== null && $context->getName() !== '') { - $tmp .= '
                                                      '.htmlspecialchars($context->getName()).''; - } - } elseif ($context->getAssembly() !== null) { - $tmp = $this->assemblyDataTableHelper->renderName($context->getAssembly()); - $tmp = $this->translator->trans('part.table.name.value.for_assembly', ['%value%' => $tmp]); - - if($context->getName() !== null && $context->getName() !== '') { - $tmp .= '
                                                      '.htmlspecialchars($context->getName()).''; - } - } + //Part exists if we reach this point + $tmp = $this->partDataTableHelper->renderName($context->getPart()); + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
                                                      '.htmlspecialchars($context->getName()).''; + } return $tmp; }, ]) @@ -121,6 +108,8 @@ public function configure(DataTable $dataTable, array $options): void if($context->getPart() instanceof Part) { return $context->getPart()->getIpn(); } + + return ''; } ]) ->add('description', MarkdownColumn::class, [ diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 17a6868f0..54305a6fa 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -22,7 +22,6 @@ namespace App\Entity\AssemblySystem; -use App\Repository\AssemblyRepository; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; @@ -57,7 +56,7 @@ * * @extends AbstractStructuralDBElement */ -#[ORM\Entity(repositoryClass: AssemblyRepository::class)] +#[ORM\Entity] #[ORM\Table(name: 'assemblies')] #[ApiResource( operations: [ @@ -108,6 +107,7 @@ class Assembly extends AbstractStructuralDBElement #[Groups(['extended', 'full', 'import'])] #[ORM\OneToMany(mappedBy: 'assembly', targetEntity: AssemblyBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])] + #[UniqueObjectCollection(message: 'assembly.bom_entry.project_already_in_bom', fields: ['project'])] #[UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name'])] protected Collection $bom_entries; diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 375fef040..f6ede2194 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -36,7 +36,7 @@ use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Contracts\TimeStampableInterface; -use App\Entity\AssemblySystem\Assembly; +use App\Entity\ProjectSystem\Project; use App\Repository\DBElementRepository; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; @@ -133,6 +133,18 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt #[Groups(['bom_entry:read', 'bom_entry:write', 'full'])] protected ?Part $part = null; + /** + * @var Project|null The associated project + */ + #[Assert\Expression( + '(this.getPart() === null or this.getProject() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))', + message: 'validator.project.bom_entry.only_part_or_assembly_allowed' + )] + #[ORM\ManyToOne(targetEntity: Project::class)] + #[ORM\JoinColumn(name: 'id_project', nullable: true)] + #[Groups(['bom_entry:read', 'bom_entry:write', ])] + protected ?Project $project = null; + /** * @var BigDecimal|null The price of this non-part BOM entry */ @@ -225,6 +237,17 @@ public function setPart(?Part $part): AssemblyBOMEntry return $this; } + public function getProject(): ?Project + { + return $this->project; + } + + public function setProject(?Project $project): AssemblyBOMEntry + { + $this->project = $project; + return $this; + } + /** * Returns the price of this BOM entry, if existing. * Prices are only valid on non-Part BOM entries. @@ -297,6 +320,7 @@ public function getComparableFields(): array return [ 'name' => $this->getName(), 'part' => $this->getPart()?->getID(), + 'project' => $this->getProject()?->getID(), ]; } } diff --git a/src/Entity/ProjectSystem/Project.php b/src/Entity/ProjectSystem/Project.php index 36a96377b..a103d6946 100644 --- a/src/Entity/ProjectSystem/Project.php +++ b/src/Entity/ProjectSystem/Project.php @@ -108,7 +108,6 @@ class Project extends AbstractStructuralDBElement #[Groups(['extended', 'full', 'import'])] #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part'])] - #[UniqueObjectCollection(message: 'project.bom_entry.assembly_already_in_bom', fields: ['assembly'])] #[UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name'])] protected Collection $bom_entries; diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index 692407730..f58e4d5eb 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -35,7 +35,6 @@ use ApiPlatform\OpenApi\Model\Operation; use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; -use App\Entity\AssemblySystem\Assembly; use App\Entity\Contracts\TimeStampableInterface; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; @@ -104,10 +103,7 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte /** * @var string|null An optional name describing this BOM entry (useful for non-part entries) */ - #[Assert\Expression( - 'this.getPart() !== null or this.getAssembly() !== null or (this.getName() !== null and this.getName() != "")', - message: 'validator.project.bom_entry.part_or_assembly_needed' - )] + #[Assert\Expression('this.getPart() !== null or this.getName() !== null', message: 'validator.project.bom_entry.name_or_part_needed')] #[ORM\Column(type: Types::STRING, nullable: true)] #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] protected ?string $name = null; @@ -135,18 +131,6 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte #[Groups(['bom_entry:read', 'bom_entry:write', 'full'])] protected ?Part $part = null; - /** - * @var Assembly|null The associated assembly - */ - #[Assert\Expression( - '(this.getPart() === null or this.getAssembly() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))', - message: 'validator.project.bom_entry.only_part_or_assembly_allowed' - )] - #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'assembly_bom_entries')] - #[ORM\JoinColumn(name: 'id_assembly')] - #[Groups(['bom_entry:read', 'bom_entry:write', ])] - protected ?Assembly $assembly = null; - /** * @var BigDecimal|null The price of this non-part BOM entry */ @@ -239,16 +223,6 @@ public function setPart(?Part $part): ProjectBOMEntry return $this; } - public function getAssembly(): ?Assembly - { - return $this->assembly; - } - - public function setAssembly(?Assembly $assembly): void - { - $this->assembly = $assembly; - } - /** * Returns the price of this BOM entry, if existing. * Prices are only valid on non-Part BOM entries. @@ -286,15 +260,6 @@ public function isPartBomEntry(): bool return $this->part instanceof Part; } - /** - * Checks whether this BOM entry is a assembly associated BOM entry or not. - * @return bool True if this BOM entry is a assembly associated BOM entry, false otherwise. - */ - public function isAssemblyBomEntry(): bool - { - return $this->assembly instanceof Assembly; - } - #[Assert\Callback] public function validate(ExecutionContextInterface $context, $payload): void { @@ -356,7 +321,6 @@ public function getComparableFields(): array return [ 'name' => $this->getName(), 'part' => $this->getPart()?->getID(), - 'assembly' => $this->getAssembly()?->getID(), ]; } } diff --git a/src/Form/AssemblySystem/AssemblyBOMEntryType.php b/src/Form/AssemblySystem/AssemblyBOMEntryType.php index 9addccb34..42d463bd9 100644 --- a/src/Form/AssemblySystem/AssemblyBOMEntryType.php +++ b/src/Form/AssemblySystem/AssemblyBOMEntryType.php @@ -8,6 +8,7 @@ use App\Form\Type\BigDecimalNumberType; use App\Form\Type\CurrencyEntityType; use App\Form\Type\PartSelectType; +use App\Form\Type\ProjectSelectType; use App\Form\Type\RichTextEditorType; use App\Form\Type\SIUnitType; use Symfony\Component\Form\AbstractType; @@ -34,11 +35,13 @@ public function buildForm(FormBuilderInterface $builder, array $options): void }); $builder - ->add('part', PartSelectType::class, [ 'required' => false, ]) - + ->add('project', ProjectSelectType::class, [ + 'label' => 'assembly.bom.project', + 'required' => false, + ]) ->add('name', TextType::class, [ 'label' => 'assembly.bom.name', 'required' => false, @@ -75,10 +78,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'label' => false, 'short' => true, - ]) - - ; - + ] + ); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Form/AssemblySystem/AssemblyBuildType.php b/src/Form/AssemblySystem/AssemblyBuildType.php index 8838706d4..e87acb868 100644 --- a/src/Form/AssemblySystem/AssemblyBuildType.php +++ b/src/Form/AssemblySystem/AssemblyBuildType.php @@ -78,35 +78,34 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, ]); - - //The form is initially empty, we have to define the fields after we know the data + //The form is initially empty, define the fields after we know the data $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { $form = $event->getForm(); - /** @var AssemblyBuildRequest $build_request */ - $build_request = $event->getData(); + /** @var AssemblyBuildRequest $assemblyBuildRequest */ + $assemblyBuildRequest = $event->getData(); $form->add('addBuildsToBuildsPart', CheckboxType::class, [ 'label' => 'assembly.build.add_builds_to_builds_part', 'required' => false, - 'disabled' => !$build_request->getAssembly()->getBuildPart() instanceof Part, + 'disabled' => !$assemblyBuildRequest->getAssembly()->getBuildPart() instanceof Part, ]); - if ($build_request->getAssembly()->getBuildPart() instanceof Part) { + if ($assemblyBuildRequest->getAssembly()->getBuildPart() instanceof Part) { $form->add('buildsPartLot', PartLotSelectType::class, [ 'label' => 'assembly.build.builds_part_lot', 'required' => false, - 'part' => $build_request->getAssembly()->getBuildPart(), + 'part' => $assemblyBuildRequest->getAssembly()->getBuildPart(), 'placeholder' => 'assembly.build.buildsPartLot.new_lot' ]); } - foreach ($build_request->getPartBomEntries() as $bomEntry) { + foreach ($assemblyBuildRequest->getPartBomEntries() as $bomEntry) { //Every part lot has a field to specify the number of parts to take from this lot - foreach ($build_request->getPartLotsForBOMEntry($bomEntry) as $lot) { + foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($bomEntry) as $lot) { $form->add('lot_' . $lot->getID(), SIUnitType::class, [ 'label' => false, 'measurement_unit' => $bomEntry->getPart()->getPartUnit(), - 'max' => min($build_request->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()), + 'max' => min($assemblyBuildRequest->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()), 'disabled' => !$this->security->isGranted('withdraw', $lot), ]); } diff --git a/src/Form/ProjectSystem/ProjectAddPartsType.php b/src/Form/ProjectSystem/ProjectAddPartsType.php index c5dbe99f3..61f72c41d 100644 --- a/src/Form/ProjectSystem/ProjectAddPartsType.php +++ b/src/Form/ProjectSystem/ProjectAddPartsType.php @@ -59,7 +59,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ], 'constraints' => [ new UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part']), - new UniqueObjectCollection(message: 'project.bom_entry.assembly_already_in_bom', fields: ['assembly']), new UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name']), ] ]); diff --git a/src/Form/ProjectSystem/ProjectBOMEntryType.php b/src/Form/ProjectSystem/ProjectBOMEntryType.php index de8eb789c..44850c304 100644 --- a/src/Form/ProjectSystem/ProjectBOMEntryType.php +++ b/src/Form/ProjectSystem/ProjectBOMEntryType.php @@ -5,7 +5,6 @@ namespace App\Form\ProjectSystem; use App\Entity\ProjectSystem\ProjectBOMEntry; -use App\Form\Type\AssemblySelectType; use App\Form\Type\BigDecimalNumberType; use App\Form\Type\CurrencyEntityType; use App\Form\Type\PartSelectType; @@ -39,10 +38,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'project.bom.part', 'required' => false, ]) - ->add('assembly', AssemblySelectType::class, [ - 'label' => 'project.bom.assembly', - 'required' => false, - ]) ->add('name', TextType::class, [ 'label' => 'project.bom.name', 'required' => false, diff --git a/src/Form/ProjectSystem/ProjectBuildType.php b/src/Form/ProjectSystem/ProjectBuildType.php index d0d4e3433..b13dd12f2 100644 --- a/src/Form/ProjectSystem/ProjectBuildType.php +++ b/src/Form/ProjectSystem/ProjectBuildType.php @@ -22,7 +22,6 @@ */ namespace App\Form\ProjectSystem; -use App\Helpers\Assemblies\AssemblyBuildRequest; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; @@ -39,11 +38,10 @@ use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Contracts\Translation\TranslatorInterface; class ProjectBuildType extends AbstractType implements DataMapperInterface { - public function __construct(private readonly Security $security, private readonly TranslatorInterface $translator) + public function __construct(private readonly Security $security) { } @@ -113,25 +111,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]); } } - - foreach ($projectBuildRequest->getAssemblyBomEntries() as $bomEntry) { - $assemblyBuildRequest = new AssemblyBuildRequest($bomEntry->getAssembly(), $projectBuildRequest->getNumberOfBuilds()); - - //Add fields for assembly bom entries - foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) { - foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $lot) { - $form->add('lot_' . $lot->getID(), SIUnitType::class, [ - 'label' => $this->translator->trans('project.build.builds_part_lot_label', [ - '%name%' => $partBomEntry->getPart()->getName(), - '%quantity%' => $partBomEntry->getQuantity() * $projectBuildRequest->getNumberOfBuilds() - ]), - 'measurement_unit' => $partBomEntry->getPart()->getPartUnit(), - 'max' => min($assemblyBuildRequest->getNeededAmountForBOMEntry($partBomEntry), $lot->getAmount()), - 'disabled' => !$this->security->isGranted('withdraw', $lot), - ]); - } - } - } }); } diff --git a/src/Form/Type/AssemblySelectType.php b/src/Form/Type/ProjectSelectType.php similarity index 82% rename from src/Form/Type/AssemblySelectType.php rename to src/Form/Type/ProjectSelectType.php index 10e858f26..18a10c08c 100644 --- a/src/Form/Type/AssemblySelectType.php +++ b/src/Form/Type/ProjectSelectType.php @@ -4,9 +4,9 @@ namespace App\Form\Type; -use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; -use App\Services\Attachments\AssemblyPreviewGenerator; +use App\Entity\ProjectSystem\Project; +use App\Services\Attachments\ProjectPreviewGenerator; use App\Services\Attachments\AttachmentURLGenerator; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bridge\Doctrine\Form\Type\EntityType; @@ -20,9 +20,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -class AssemblySelectType extends AbstractType implements DataMapperInterface +class ProjectSelectType extends AbstractType implements DataMapperInterface { - public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em, private readonly AssemblyPreviewGenerator $previewGenerator, private readonly AttachmentURLGenerator $attachmentURLGenerator) + public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em, private readonly ProjectPreviewGenerator $previewGenerator, private readonly AttachmentURLGenerator $attachmentURLGenerator) { } @@ -69,28 +69,28 @@ public function buildForm(FormBuilderInterface $builder, array $options): void public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'class' => Assembly::class, + 'class' => Project::class, 'choice_label' => 'name', 'compound' => true, 'error_bubbling' => false, ]); - error_log($this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__'])); + error_log($this->urlGenerator->generate('typeahead_projects', ['query' => '__QUERY__'])); $resolver->setDefaults([ 'attr' => [ - 'data-controller' => 'elements--assembly-select', - 'data-autocomplete' => $this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']), + 'data-controller' => 'elements--project-select', + 'data-autocomplete' => $this->urlGenerator->generate('typeahead_projects', ['query' => '__QUERY__']), 'autocomplete' => 'off', ], ]); $resolver->setDefaults([ //Prefill the selected choice with the needed data, so the user can see it without an additional Ajax request - 'choice_attr' => ChoiceList::attr($this, function (?Assembly $assembly) { - if($assembly instanceof Assembly) { + 'choice_attr' => ChoiceList::attr($this, function (?Project $project) { + if($project instanceof Project) { //Determine the picture to show: - $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($assembly); + $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($project); if ($preview_attachment instanceof Attachment) { $preview_url = $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); @@ -99,8 +99,8 @@ public function configureOptions(OptionsResolver $resolver): void } } - return $assembly instanceof Assembly ? [ - 'data-description' => $assembly->getDescription() ? mb_strimwidth($assembly->getDescription(), 0, 127, '...') : '', + return $project instanceof Project ? [ + 'data-description' => $project->getDescription() ? mb_strimwidth($project->getDescription(), 0, 127, '...') : '', 'data-category' => '', 'data-footprint' => '', 'data-image' => $preview_url, diff --git a/src/Helpers/Projects/ProjectBuildRequest.php b/src/Helpers/Projects/ProjectBuildRequest.php index 3254565a5..24bb5eb78 100644 --- a/src/Helpers/Projects/ProjectBuildRequest.php +++ b/src/Helpers/Projects/ProjectBuildRequest.php @@ -22,13 +22,10 @@ */ namespace App\Helpers\Projects; -use App\Entity\AssemblySystem\Assembly; -use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; -use App\Helpers\Assemblies\AssemblyBuildRequest; use App\Validator\Constraints\ProjectSystem\ValidProjectBuildRequest; /** @@ -82,7 +79,7 @@ private function initializeArray(): void //Completely reset the array $this->withdraw_amounts = []; - //Now create an array for each part BOM entry + //Now create an array for each BOM entry foreach ($this->getPartBomEntries() as $bom_entry) { $remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry); foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) { @@ -91,21 +88,6 @@ private function initializeArray(): void $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]); } } - - //Now create an array for each assembly BOM entry - foreach ($this->getAssemblyBomEntries() as $assemblyBomEntry) { - $assemblyBuildRequest = new AssemblyBuildRequest($assemblyBomEntry->getAssembly(), $this->number_of_builds); - - //Add fields for assembly bom entries - foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) { - $remaining_amount = $assemblyBuildRequest->getNeededAmountForBOMEntry($partBomEntry) * $assemblyBomEntry->getQuantity(); - - foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $lot) { - $this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount()); - $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]); - } - } - } } /** @@ -248,77 +230,12 @@ public function getPartLotsForBOMEntry(ProjectBOMEntry $projectBOMEntry): ?array { $this->ensureBOMEntryValid($projectBOMEntry); - if (!$projectBOMEntry->getPart() instanceof Part && !$projectBOMEntry->getAssembly() instanceof Assembly) { + if (!$projectBOMEntry->getPart() instanceof Part) { return null; } //Filter out all lots which have unknown instock - if ($projectBOMEntry->getPart() instanceof Part) { - return $projectBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); - } elseif ($projectBOMEntry->getAssembly() instanceof Assembly) { - $assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds); - - //Add fields for assembly bom entries - $result = []; - foreach ($assemblyBuildRequest->getPartBomEntries() as $assemblyBOMEntry) { - $tmp = $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); - $result = array_merge($result, $tmp); - } - - return $result; - } - - return null; - } - - /** - * Returns all available assembly BOM-entries with no part assigned. - * @return AssemblyBOMEntry[]|null Returns null if no entries found - */ - public function getAssemblyBomEntriesWithoutPart(ProjectBOMEntry $projectBOMEntry): ?array - { - $this->ensureBOMEntryValid($projectBOMEntry); - - if (!$projectBOMEntry->getAssembly() instanceof Assembly) { - return null; - } - - $assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds); - - $result = []; - - foreach ($assemblyBuildRequest->getBomEntries() as $assemblyBOMEntry) { - if ($assemblyBOMEntry->getPart() === null) { - $result[] = $assemblyBOMEntry; - } - } - - return count($result) > 0 ? $result : null; - } - - /** - * Returns all available assembly BOM-entries with no part assigned. - * @return AssemblyBOMEntry[]|null Returns null if no entries found - */ - public function getAssemblyBomEntriesWithPartNoStock(ProjectBOMEntry $projectBOMEntry): ?array - { - $this->ensureBOMEntryValid($projectBOMEntry); - - if (!$projectBOMEntry->getAssembly() instanceof Assembly) { - return null; - } - - $assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds); - - $result = []; - - foreach ($assemblyBuildRequest->getBomEntries() as $assemblyBOMEntry) { - if ($assemblyBOMEntry->getPart() instanceof Part && $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->count() === 0) { - $result[] = $assemblyBOMEntry; - } - } - - return count($result) > 0 ? $result : null; + return $projectBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); } /** @@ -349,15 +266,6 @@ public function getPartBomEntries(): array return $this->project->getBomEntries()->filter(fn(ProjectBOMEntry $entry) => $entry->isPartBomEntry())->toArray(); } - /** - * Returns the all assembly bom entries that have to be built. - * @return ProjectBOMEntry[] - */ - public function getAssemblyBomEntries(): array - { - return $this->project->getBomEntries()->filter(fn(ProjectBOMEntry $entry) => $entry->isAssemblyBomEntry())->toArray(); - } - /** * Returns which project should be build */ diff --git a/src/Repository/AssemblyRepository.php b/src/Repository/AssemblyRepository.php deleted file mode 100644 index 031e6e82b..000000000 --- a/src/Repository/AssemblyRepository.php +++ /dev/null @@ -1,69 +0,0 @@ -. - */ - -declare(strict_types=1); - -/** - * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). - * - * Copyright (C) 2019 - 2022 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 . - */ - -namespace App\Repository; - -use App\Entity\AssemblySystem\Assembly; - -/** - * @template TEntityClass of Assembly - * @extends DBElementRepository - */ -class AssemblyRepository extends StructuralDBElementRepository -{ - /** - * @return Assembly[] - */ - public function autocompleteSearch(string $query, int $max_limits = 50): array - { - $qb = $this->createQueryBuilder('assembly'); - $qb->select('assembly') - ->where('ILIKE(assembly.name, :query) = TRUE') - ->orWhere('ILIKE(assembly.description, :query) = TRUE'); - - $qb->setParameter('query', '%'.$query.'%'); - - $qb->setMaxResults($max_limits); - $qb->orderBy('NATSORT(assembly.name)', 'ASC'); - - return $qb->getQuery()->getResult(); - } -} \ No newline at end of file diff --git a/src/Repository/Parts/DeviceRepository.php b/src/Repository/Parts/DeviceRepository.php index 442c91e58..c714523af 100644 --- a/src/Repository/Parts/DeviceRepository.php +++ b/src/Repository/Parts/DeviceRepository.php @@ -51,4 +51,22 @@ public function getPartsCount(object $element): int //Prevent user from deleting devices, to not accidentally remove filled devices from old versions return 1; } + + /** + * @return Project[] + */ + public function autocompleteSearch(string $query, int $max_limits = 50): array + { + $qb = $this->createQueryBuilder('project'); + $qb->select('project') + ->where('ILIKE(project.name, :query) = TRUE') + ->orWhere('ILIKE(project.description, :query) = TRUE'); + + $qb->setParameter('query', '%'.$query.'%'); + + $qb->setMaxResults($max_limits); + $qb->orderBy('NATSORT(project.name)', 'ASC'); + + return $qb->getQuery()->getResult(); + } } diff --git a/src/Services/AssemblySystem/AssemblyBuildHelper.php b/src/Services/AssemblySystem/AssemblyBuildHelper.php index 8c95a4b66..3fb3221ac 100644 --- a/src/Services/AssemblySystem/AssemblyBuildHelper.php +++ b/src/Services/AssemblySystem/AssemblyBuildHelper.php @@ -27,14 +27,17 @@ use App\Entity\Parts\Part; use App\Helpers\Assemblies\AssemblyBuildRequest; use App\Services\Parts\PartLotWithdrawAddHelper; +use App\Services\ProjectSystem\ProjectBuildHelper; /** * @see \App\Tests\Services\AssemblySystem\AssemblyBuildHelperTest */ class AssemblyBuildHelper { - public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper) - { + public function __construct( + private readonly PartLotWithdrawAddHelper $withdraw_add_helper, + private readonly ProjectBuildHelper $projectBuildHelper + ) { } /** @@ -66,12 +69,16 @@ public function getMaximumBuildableCount(Assembly $assembly): int $maximum_buildable_count = PHP_INT_MAX; foreach ($assembly->getBomEntries() as $bom_entry) { //Skip BOM entries without a part (as we can not determine that) - if (!$bom_entry->isPartBomEntry()) { + if (!$bom_entry->isPartBomEntry() && $bom_entry->getProject() === null) { continue; } - //The maximum buildable count for the whole assembly is the minimum of all BOM entries - $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); + //The maximum buildable count for the whole project is the minimum of all BOM entries + if ($bom_entry->getPart() !== null) { + $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); + } elseif ($bom_entry->getProject() !== null) { + $maximum_buildable_count = min($maximum_buildable_count, $this->projectBuildHelper->getMaximumBuildableCount($bom_entry->getProject())); + } } return $maximum_buildable_count; @@ -97,7 +104,7 @@ public function isBOMEntryBuildable(AssemblyBOMEntry $bom_entry, int $number_of_ } /** - * Returns the assembly BOM entries for which parts are missing in the stock for the given number of builds + * Returns the project BOM entries for which parts are missing in the stock for the given number of builds * @param Assembly $assembly The assembly for which the BOM entries should be checked * @param int $number_of_builds How often should the assembly be build? * @return AssemblyBOMEntry[] @@ -108,24 +115,29 @@ public function getNonBuildableAssemblyBomEntries(Assembly $assembly, int $numbe throw new \InvalidArgumentException('The number of builds must be greater than 0!'); } - $non_buildable_entries = []; + $nonBuildableEntries = []; foreach ($assembly->getBomEntries() as $bomEntry) { $part = $bomEntry->getPart(); //Skip BOM entries without a part (as we can not determine that) - if (!$part instanceof Part) { + if (!$part instanceof Part && $bomEntry->getAssembly() === null) { continue; } - $amount_sum = $part->getAmountSum(); + if ($bomEntry->getPart() !== null) { + $amount_sum = $part->getAmountSum(); - if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { - $non_buildable_entries[] = $bomEntry; + if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { + $nonBuildableEntries[] = $bomEntry; + } + } elseif ($bomEntry->getAssembly() !== null) { + $nonBuildableAssemblyEntries = $this->projectBuildHelper->getNonBuildableProjectBomEntries($bomEntry->getProject(), $number_of_builds); + $nonBuildableEntries = array_merge($nonBuildableEntries, $nonBuildableAssemblyEntries); } } - return $non_buildable_entries; + return $nonBuildableEntries; } /** diff --git a/src/Services/Attachments/AssemblyPreviewGenerator.php b/src/Services/Attachments/ProjectPreviewGenerator.php similarity index 75% rename from src/Services/Attachments/AssemblyPreviewGenerator.php rename to src/Services/Attachments/ProjectPreviewGenerator.php index 9ecbbd070..9929dbd3c 100644 --- a/src/Services/Attachments/AssemblyPreviewGenerator.php +++ b/src/Services/Attachments/ProjectPreviewGenerator.php @@ -22,38 +22,38 @@ namespace App\Services\Attachments; -use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; +use App\Entity\ProjectSystem\Project; -class AssemblyPreviewGenerator +class ProjectPreviewGenerator { public function __construct(protected AttachmentManager $attachmentHelper) { } /** - * Returns a list of attachments that can be used for previewing the assembly ordered by priority. + * Returns a list of attachments that can be used for previewing the project ordered by priority. * - * @param Assembly $assembly the assembly for which the attachments should be determined + * @param Project $project the project for which the attachments should be determined * * @return (Attachment|null)[] * * @psalm-return list */ - public function getPreviewAttachments(Assembly $assembly): array + public function getPreviewAttachments(Project $project): array { $list = []; //Master attachment has top priority - $attachment = $assembly->getMasterPictureAttachment(); + $attachment = $project->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { $list[] = $attachment; } - //Then comes the other images of the assembly - foreach ($assembly->getAttachments() as $attachment) { + //Then comes the other images of the project + foreach ($project->getAttachments() as $attachment) { //Dont show the master attachment twice - if ($this->isAttachmentValidPicture($attachment) && $attachment !== $assembly->getMasterPictureAttachment()) { + if ($this->isAttachmentValidPicture($attachment) && $attachment !== $project->getMasterPictureAttachment()) { $list[] = $attachment; } } @@ -62,14 +62,14 @@ public function getPreviewAttachments(Assembly $assembly): array } /** - * Determines what attachment should be used for previewing a assembly (especially in assembly table). + * Determines what attachment should be used for previewing a project (especially in project table). * The returned attachment is guaranteed to be existing and be a picture. * - * @param Assembly $assembly The assembly for which the attachment should be determined + * @param Project $project The project for which the attachment should be determined */ - public function getTablePreviewAttachment(Assembly $assembly): ?Attachment + public function getTablePreviewAttachment(Project $project): ?Attachment { - $attachment = $assembly->getMasterPictureAttachment(); + $attachment = $project->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { return $attachment; } diff --git a/src/Services/ProjectSystem/ProjectBuildHelper.php b/src/Services/ProjectSystem/ProjectBuildHelper.php index d7ba9e6c9..269c7e4c2 100644 --- a/src/Services/ProjectSystem/ProjectBuildHelper.php +++ b/src/Services/ProjectSystem/ProjectBuildHelper.php @@ -22,13 +22,10 @@ */ namespace App\Services\ProjectSystem; -use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; -use App\Helpers\Assemblies\AssemblyBuildRequest; use App\Helpers\Projects\ProjectBuildRequest; -use App\Services\AssemblySystem\AssemblyBuildHelper; use App\Services\Parts\PartLotWithdrawAddHelper; /** @@ -36,10 +33,8 @@ */ class ProjectBuildHelper { - public function __construct( - private readonly PartLotWithdrawAddHelper $withdrawAddHelper, - private readonly AssemblyBuildHelper $assemblyBuildHelper - ) { + public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper) + { } /** @@ -71,16 +66,12 @@ public function getMaximumBuildableCount(Project $project): int $maximum_buildable_count = PHP_INT_MAX; foreach ($project->getBomEntries() as $bom_entry) { //Skip BOM entries without a part (as we can not determine that) - if (!$bom_entry->isPartBomEntry() && $bom_entry->getAssembly() === null) { + if (!$bom_entry->isPartBomEntry()) { continue; } //The maximum buildable count for the whole project is the minimum of all BOM entries - if ($bom_entry->getPart() !== null) { - $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); - } elseif ($bom_entry->getAssembly() !== null) { - $maximum_buildable_count = min($maximum_buildable_count, $this->assemblyBuildHelper->getMaximumBuildableCount($bom_entry->getAssembly())); - } + $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); } return $maximum_buildable_count; @@ -106,10 +97,10 @@ public function isBOMEntryBuildable(ProjectBOMEntry $bom_entry, int $number_of_b } /** - * Returns the project or assembly BOM entries for which parts are missing in the stock for the given number of builds + * Returns the project BOM entries for which parts are missing in the stock for the given number of builds * @param Project $project The project for which the BOM entries should be checked * @param int $number_of_builds How often should the project be build? - * @return ProjectBOMEntry[]|AssemblyBOMEntry[] + * @return ProjectBOMEntry[] */ public function getNonBuildableProjectBomEntries(Project $project, int $number_of_builds = 1): array { @@ -117,29 +108,24 @@ public function getNonBuildableProjectBomEntries(Project $project, int $number_o throw new \InvalidArgumentException('The number of builds must be greater than 0!'); } - $nonBuildableEntries = []; + $non_buildable_entries = []; foreach ($project->getBomEntries() as $bomEntry) { $part = $bomEntry->getPart(); //Skip BOM entries without a part (as we can not determine that) - if (!$part instanceof Part && $bomEntry->getAssembly() === null) { + if (!$part instanceof Part) { continue; } - if ($bomEntry->getPart() !== null) { - $amount_sum = $part->getAmountSum(); + $amount_sum = $part->getAmountSum(); - if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { - $nonBuildableEntries[] = $bomEntry; - } - } elseif ($bomEntry->getAssembly() !== null) { - $nonBuildableAssemblyEntries = $this->assemblyBuildHelper->getNonBuildableAssemblyBomEntries($bomEntry->getAssembly(), $number_of_builds); - $nonBuildableEntries = array_merge($nonBuildableEntries, $nonBuildableAssemblyEntries); + if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { + $non_buildable_entries[] = $bomEntry; } } - return $nonBuildableEntries; + return $non_buildable_entries; } /** @@ -147,37 +133,22 @@ public function getNonBuildableProjectBomEntries(Project $project, int $number_o * The ProjectBuildRequest has to be validated before!! * You have to flush changes to DB afterward */ - public function doBuild(ProjectBuildRequest $projectBuildRequest): void + public function doBuild(ProjectBuildRequest $buildRequest): void { - $message = $projectBuildRequest->getComment(); - $message .= ' (Project build: '.$projectBuildRequest->getProject()->getName().')'; + $message = $buildRequest->getComment(); + $message .= ' (Project build: '.$buildRequest->getProject()->getName().')'; - foreach ($projectBuildRequest->getPartBomEntries() as $bomEntry) { - foreach ($projectBuildRequest->getPartLotsForBOMEntry($bomEntry) as $partLot) { - $amount = $projectBuildRequest->getLotWithdrawAmount($partLot); + foreach ($buildRequest->getPartBomEntries() as $bom_entry) { + foreach ($buildRequest->getPartLotsForBOMEntry($bom_entry) as $part_lot) { + $amount = $buildRequest->getLotWithdrawAmount($part_lot); if ($amount > 0) { - $this->withdrawAddHelper->withdraw($partLot, $amount, $message); - } - } - } - - foreach ($projectBuildRequest->getAssemblyBomEntries() as $bomEntry) { - $assemblyBuildRequest = new AssemblyBuildRequest($bomEntry->getAssembly(), $projectBuildRequest->getNumberOfBuilds()); - - //Add fields for assembly bom entries - foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) { - foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $partLot) { - //Read amount from build configuration of the projectBuildRequest - $amount = $projectBuildRequest->getLotWithdrawAmount($partLot); - if ($amount > 0) { - $this->withdrawAddHelper->withdraw($partLot, $amount, $message); - } + $this->withdraw_add_helper->withdraw($part_lot, $amount, $message); } } } - if ($projectBuildRequest->getAddBuildsToBuildsPart()) { - $this->withdrawAddHelper->add($projectBuildRequest->getBuildsPartLot(), $projectBuildRequest->getNumberOfBuilds(), $message); + if ($buildRequest->getAddBuildsToBuildsPart()) { + $this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message); } } } diff --git a/src/Twig/AssemblyTwigExtension.php b/src/Twig/AssemblyTwigExtension.php index 3430f7d1d..d43c201e8 100644 --- a/src/Twig/AssemblyTwigExtension.php +++ b/src/Twig/AssemblyTwigExtension.php @@ -10,14 +10,14 @@ class AssemblyTwigExtension extends AbstractExtension public function getFunctions(): array { return [ - new TwigFunction('has_assembly', [$this, 'hasAssembly']), + new TwigFunction('has_project', [$this, 'hasProject']), ]; } - public function hasAssembly(array $bomEntries): bool + public function hasProject(array $bomEntries): bool { foreach ($bomEntries as $entry) { - if ($entry->getAssembly() !== null) { + if ($entry->getProject() !== null) { return true; } } diff --git a/templates/form/collection_types_layout.html.twig b/templates/form/collection_types_layout.html.twig index 552fd5421..96b71bf0f 100644 --- a/templates/form/collection_types_layout.html.twig +++ b/templates/form/collection_types_layout.html.twig @@ -6,7 +6,7 @@ {# expand button #} {% trans %}project.bom.quantity{% endtrans %} - {% trans %}project.bom.partOrAssembly{% endtrans %} + {% trans %}project.bom.part{% endtrans %} {% trans %}project.bom.name{% endtrans %} {# Remove button #} @@ -41,21 +41,9 @@ {{ form_widget(form.quantity) }} {{ form_errors(form.quantity) }} - - {{ form_row(form.part) }} + + {{ form_widget(form.part) }} {{ form_errors(form.part) }} - - {% if form.vars.value is not null and form.vars.value.project is not null %} - {% set hasAssembly = false %} - {% if is_granted("@assemblies.read") or has_assembly(form.vars.value.project.bomEntries.toArray) %} -
                                                      - {{ form_widget(form.assembly) }} - {{ form_errors(form.assembly) }} - {% endif %} - {% elseif is_granted("@assemblies.read") %} - {{ form_widget(form.assembly) }} - {{ form_errors(form.assembly) }} - {% endif %} {{ form_widget(form.name) }} diff --git a/templates/form/collection_types_layout_assembly.html.twig b/templates/form/collection_types_layout_assembly.html.twig index c5acebda0..24964801a 100644 --- a/templates/form/collection_types_layout_assembly.html.twig +++ b/templates/form/collection_types_layout_assembly.html.twig @@ -6,7 +6,7 @@ {# expand button #} {% trans %}assembly.bom.quantity{% endtrans %} - {% trans %}assembly.bom.part{% endtrans %} + {% trans %}assembly.bom.partOrProject{% endtrans %} {% trans %}assembly.bom.name{% endtrans %} {# Remove button #} @@ -41,9 +41,21 @@ {{ form_widget(form.quantity) }} {{ form_errors(form.quantity) }} - - {{ form_widget(form.part) }} + + {{ form_row(form.part) }} {{ form_errors(form.part) }} + + {% if form.vars.value is not null and form.vars.value.assembly is not null %} + {% if is_granted("@projects.read") or has_project(form.vars.value.assembly.bomEntries.toArray) %} +
                                                      + {{ form_widget(form.project) }} + {{ form_errors(form.project) }} + {% endif %} + {% elseif is_granted("@projects.read") %} +
                                                      + {{ form_widget(form.project) }} + {{ form_errors(form.project) }} + {% endif %} {{ form_widget(form.name) }} diff --git a/templates/projects/build/_form.html.twig b/templates/projects/build/_form.html.twig index 340b86700..b25ca81eb 100644 --- a/templates/projects/build/_form.html.twig +++ b/templates/projects/build/_form.html.twig @@ -27,9 +27,7 @@ {% if bom_entry.part %} - {{ 'projects.build.form.part'|trans({'%name%': bom_entry.part.name}) }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} - {% elseif bom_entry.assembly %} - {{ 'projects.build.form.assembly'|trans({'%name%': bom_entry.assembly.name}) }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} + {{ bom_entry.part.name }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} {% else %} {{ bom_entry.name }} {% endif %} @@ -47,29 +45,9 @@ {% set lots = build_request.partLotsForBOMEntry(bom_entry) %} - {% set assemblyBomEntriesWithoutPart = build_request.assemblyBomEntriesWithoutPart(bom_entry) %} - {% set assemblyBomEntriesWithPartNoStock = build_request.assemblyBomEntriesWithPartNoStock(bom_entry) %} {% if lots is not null %} - {% set previousLabel = null %} - {% for lot in lots %} {# @var lot \App\Entity\Parts\PartLot #} - - {% set label = '' %} - {% if form["lot_"~lot.id].vars.label is defined and form["lot_"~lot.id].vars.label is not empty %} - {% set label = form["lot_"~lot.id].vars.label %} - {% endif %} - - {% if label != '' and (previousLabel is null or label != previousLabel) %} -
                                                      - -
                                                      - {% endif %} - - {% set previousLabel = label %} -
                                                      -
                                                      +
                                                      / {{ lot.amount | format_amount(lot.part.partUnit) }} {% trans %}project.builds.stocked{% endtrans %}
                                                      {% endfor %} {% endif %} - {% if assemblyBomEntriesWithoutPart is not null %} - {% for bomEntryWithoutPart in assemblyBomEntriesWithoutPart %} -
                                                      - -
                                                      -
                                                      - / {% trans %}project.builds.no_stock{% endtrans %} -
                                                      -
                                                      - {% endfor %} - {% endif %} - {% if assemblyBomEntriesWithPartNoStock is not null %} - {% for bomEntryWithPartNoStock in assemblyBomEntriesWithPartNoStock %} -
                                                      -
                                                      - -
                                                      -
                                                      - / {% trans %}project.builds.no_stock{% endtrans %} -
                                                      -
                                                      -
                                                      - {% endfor %} - {% endif %} {% endfor %} @@ -126,7 +75,7 @@ {{ form_row(form.comment) }}
                                                      -{{ form_row(form.dontCheckQuantity) }} + {{ form_row(form.dontCheckQuantity) }}
                                                      {{ form_row(form.addBuildsToBuildsPart) }} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index b0ec93321..f6e53eddd 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -4741,19 +4741,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Název
                                                      - - - project.bom.assembly - Sestava - - - - - project.bom.partOrAssembly - Výběr - - - + Part-DB1\src\DataTables\PartsDataTable.php:178 Part-DB1\src\DataTables\PartsDataTable.php:126 @@ -9798,18 +9786,6 @@ Element 3 Díl - - - project.bom.assembly - Baugruppe - - - - - project.bom.partOrAssembly - Auswahl - - project.bom.add_entry @@ -10200,12 +10176,6 @@ Element 3 k dispozici - - - project.builds.no_stock - není uveden žádný sklad - - project.builds.needed @@ -10278,13 +10248,7 @@ Element 3 Cílový inventář - - - project.build.builds_part_lot_label - %name% (%quantity% požadováno) - - - + project.builds.number_of_builds Množství sestavy @@ -13113,10 +13077,10 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz %value% (Součást) - + - part.table.name.value.for_assembly - %value% (Sestava) + part.table.name.value.for_project + %value% (Projekt) @@ -13347,12 +13311,24 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz potřebné + + + assembly.bom.delete.confirm + Opravdu chcete tuto položku smazat? + + assembly.add_parts_to_assembly Přidat součásti do sestavy + + + assembly.bom.project + Projekt + + assembly.bom.name @@ -13389,9 +13365,9 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Importovat součásti do sestavy - + - assembly.bom.part + assembly.bom.partOrProject Součást diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 4322fd9f8..24d42ac6a 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -4748,18 +4748,6 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Navn - - - project.bom.assembly - Montering - - - - - project.bom.partOrAssembly - Valg - - Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9824,18 +9812,6 @@ Element 3 Komponent - - - project.bom.assembly - Baugruppe - - - - - project.bom.partOrAssembly - Auswahl - - project.bom.add_entry @@ -10226,12 +10202,6 @@ Element 3 På lager - - - project.builds.no_stock - intet lager angivet - - project.builds.needed @@ -10304,12 +10274,6 @@ Element 3 Mål mængde - - - project.build.builds_part_lot_label - %name% (%quantity% påkrævet) - - project.builds.number_of_builds @@ -12274,10 +12238,10 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver %value% (Del) - + - part.table.name.value.for_assembly - %value% (Samlingsenhed) + part.table.name.value.for_project + %value% (Projekt) @@ -12508,12 +12472,24 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver nødvendig + + + assembly.bom.delete.confirm + Vil du virkelig slette denne post? + + assembly.add_parts_to_assembly Tilføj dele til samlingen + + + assembly.bom.project + Projekt + + assembly.bom.name @@ -12550,9 +12526,9 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Importer dele til samling - + - assembly.bom.part + assembly.bom.partOrProject Del @@ -12999,41 +12975,5 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver - - - typeahead.parts.part.name - %name% (del) - - - - - typeahead.parts.assembly.name - %name% (samling) - - - - - projects.build.form.part - Del "%name%" - - - - - projects.build.form.assembly - Samling "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% nødvendig) - - - - - projects.build.form.assembly.bom.entry.no.stock - ikke på lager - - diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 8377facc9..c25fd7e77 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -4746,10 +4746,10 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr %value% (Bauteil) - + - part.table.name.value.for_assembly - %value% (Baugruppe) + part.table.name.value.for_project + %value% (Projekt) @@ -9800,18 +9800,6 @@ Element 1 -> Element 1.2 Bauteil - - - project.bom.assembly - Baugruppe - - - - - project.bom.partOrAssembly - Auswahl - - project.bom.add_entry @@ -10202,12 +10190,6 @@ Element 1 -> Element 1.2 vorhanden - - - project.builds.no_stock - kein Lager angegeben - - project.builds.needed @@ -10280,12 +10262,6 @@ Element 1 -> Element 1.2 Ziel-Bestand - - - project.build.builds_part_lot_label - %name% (%quantity% benötigt) - - project.builds.number_of_builds @@ -13151,12 +13127,24 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön benötigt + + + assembly.bom.delete.confirm + Wollen sie diesen Eintrag wirklich löschen? + + assembly.add_parts_to_assembly Bauteile zur Baugruppe hinzufügen + + + assembly.bom.project + Projekt + + assembly.bom.name @@ -13193,10 +13181,10 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Importiere Parts für Baugruppe - + - assembly.bom.part - Bauteil + assembly.bom.partOrProject + Bauteil oder Projekt @@ -13642,42 +13630,6 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön - - - typeahead.parts.part.name - %name% (Bauteil) - - - - - typeahead.parts.assembly.name - %name% (Baugruppe) - - - - - projects.build.form.part - Bauteil "%name%" - - - - - projects.build.form.assembly - Baugruppe "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% benötigt) - - - - - projects.build.form.assembly.bom.entry.no.stock - nicht auf Lager - - part.table.actions.error diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index c97f06304..a6dda3d47 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1541,22 +1541,10 @@ %value% (Μέρος) - + - part.table.name.value.for_assembly - %value% (Συναρμολόγηση) - - - - - project.bom.assembly - Συναρμολόγηση - - - - - project.bom.partOrAssembly - Επιλογή + part.table.name.value.for_project + %value% (Έργο) @@ -1595,18 +1583,6 @@ Αρχειοθετήθηκε - - - project.builds.no_stock - δεν έχει καθοριστεί απόθεμα - - - - - project.build.builds_part_lot_label - %name% (%quantity% απαιτείται) - - assembly.label @@ -1835,12 +1811,24 @@ απαιτούμενο + + + assembly.bom.delete.confirm + Θέλετε πραγματικά να διαγράψετε αυτήν την εγγραφή; + + assembly.add_parts_to_assembly Προσθήκη εξαρτημάτων στη συναρμολόγηση + + + assembly.bom.project + έργο + + assembly.bom.name @@ -1877,9 +1865,9 @@ Εισαγωγή εξαρτημάτων συναρμολόγησης - + - assembly.bom.part + assembly.bom.partOrProject Εξάρτημα @@ -2326,41 +2314,5 @@ - - - typeahead.parts.part.name - %name% (Εξάρτημα) - - - - - typeahead.parts.assembly.name - %name% (Συναρμολόγηση) - - - - - projects.build.form.part - Εξάρτημα "%name%" - - - - - projects.build.form.assembly - Συναρμολόγηση "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% απαιτείται) - - - - - projects.build.form.assembly.bom.entry.no.stock - δεν υπάρχει στο απόθεμα - - diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index f8d762f2f..763b6aff0 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -4747,10 +4747,10 @@ If you have done this incorrectly or if a computer is no longer trusted, you can %value% (Part) - + - part.table.name.value.for_assembly - %value% (Assembly) + part.table.name.value.for_project + %value% (Project) @@ -9801,18 +9801,6 @@ Element 1 -> Element 1.2 Part - - - project.bom.assembly - Assembly - - - - - project.bom.partOrAssembly - Selection - - project.bom.add_entry @@ -10203,12 +10191,6 @@ Element 1 -> Element 1.2 stocked - - - project.builds.no_stock - no stock specified - - project.builds.needed @@ -10281,12 +10263,6 @@ Element 1 -> Element 1.2 Target lot - - - project.build.builds_part_lot_label - %name% (%quantity% needed) - - project.builds.number_of_builds @@ -13152,12 +13128,24 @@ Please note, that you can not impersonate a disabled user. If you try you will g needed + + + assembly.bom.delete.confirm + Do you really want to delete this entry? + + assembly.add_parts_to_assembly Add parts to assembly + + + assembly.bom.project + Project + + assembly.bom.name @@ -13194,9 +13182,9 @@ Please note, that you can not impersonate a disabled user. If you try you will g Import part list for assembly - + - assembly.bom.part + assembly.bom.partOrProject Part @@ -13643,42 +13631,6 @@ Please note, that you can not impersonate a disabled user. If you try you will g - - - typeahead.parts.part.name - %name% (Part) - - - - - typeahead.parts.assembly.name - %name% (Assembly) - - - - - projects.build.form.part - Part "%name%" - - - - - projects.build.form.assembly - Assembly "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% needed) - - - - - projects.build.form.assembly.bom.entry.no.stock - not in stock - - part.table.actions.error diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index ff1183f02..0b17cb8a6 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -4746,10 +4746,10 @@ Subelementos serán desplazados hacia arriba. %value% (Componente) - + - part.table.name.value.for_assembly - %value% (Ensamblaje) + part.table.name.value.for_project + %value% (Proyecto) @@ -9816,18 +9816,6 @@ Elemento 3 Componente - - - project.bom.assembly - Baugruppe - - - - - project.bom.partOrAssembly - Auswahl - - project.bom.add_entry @@ -10218,12 +10206,6 @@ Elemento 3 Almacenado - - - project.builds.no_stock - no se ha especificado stock - - project.builds.needed @@ -10296,12 +10278,6 @@ Elemento 3 Lote objetivo - - - project.build.builds_part_lot_label - %name% (se requiere %quantity%) - - project.builds.number_of_builds @@ -12644,12 +12620,24 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S necesario + + + assembly.bom.delete.confirm + ¿Realmente desea eliminar esta entrada? + + assembly.add_parts_to_assembly Añadir piezas al ensamblaje + + + assembly.bom.project + Proyecto + + assembly.bom.name @@ -12686,9 +12674,9 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Importar piezas para ensamblaje - + - assembly.bom.part + assembly.bom.partOrProject Pieza @@ -13135,42 +13123,6 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S - - - typeahead.parts.part.name - %name% (Componente) - - - - - typeahead.parts.assembly.name - %name% (Ensamblaje) - - - - - projects.build.form.part - Componente "%name%" - - - - - projects.build.form.assembly - Ensamblaje "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% necesario) - - - - - projects.build.form.assembly.bom.entry.no.stock - sin stock - - part.table.actions.error diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 9cd293219..234dee6ef 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -4709,11 +4709,11 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia %value% (Componente) - - - part.table.name.value.for_assembly - %value% (Assemblaggio) - + + + part.table.name.value.for_project + %value% (Projet) + @@ -9109,18 +9109,6 @@ exemple de ville Si vous avez des questions à propos de Part-DB , rendez vous sur <a href="%href%" class="link-external" target="_blank">Github</a> - - - project.bom.assembly - Assemblage - - - - - project.bom.partOrAssembly - Sélection - - assembly.edit.status @@ -9157,18 +9145,6 @@ exemple de ville Archivé - - - project.builds.no_stock - aucun stock indiqué - - - - - project.build.builds_part_lot_label - %name% (%quantity% requis) - - assembly.label @@ -9397,12 +9373,24 @@ exemple de ville nécessaire + + + assembly.bom.delete.confirm + Voulez-vous vraiment supprimer cette entrée ? + + assembly.add_parts_to_assembly Ajouter des pièces à l'assemblage + + + assembly.bom.project + Projet + + assembly.bom.name @@ -9439,9 +9427,9 @@ exemple de ville Importer des pièces pour l'assemblage - + - assembly.bom.part + assembly.bom.partOrProject Pièce @@ -9888,41 +9876,5 @@ exemple de ville - - - typeahead.parts.part.name - %name% (pièce) - - - - - typeahead.parts.assembly.name - %name% (assemblage) - - - - - projects.build.form.part - Pièce "%name%" - - - - - projects.build.form.assembly - Assemblage "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% nécessaires) - - - - - projects.build.form.assembly.bom.entry.no.stock - Non disponible en stock - - diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 8e39c0313..57654f44e 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -4748,11 +4748,11 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi %value% (Componente) - - - part.table.name.value.for_assembly - %value% (Assemblaggio) - + + + part.table.name.value.for_project + %value% (Progetto) + @@ -9818,18 +9818,6 @@ Element 3 Componente - - - project.bom.assembly - Assemblaggio - - - - - project.bom.partOrAssembly - Selezione - - project.bom.add_entry @@ -10220,12 +10208,6 @@ Element 3 a magazzino - - - project.builds.no_stock - nessuna scorta specificata - - project.builds.needed @@ -10298,12 +10280,6 @@ Element 3 Lotto target - - - project.build.builds_part_lot_label - %name% (%quantity% richiesti) - - project.builds.number_of_builds @@ -12646,12 +12622,24 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a necessari + + + assembly.bom.delete.confirm + Vuoi davvero eliminare questa voce? + + assembly.add_parts_to_assembly Aggiungi componenti al gruppo + + + assembly.bom.project + Progetto + + assembly.bom.name @@ -12688,9 +12676,9 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Importa componenti per il gruppo - + - assembly.bom.part + assembly.bom.partOrProject Componente @@ -13137,42 +13125,6 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a - - - typeahead.parts.part.name - %name% (componente) - - - - - typeahead.parts.assembly.name - %name% (gruppo) - - - - - projects.build.form.part - Componente "%name%" - - - - - projects.build.form.assembly - Gruppo "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% necessari) - - - - - projects.build.form.assembly.bom.entry.no.stock - Non disponibile in magazzino - - part.table.actions.error diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index fb64a0f04..2ff7a7446 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -4709,11 +4709,11 @@ %value%(部品) - - - part.table.name.value.for_assembly - %value%(アセンブリ) - + + + part.table.name.value.for_project + %value%(プロジェクト) + @@ -8846,18 +8846,6 @@ Exampletown Part-DBについての質問は、<a href="%href%" class="link-external" target="_blank">GitHub</a> にスレッドがあります。 - - - project.bom.assembly - アセンブリ - - - - - project.bom.partOrAssembly - 選択 - - assembly.edit.status @@ -8894,18 +8882,6 @@ Exampletown アーカイブ済み - - - project.builds.no_stock - nessuna scorta specificata - - - - - project.build.builds_part_lot_label - %name% (必要数: %quantity%) - - assembly.label @@ -9134,12 +9110,48 @@ Exampletown 必要数量 + + + assembly.bom.delete.confirm + 本当にこのエントリを削除しますか? + + assembly.add_parts_to_assembly アセンブリに部品を追加 + + + assembly.bom.project + プロジェクト + + + + + assembly.bom.name + 名前 + + + + + assembly.bom.comment + コメント + + + + + assembly.builds.following_bom_entries_miss_instock_n + このアセンブリを%number_of_builds%回作成するための部品が十分に在庫にありません。以下の部品が不足しています: + + + + + assembly.build.help + どの在庫から必要な部品を取り出すか(およびその数量)を選択してください。部品を取り出した場合は、各項目のチェックをオンにするか、最上部のチェックボックスを使って一括でオンにすることができます。 + + assembly.build.required_qty @@ -9589,41 +9601,5 @@ Exampletown - - - typeahead.parts.part.name - %name%(部品) - - - - - typeahead.parts.assembly.name - %name%(アセンブリ) - - - - - projects.build.form.part - 部品「%name%」 - - - - - projects.build.form.assembly - アセンブリ「%name%」 - - - - - projects.build.form.assembly.bom.entry - %name% (必要数量: %quantity%) - - - - - projects.build.form.assembly.bom.entry.no.stock - 在庫なし - - diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 7cca59753..be8728ce5 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -730,22 +730,10 @@ %value% (Onderdeel) - + - part.table.name.value.for_assembly - %value% (Samenstelling) - - - - - project.bom.assembly - Assemblage - - - - - project.bom.partOrAssembly - Selectie + part.table.name.value.for_project + %value% (Project) @@ -784,18 +772,6 @@ Αρχειοθετήθηκε - - - project.builds.no_stock - geen voorraad opgegeven - - - - - project.build.builds_part_lot_label - %name% (%quantity% vereist) - - assembly.label @@ -1024,12 +1000,24 @@ Nodig + + + assembly.bom.delete.confirm + Weet u zeker dat u dit item wilt verwijderen? + + assembly.add_parts_to_assembly Onderdelen toevoegen aan assemblage + + + assembly.bom.project + Project + + assembly.bom.name @@ -1066,9 +1054,9 @@ Importeer onderdelen voor assemblage - + - assembly.bom.part + assembly.bom.partOrProject Onderdeel @@ -1551,41 +1539,5 @@ - - - typeahead.parts.part.name - %name% (Onderdeel) - - - - - typeahead.parts.assembly.name - %name% (Assemblage) - - - - - projects.build.form.part - Onderdelen "%name%" - - - - - projects.build.form.assembly - Assemblage "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% benodigd) - - - - - projects.build.form.assembly.bom.entry.no.stock - niet op voorraad - - diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 86894d998..d48e0d930 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -4751,11 +4751,11 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo %value%(部品) - - - part.table.name.value.for_assembly - %value%(アセンブリ) - + + + part.table.name.value.for_project + %value% (Projekt) + @@ -9821,18 +9821,6 @@ Element 3 Komponent - - - project.bom.assembly - Zespół - - - - - project.bom.partOrAssembly - Wybór - - project.bom.add_entry @@ -10223,12 +10211,6 @@ Element 3 dostępny - - - project.builds.no_stock - brak podanego stanu magazynowego - - project.builds.needed @@ -10301,12 +10283,6 @@ Element 3 Partia docelowa - - - project.build.builds_part_lot_label - %name% (%quantity% wymagane) - - project.builds.number_of_builds @@ -12523,12 +12499,24 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli potrzebne + + + assembly.bom.delete.confirm + Czy na pewno chcesz usunąć ten element? + + assembly.add_parts_to_assembly Dodaj części do zespołu + + + assembly.bom.project + Projekt + + assembly.bom.name @@ -12565,9 +12553,9 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Importuj części dla zespołu - + - assembly.bom.part + assembly.bom.partOrProject Część @@ -13014,41 +13002,5 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli - - - typeahead.parts.part.name - %name% (część) - - - - - typeahead.parts.assembly.name - %name% (zespół) - - - - - projects.build.form.part - Część "%name%" - - - - - projects.build.form.assembly - Zespół "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (wymagana ilość: %quantity%) - - - - - projects.build.form.assembly.bom.entry.no.stock - brak na magazynie - - diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index ac04ed290..f27cd8586 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -4757,11 +4757,11 @@ %value% (Часть) - - - part.table.name.value.for_assembly - %value% (Сборка) - + + + part.table.name.value.for_project + %value% (Проект) + @@ -9825,18 +9825,6 @@ Компонент - - - project.bom.assembly - Сборка - - - - - project.bom.partOrAssembly - Выбор - - project.bom.add_entry @@ -10227,12 +10215,6 @@ запасено - - - project.builds.no_stock - склад не указан - - project.builds.needed @@ -10305,12 +10287,6 @@ Целевой лот - - - project.build.builds_part_lot_label - %name% (требуется: %quantity%) - - project.builds.number_of_builds @@ -12623,12 +12599,24 @@ Необходимо + + + assembly.bom.delete.confirm + Вы действительно хотите удалить этот элемент? + + assembly.add_parts_to_assembly Добавить детали в сборку + + + assembly.bom.project + Проект + + assembly.bom.name @@ -12665,9 +12653,9 @@ Импортировать детали для сборки - + - assembly.bom.part + assembly.bom.partOrProject Компонент @@ -13114,41 +13102,5 @@ - - - typeahead.parts.part.name - %name% (Деталь) - - - - - typeahead.parts.assembly.name - %name% (Сборка) - - - - - projects.build.form.part - Компонент "%name%" - - - - - projects.build.form.assembly - Сборка "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (необходимо: %quantity%) - - - - - projects.build.form.assembly.bom.entry.no.stock - Нет на складе - - diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 426566225..9fd6855a0 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -4755,11 +4755,11 @@ %value%(部件) - - - part.table.name.value.for_assembly - %value%(组件) - + + + part.table.name.value.for_project + %value%(项目) + @@ -9824,18 +9824,6 @@ Element 3 部件 - - - project.bom.assembly - 装配 - - - - - project.bom.partOrAssembly - 选择 - - project.bom.add_entry @@ -10226,12 +10214,6 @@ Element 3 在库 - - - project.builds.no_stock - 未指定库存 - - project.builds.needed @@ -10304,12 +10286,6 @@ Element 3 目标批次 - - - project.build.builds_part_lot_label - %name% (需求数量: %quantity%) - - project.builds.number_of_builds @@ -12508,12 +12484,24 @@ Element 3 需要 + + + assembly.bom.delete.confirm + 您确定要删除此项目吗? + + assembly.add_parts_to_assembly 添加零件到组件 + + + assembly.bom.project + 项目 + + assembly.bom.name @@ -12550,9 +12538,9 @@ Element 3 导入组件的零件 - + - assembly.bom.part + assembly.bom.partOrProject 零件 @@ -12999,41 +12987,5 @@ Element 3 - - - typeahead.parts.part.name - %name%(零件) - - - - - typeahead.parts.assembly.name - %name%(组件) - - - - - projects.build.form.part - 零件“%name%” - - - - - projects.build.form.assembly - 组件“%name%” - - - - - projects.build.form.assembly.bom.entry - %name%(需数量:%quantity%) - - - - - projects.build.form.assembly.bom.entry.no.stock - 库存不足 - - diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 699b5d2f3..ee69c98c5 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -389,6 +389,12 @@ Tato součást již existuje ve skupině! + + + assembly.bom_entry.project_already_in_bom + Tento projekt již v této skupině existuje! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 056871bbf..8494e436e 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -365,6 +365,12 @@ Denne del eksisterer allerede i gruppen! + + + assembly.bom_entry.project_already_in_bom + Dette projekt eksisterer allerede i gruppen! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 8771a0e65..23794c3a4 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -389,6 +389,12 @@ Dieses Bauteil existiert bereits in der Gruppe! + + + assembly.bom_entry.project_already_in_bom + Dieses Projekt existiert bereits in der Gruppe! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index bb78c7994..e04b9dae3 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -31,6 +31,12 @@ Αυτό το εξάρτημα υπάρχει ήδη στην ομάδα! + + + assembly.bom_entry.project_already_in_bom + Αυτό το έργο υπάρχει ήδη στην ομάδα! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 4c53ed187..ef4606ba6 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -386,19 +386,25 @@ assembly.bom_entry.part_already_in_bom - __assembly.bom_entry.part_already_in_bom + This part already exists in the list! + + + + + assembly.bom_entry.project_already_in_bom + This project already exists in the list! assembly.bom_entry.name_already_in_bom - __assembly.bom_entry.name_already_in_bom + There is already a part with this name! validator.assembly.bom_entry.name_or_part_needed - __validator.assembly.bom_entry.name_or_part_needed + You must select a part or set a name for the entry! diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index 957a47916..e603bdaf8 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -227,6 +227,12 @@ Cette pièce existe déjà dans le groupe! + + + assembly.bom_entry.project_already_in_bom + Ce projet existe déjà dans le groupe! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 639dff8bc..4df9c735b 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -383,6 +383,12 @@ Ovaj dio već postoji u grupi! + + + assembly.bom_entry.project_already_in_bom + Ovaj projekt već postoji u grupi! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index a1b9b2f09..cbc331d57 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -383,6 +383,12 @@ Questa parte è già presente nel gruppo! + + + assembly.bom_entry.project_already_in_bom + Questo progetto esiste già nel gruppo! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index 4a36a79ac..070281ccc 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -227,6 +227,12 @@ この部品はすでにグループに存在します! + + + assembly.bom_entry.project_already_in_bom + このプロジェクトは既にグループに存在しています! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index e80dd23bb..1ef74c8eb 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -383,6 +383,12 @@ Ten element już istnieje w grupie! + + + assembly.bom_entry.project_already_in_bom + Ten projekt już znajduje się w grupie! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 48f0737eb..a878cc931 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -383,6 +383,12 @@ Эта деталь уже существует в группе! + + + assembly.bom_entry.project_already_in_bom + Этот проект уже находится в группе! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index dea45ccc9..3ac139f16 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -371,6 +371,12 @@ 此零件已存在于组中! + + + assembly.bom_entry.project_already_in_bom + 该项目已在组中! + + assembly.bom_entry.name_already_in_bom From 902f6dcbe19438504c50f3d9ab52e43df2ce0e93 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 17 Jun 2025 14:02:30 +0200 Subject: [PATCH 12/83] SQL-Formatierung in Migration verbessern --- migrations/Version20250304081039.php | 99 +++++++++++++++++++++++----- migrations/Version20250304154507.php | 24 +++++-- 2 files changed, 101 insertions(+), 22 deletions(-) diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php index 755ae2360..c0fc08d96 100644 --- a/migrations/Version20250304081039.php +++ b/migrations/Version20250304081039.php @@ -16,26 +16,93 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { - $this->addSql('CREATE TABLE assemblies (id INT AUTO_INCREMENT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, name VARCHAR(255) NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, order_quantity INT NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts TINYINT(1) NOT NULL, description LONGTEXT NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_5F3832C0727ACA70 (parent_id), INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('CREATE TABLE assembly_bom_entries (id INT AUTO_INCREMENT NOT NULL, id_assembly INT DEFAULT NULL, id_part INT DEFAULT NULL, id_project INT DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames LONGTEXT NOT NULL, name VARCHAR(255) DEFAULT NULL, comment LONGTEXT NOT NULL, price NUMERIC(11, 5) DEFAULT NULL, price_currency_id INT DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_8C74887E2F180363 (id_assembly), INDEX IDX_8C74887EC22F6CC4 (id_part), INDEX IDX_8C74887EF12E799E (id_project), INDEX IDX_8C74887E3FFDCD60 (price_currency_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id)'); - $this->addSql('ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES `attachments` (id) ON DELETE SET NULL'); - $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E2F180363 FOREIGN KEY (id_assembly) REFERENCES assemblies (id)'); - $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES `parts` (id)'); - $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES `projects` (id)'); - $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id)'); + $this->addSql(<<<'SQL' + CREATE TABLE assemblies ( + id INT AUTO_INCREMENT NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, + name VARCHAR(255) NOT NULL, + comment LONGTEXT NOT NULL, + not_selectable TINYINT(1) NOT NULL, + alternative_names LONGTEXT DEFAULT NULL, + order_quantity INT NOT NULL, + status VARCHAR(64) DEFAULT NULL, + order_only_missing_parts TINYINT(1) NOT NULL, + description LONGTEXT NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + INDEX IDX_5F3832C0727ACA70 (parent_id), + INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), + PRIMARY KEY(id) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE assembly_bom_entries ( + id INT AUTO_INCREMENT NOT NULL, + id_assembly INT DEFAULT NULL, + id_part INT DEFAULT NULL, + id_project INT DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames LONGTEXT NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment LONGTEXT NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + price_currency_id INT DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + INDEX IDX_8C74887E2F180363 (id_assembly), + INDEX IDX_8C74887EC22F6CC4 (id_part), + INDEX IDX_8C74887EF12E799E (id_project), + INDEX IDX_8C74887E3FFDCD60 (price_currency_id), + PRIMARY KEY(id) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES `attachments` (id) ON DELETE SET NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E2F180363 FOREIGN KEY (id_assembly) REFERENCES assemblies (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES `parts` (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES `projects` (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) + SQL); } public function mySQLDown(Schema $schema): void { - $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0727ACA70'); - $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0EA7100A1'); - $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E2F180363'); - $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EC22F6CC4'); - $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EF12E799E'); - $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E3FFDCD60'); - $this->addSql('DROP TABLE assemblies'); - $this->addSql('DROP TABLE assembly_bom_entries'); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0727ACA70 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0EA7100A1 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E2F180363 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EC22F6CC4 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EF12E799E + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E3FFDCD60 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assemblies + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assembly_bom_entries + SQL); } public function sqLiteUp(Schema $schema): void diff --git a/migrations/Version20250304154507.php b/migrations/Version20250304154507.php index 904a3b658..4f7fed2f4 100644 --- a/migrations/Version20250304154507.php +++ b/migrations/Version20250304154507.php @@ -17,16 +17,28 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { - $this->addSql('ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON parts (built_assembly_id)'); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON parts (built_assembly_id) + SQL); } public function mySQLDown(Schema $schema): void { - $this->addSql('ALTER TABLE `parts` DROP FOREIGN KEY FK_6940A7FECC660B3C'); - $this->addSql('DROP INDEX UNIQ_6940A7FECC660B3C ON `parts`'); - $this->addSql('ALTER TABLE `parts` DROP built_assembly_id'); + $this->addSql(<<<'SQL' + ALTER TABLE parts DROP FOREIGN KEY FK_6940A7FECC660B3C + SQL); + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_6940A7FECC660B3C ON parts + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE `parts` DROP built_assembly_id + SQL); } public function sqLiteUp(Schema $schema): void From 47ec342d61cf94a7298b352ea9a0f713564b548a Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 24 Jun 2025 09:42:12 +0200 Subject: [PATCH 13/83] Projekt-Importer um JSON/CSV Importer analog zu Assemblies erweitern --- src/Controller/ProjectController.php | 42 ++- src/Entity/ProjectSystem/ProjectBOMEntry.php | 3 +- .../ImportExportSystem/BOMImporter.php | 51 ++- templates/assemblies/import_bom.html.twig | 1 - templates/projects/import_bom.html.twig | 96 ++++- translations/messages.cs.xlf | 329 ++++++++++++++++- translations/messages.da.xlf | 331 +++++++++++++++++- translations/messages.de.xlf | 331 +++++++++++++++++- translations/messages.el.xlf | 4 +- translations/messages.en.xlf | 329 ++++++++++++++++- translations/messages.es.xlf | 329 ++++++++++++++++- translations/messages.fr.xlf | 16 +- translations/messages.it.xlf | 329 ++++++++++++++++- translations/messages.ja.xlf | 4 +- translations/messages.nl.xlf | 4 +- translations/messages.pl.xlf | 329 ++++++++++++++++- translations/messages.ru.xlf | 329 ++++++++++++++++- translations/messages.zh.xlf | 329 ++++++++++++++++- 18 files changed, 3124 insertions(+), 62 deletions(-) diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 2a6d19ee2..2106098c6 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -46,14 +46,16 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Validator\Validator\ValidatorInterface; - +use Symfony\Contracts\Translation\TranslatorInterface; use function Symfony\Component\Translation\t; #[Route(path: '/project')] class ProjectController extends AbstractController { - public function __construct(private readonly DataTableFactory $dataTableFactory) - { + public function __construct( + private readonly DataTableFactory $dataTableFactory, + private readonly TranslatorInterface $translator, + ) { } #[Route(path: '/{id}/info', name: 'project_info', requirements: ['id' => '\d+'])] @@ -147,6 +149,8 @@ public function importBOM( 'label' => 'project.bom_import.type', 'required' => true, 'choices' => [ + 'project.bom_import.type.json' => 'json', + 'project.bom_import.type.csv' => 'csv', 'project.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew', 'project.bom_import.type.kicad_schematic' => 'kicad_schematic', 'project.bom_import.type.generic_csv' => 'generic_csv', @@ -193,13 +197,20 @@ public function importBOM( 'type' => $import_type, ]); + $importerResult = $BOMImporter->importFileIntoProject($form->get('file')->getData(), $project, [ + 'type' => $import_type, + ]); + // Validate the project entries $errors = $validator->validateProperty($project, 'bom_entries'); - // If no validation errors occurred, save the changes and redirect to edit page - if (count($errors) === 0) { + //If no validation errors occurred, save the changes and redirect to edit page + if (count($errors) === 0 && $importerResult->getViolations()->count() === 0) { + $entries = $importerResult->getBomEntries(); + $this->addFlash('success', t('project.bom_import.flash.success', ['%count%' => count($entries)])); $entityManager->flush(); + return $this->redirectToRoute('project_edit', ['id' => $project->getID()]); } @@ -211,10 +222,29 @@ public function importBOM( } } + $jsonTemplate = [ + [ + "quantity" => 1.0, + "name" => $this->translator->trans('project.bom_import.template.entry.name'), + "part" => [ + "id" => null, + "ipn" => $this->translator->trans('project.bom_import.template.entry.part.ipn'), + "mpnr" => $this->translator->trans('project.bom_import.template.entry.part.mpnr'), + "name" => $this->translator->trans('project.bom_import.template.entry.part.name'), + "manufacturer" => [ + "id" => null, + "name" => $this->translator->trans('project.bom_import.template.entry.part.manufacturer.name') + ], + ] + ] + ]; + return $this->render('projects/import_bom.html.twig', [ 'project' => $project, + 'jsonTemplate' => $jsonTemplate, 'form' => $form, - 'errors' => $errors ?? null, + 'validationErrors' => $errors ?? null, + 'importerErrors' => isset($importerResult) ? $importerResult->getViolations() : null, ]); } diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index f58e4d5eb..b2a3b2e95 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -36,6 +36,7 @@ use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Contracts\TimeStampableInterface; +use App\Repository\DBElementRepository; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; @@ -54,7 +55,7 @@ * The ProjectBOMEntry class represents an entry in a project's BOM. */ #[ORM\HasLifecycleCallbacks] -#[ORM\Entity] +#[ORM\Entity(repositoryClass: DBElementRepository::class)] #[ORM\Table('project_bom_entries')] #[ApiResource( operations: [ diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 2f6236afc..e345e595c 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -52,6 +52,7 @@ class BOMImporter private const IMPORT_TYPE_JSON = 'json'; private const IMPORT_TYPE_CSV = 'csv'; private const IMPORT_TYPE_KICAD_PCB = 'kicad_pcbnew'; + private const IMPORT_TYPE_KICAD_SCHEMATIC = 'kicad_schematic'; private const MAP_KICAD_PCB_FIELDS = [ 0 => 'Id', @@ -80,7 +81,7 @@ public function __construct( protected function configureOptions(OptionsResolver $resolver): OptionsResolver { $resolver->setRequired('type'); - $resolver->setAllowedValues('type', ['kicad_pcbnew', 'kicad_schematic', 'json']); + $resolver->setAllowedValues('type', [self::IMPORT_TYPE_KICAD_PCB, self::IMPORT_TYPE_KICAD_SCHEMATIC, self::IMPORT_TYPE_JSON, self::IMPORT_TYPE_CSV]); // For flexible schematic import with field mapping $resolver->setDefined(['field_mapping', 'field_priorities', 'delimiter']); @@ -96,18 +97,19 @@ protected function configureOptions(OptionsResolver $resolver): OptionsResolver /** * Converts the given file into an array of BOM entries using the given options and save them into the given project. * The changes are not saved into the database yet. - * @return ProjectBOMEntry[] */ - public function importFileIntoProject(File $file, Project $project, array $options): array + public function importFileIntoProject(UploadedFile $file, Project $project, array $options): ImporterResult { - $bom_entries = $this->fileToBOMEntries($file, $options); + $importerResult = $this->fileToImporterResult($file, $options); - //Assign the bom_entries to the project - foreach ($bom_entries as $bom_entry) { - $project->addBomEntry($bom_entry); + if ($importerResult->getViolations()->count() === 0) { + //Assign the bom_entries to the project + foreach ($importerResult->getBomEntries() as $bomEntry) { + $project->addBomEntry($bomEntry); + } } - return $bom_entries; + return $importerResult; } /** @@ -170,7 +172,7 @@ public function fileToImporterResult(UploadedFile $file, array $options, string $fileExtension, [ '%extension%' => $fileExtension, - '%importType%' => $this->translator->trans('assembly.bom_import.type.'.$options['type']), + '%importType%' => $this->translator->trans($objectType === ProjectBOMEntry::class ? 'project.bom_import.type.'.$options['type'] : 'assembly.bom_import.type.'.$options['type']), '%allowedExtensions%' => implode(', ', $validExtensions), ] )); @@ -211,9 +213,9 @@ public function stringToBOMEntries(string $data, array $options, string $objectT $options = $resolver->resolve($options); return match ($options['type']) { - 'kicad_pcbnew' => $this->parseKiCADPCB($data, $objectType), - 'kicad_schematic' => $this->parseKiCADSchematic($data, $options), - default => throw new InvalidArgumentException('Invalid import type!'), + self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $objectType)->getBomEntries(), + self::IMPORT_TYPE_KICAD_SCHEMATIC => $this->parseKiCADSchematic($data, $options), + default => throw new InvalidArgumentException($this->translator->trans('validator.bom_importer.invalid_import_type', [], 'validators')), }; } @@ -697,7 +699,7 @@ private function processPart(array $entry, ImporterResult $result, int $key, str $category = $categoryIdValid ? $this->categoryRepository->findOneBy(['id' => $entry['part']['category']['id']]) : null; $category = $category ?? ($categoryNameValid ? $this->categoryRepository->findOneBy(['name' => trim($entry['part']['category']['name'])]) : null); - if (($categoryIdValid || $categoryNameValid) && $category === null) { + if (($categoryIdValid || $categoryNameValid)) { $value = sprintf( 'category.id: %s, category.name: %s', isset($entry['part']['category']['id']) && $entry['part']['category']['id'] !== null ? '' . $entry['part']['category']['id'] . '' : '-', @@ -734,12 +736,12 @@ private function processPart(array $entry, ImporterResult $result, int $key, str $part->setDescription($partDescription); } - if ($manufacturer !== null && $manufacturer->getID() !== $part->getManufacturerID()) { + if ($manufacturer !== null && $manufacturer->getID() !== $part->getManufacturer()->getID()) { //When updating the associated parts, take over to a assembly of the manufacturer of the part. $part->setManufacturer($manufacturer); } - if ($category !== null && $category->getID() !== $part->getCategoryID()) { + if ($category !== null && $category->getID() !== $part->getCategory()->getID()) { //When updating the associated parts to a assembly, take over the category of the part. $part->setCategory($category); } @@ -757,11 +759,26 @@ private function processPart(array $entry, ImporterResult $result, int $key, str } } } else { - $bomEntry = new ProjectBOMEntry(); + $bomEntry = $this->projectBOMEntryRepository->findOneBy(['part' => $part]); + + if ($bomEntry === null) { + if (isset($entry['name']) && $entry['name'] !== '') { + $bomEntry = $this->projectBOMEntryRepository->findOneBy(['name' => $entry['name']]); + } + + if ($bomEntry === null) { + $bomEntry = new ProjectBOMEntry(); + } + } } $bomEntry->setQuantity((float) $entry['quantity']); - $bomEntry->setName($entry['name'] ?? ''); + + if (isset($entry['name'])) { + $bomEntry->setName(trim($entry['name']) === '' ? null : trim ($entry['name'])); + } else { + $bomEntry->setName(null); + } $bomEntry->setPart($part); diff --git a/templates/assemblies/import_bom.html.twig b/templates/assemblies/import_bom.html.twig index 9e99c5417..89f504c2f 100644 --- a/templates/assemblies/import_bom.html.twig +++ b/templates/assemblies/import_bom.html.twig @@ -29,7 +29,6 @@ {% endif %} {% endblock %} - {% block card_title %} {% trans %}assembly.import_bom{% endtrans %}{% if assembly %}: {{ assembly.name }}{% endif %} diff --git a/templates/projects/import_bom.html.twig b/templates/projects/import_bom.html.twig index 0e7f17875..3f9059120 100644 --- a/templates/projects/import_bom.html.twig +++ b/templates/projects/import_bom.html.twig @@ -3,29 +3,107 @@ {% block title %}{% trans %}project.import_bom{% endtrans %}{% endblock %} {% block before_card %} - {% if errors %} + {% if validationErrors or importerErrors %}

                                                      {% trans %}parts.import.errors.title{% endtrans %}

                                                        - {% for violation in errors %} -
                                                      • - {{ violation.propertyPath }}: - {{ violation.message|trans(violation.parameters, 'validators') }} -
                                                      • - {% endfor %} + {% if validationErrors %} + {% for violation in validationErrors %} +
                                                      • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators') }} +
                                                      • + {% endfor %} + {% endif %} + + {% if importerErrors %} + {% for violation in importerErrors %} +
                                                      • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators')|raw }} +
                                                      • + {% endfor %} + {% endif %}
                                                      {% endif %} {% endblock %} - {% block card_title %} {% trans %}project.import_bom{% endtrans %}{% if project %}: {{ project.name }}{% endif %} {% endblock %} {% block card_content %} - {{ form(form) }} +{% endblock %} + +{% block additional_content %} +
                                                      +
                                                      +
                                                      +
                                                      + {% trans %}project.import_bom.template.header.json{% endtrans %} +
                                                      +
                                                      +
                                                      {{ jsonTemplate|json_encode(constant('JSON_PRETTY_PRINT') b-or constant('JSON_UNESCAPED_UNICODE')) }}
                                                      + + {{ 'project.bom_import.template.json.table'|trans|raw }} +
                                                      +
                                                      +
                                                      +
                                                      +
                                                      +
                                                      + {% trans %}project.import_bom.template.header.csv{% endtrans %} +
                                                      +
                                                      + {{ 'project.bom_import.template.csv.exptected_columns'|trans }} +
                                                      quantity;name;part_id;part_mpnr;part_ipn;part_name;part_manufacturer_id;part_manufacturer_name
                                                      + +
                                                        +
                                                      • quantity
                                                      • +
                                                      • name
                                                      • +
                                                      • part_id
                                                      • +
                                                      • part_mpnr
                                                      • +
                                                      • part_ipn
                                                      • +
                                                      • part_name
                                                      • +
                                                      • part_manufacturer_id
                                                      • +
                                                      • part_manufacturer_name
                                                      • +
                                                      + + {{ 'project.bom_import.template.csv.table'|trans|raw }} +
                                                      +
                                                      +
                                                      +
                                                      +
                                                      +
                                                      + {% trans %}project.import_bom.template.header.kicad_pcbnew{% endtrans %} +
                                                      +
                                                      + {{ 'project.bom_import.template.kicad_pcbnew.exptected_columns'|trans }} +
                                                      Id;Designator;Package;Quantity;Designation;Supplier and ref
                                                      + +
                                                        +
                                                      • Id
                                                      • +
                                                      • Designator
                                                      • +
                                                      • Package
                                                      • +
                                                      • Quantity
                                                      • +
                                                      • Designation
                                                      • +
                                                      • Supplier and ref
                                                      • +
                                                      • Note
                                                      • +
                                                      • Footprint
                                                      • +
                                                      • Value
                                                      • +
                                                      • Footprint
                                                      • +
                                                      + + {{ 'project.bom_import.template.kicad_pcbnew.exptected_columns.note'|trans|raw }} + + {{ 'project.bom_import.template.kicad_pcbnew.table'|trans|raw }} +
                                                      +
                                                      +
                                                      +
                                                      {% endblock %} \ No newline at end of file diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index f6e53eddd..72331a795 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -11046,6 +11046,18 @@ Element 3 Typ
                                                      + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11064,6 +11076,319 @@ Element 3 Výběrem této možnosti odstraníte všechny existující položky BOM v projektu a přepíšete je importovaným souborem BOM! + + + project.import_bom.template.header.json + Šablona importu JSON + + + + + project.import_bom.template.header.csv + Šablona importu CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Šablona importu CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Název komponenty v projektu + + + + + project.bom_import.template.entry.part.mpnr + Jedinečné číslo produktu u výrobce + + + + + project.bom_import.template.entry.part.ipn + Jedinečné IPN součásti + + + + + project.bom_import.template.entry.part.name + Jedinečný název součásti + + + + + project.bom_import.template.entry.part.manufacturer.name + Jedinečný název výrobce + + + + + project.bom_import.template.json.table + + + + + Pole + Podmínka + Datový typ + Popis + + + + + quantity + Povinné + Desetinné číslo (Float) + Musí být zadáno a obsahovat desetinnou hodnotu (Float), která je větší než 0.0. + + + name + Volitelné + Řetězec (String) + Pokud je přítomen, musí být neprázdný řetězec. Název položky v kusovníku. + + + part + Volitelné + Objekt/Array + + Pokud je potřeba přiřadit součástku, musí to být objekt/pole a musí být vyplněno alespoň jedno z následujících polí: +
                                                        +
                                                      • part.id
                                                      • +
                                                      • part.mpnr
                                                      • +
                                                      • part.ipn
                                                      • +
                                                      • part.name
                                                      • +
                                                      + + + + part.id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID součástky v Part-DB. + + + part.mpnr + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno part.id, part.ipn nebo part.name. + + + part.ipn + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno part.id, part.mpnr nebo part.name. + + + part.name + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno part.id, part.mpnr nebo part.ipn. + + + part.manufacturer + Volitelné + Objekt/Array + + Pokud má být upraven výrobce součástky nebo pokud má být součástka nalezena jednoznačně na základě part.mpnr, musí to být objekt/pole a musí být vyplněno alespoň jedno z následujících polí: +
                                                        +
                                                      • manufacturer.id
                                                      • +
                                                      • manufacturer.name
                                                      • +
                                                      + + + + manufacturer.id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID výrobce. + + + manufacturer.name + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno manufacturer.id. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.csv.exptected_columns + Možné sloupce: + + + + + project.bom_import.template.csv.table + + + + + Sloupec + Podmínka + Datový typ + Popis + + + + + quantity + Povinné + Desetinné číslo (Float) + Musí být uvedeno a obsahovat hodnotu desetinného čísla (Float) větší než 0.0. + + + name + Optional + String + Název položky v kusovníku. + + + Sloupce začínající part_ + + Pokud má být přiřazena součástka, musí být uveden a vyplněn alespoň jeden z následujících sloupců: +
                                                        +
                                                      • part_id
                                                      • +
                                                      • part_mpnr
                                                      • +
                                                      • part_ipn
                                                      • +
                                                      • part_name
                                                      • +
                                                      + + + + part_id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID součástky v Part-DB. + + + part_mpnr + Volitelné + Řetězec (String) + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_ipn nebo part_name. + + + part_ipn + Volitelné + Řetězec (String) + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_mpnr nebo part_name. + + + part_name + Volitelné + Řetězec (String) + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_mpnr nebo part_ipn. + + + Sloupce začínající part_manufacturer_ + + Pokud má být upraven výrobce dílu nebo má být díl jednoznačně identifikován podle hodnoty part_mpnr, musí být uveden a vyplněn alespoň jeden z následujících sloupců: +
                                                        +
                                                      • part_manufacturer_id
                                                      • +
                                                      • part_manufacturer_name
                                                      • +
                                                      + + + + part_manufacturer_id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID výrobce. + + + part_manufacturer_name + Volitelné + Řetězec (String) + Musí být uvedeno, pokud není vyplněn sloupec part_manufacturer_id. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Očekávané sloupce: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Poznámka: Nedochází k přiřazení ke konkrétním součástkám ze správy kategorií.

                                                      + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.table + + + + + Pole + Podmínka + Datový typ + Popis + + + + + Id + Volitelné + Celé číslo (Integer) + Volný údaj. Jedinečné identifikační číslo pro každou součástku. + + + Designator + Volitelné + Řetězec (String) + Volný údaj. Jedinečný referenční označovač součástky na desce plošných spojů, např. „R1“ pro odpor 1.
                                                      Je převzat do osazovacího názvu záznamu součástky. + + + Package + Volitelné + Řetězec (String) + Volný údaj. Pouzdro nebo tvar součástky, např. „0805“ pro SMD odpory.
                                                      Pro záznam součástky není převzato. + + + Quantity + Povinné pole + Celé číslo (Integer) + Počet identických komponent, které jsou potřebné k vytvoření instance.
                                                      Je převzat jako počet položky komponenty. + + + Designation + Povinné pole + Řetězec (String) + Popis nebo funkce součástky, např. hodnota odporu „10kΩ“ nebo kapacita kondenzátoru „100nF“.
                                                      Je převzato do názvu záznamu součástky. + + + Supplier and ref + Volitelné + Řetězec (String) + Volný údaj. Může obsahovat např. distribuční specifickou hodnotu.
                                                      Je převzato jako poznámka ke záznamu součástky. + + + + ]]> +
                                                      +
                                                      +
                                                      project.bom_import.flash.invalid_file @@ -13616,13 +13941,13 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz - + assembly.bom_import.template.csv.exptected_columns Možné sloupce: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 24d42ac6a..108df80eb 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -11078,6 +11078,18 @@ Oversættelsen Typ + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11090,6 +11102,319 @@ Oversættelsen let eksisterende styklisteposter før import + + + project.import_bom.template.header.json + JSON-importskabelon + + + + + project.import_bom.template.header.csv + CSV-importskabelon + + + + + project.import_bom.template.header.kicad_pcbnew + CSV-importskabelon (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Komponentens navn i projektet + + + + + project.bom_import.template.entry.part.mpnr + Unikt produktnummer hos producenten + + + + + project.bom_import.template.entry.part.ipn + Komponentens unikke IPN + + + + + project.bom_import.template.entry.part.name + Komponentens unikke navn + + + + + project.bom_import.template.entry.part.manufacturer.name + Producentens unikke navn + + + + + project.bom_import.template.json.table + + + + + Felt + Betingelse + Datatype + Beskrivelse + + + + + quantity + Obligatorisk + Decimaltal (Float) + Skal være angivet og skal indeholde en decimaltalsværdi (Float), der er større end 0.0. + + + name + Valgfrit + String + Hvis til stede, skal det være en ikke-tom streng. Navnet på posten i stykliste. + + + part + Valgfrit + Objekt/Array + + Hvis en komponent skal knyttes, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: +
                                                        +
                                                      • part.id
                                                      • +
                                                      • part.mpnr
                                                      • +
                                                      • part.ipn
                                                      • +
                                                      • part.name
                                                      • +
                                                      + + + + part.id + Valgfrit + Heltal (Integer) + Heltal (Integer) > 0. Svarer til det interne numeriske ID for komponenten i Part-DB. + + + part.mpnr + Valgfrit + String + En ikke-tom streng, hvis hverken part.id, part.ipn eller part.name er angivet. + + + part.ipn + Valgfrit + String + En ikke-tom streng, hvis hverken part.id, part.mpnr eller part.name er angivet. + + + part.name + Valgfrit + String + En ikke-tom streng, hvis hverken part.id, part.mpnr eller part.ipn er angivet. + + + part.manufacturer + Valgfrit + Objekt/Array + + Hvis en komponents producent skal justeres, eller hvis komponenten skal findes entydigt via part.mpnr, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: +
                                                        +
                                                      • manufacturer.id
                                                      • +
                                                      • manufacturer.name
                                                      • +
                                                      + + + + manufacturer.id + Valgfrit + Heltal (Integer) + Heltal (Integer) > 0. Svarer til producentens interne numeriske ID. + + + manufacturer.name + Valgfrit + String + En ikke-tom streng, hvis manufacturer.id ikke er angivet. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.csv.exptected_columns + Mulige kolonner: + + + + + project.bom_import.template.csv.table + + + + + Kolonne + Betingelse + Datatype + Beskrivelse + + + + + quantity + Obligatorisk + Decimaltal (Float) + Skal være angivet og indeholde en decimaltalsværdi (Float), som er større end 0,0. + + + name + Optional + String + Hvis tilgængelig, skal det være en ikke-tom streng. Navnet på elementet inden for stykliste. + + + Kolonner, der begynder med part_ + + Hvis en komponent skal tildeles, skal mindst én af følgende kolonner være angivet og udfyldt: +
                                                        +
                                                      • part_id
                                                      • +
                                                      • part_mpnr
                                                      • +
                                                      • part_ipn
                                                      • +
                                                      • part_name
                                                      • +
                                                      + + + + part_id + Valgfri + Heltal (Integer) + Heltal (Integer) > 0. Svarer til den interne numeriske ID for komponenten i Part-DB. + + + part_mpnr + Valgfri + Streng (String) + Skal angives, hvis kolonnerne part_id, part_ipn eller part_name ikke er udfyldt. + + + part_ipn + Valgfri + Streng (String) + Skal angives, hvis kolonnerne part_id, part_mpnr eller part_name ikke er udfyldt. + + + part_name + Valgfri + Streng (String) + Skal angives, hvis kolonnerne part_id, part_mpnr eller part_ipn ikke er udfyldt. + + + Kolonner, der begynder med part_manufacturer_ + + Hvis komponentens producent skal ændres eller identificeres entydigt baseret på part_mpnr, skal mindst én af følgende kolonner være angivet og udfyldt: +
                                                        +
                                                      • part_manufacturer_id
                                                      • +
                                                      • part_manufacturer_name
                                                      • +
                                                      + + + + part_manufacturer_id + Valgfri + Heltal (Integer) + Heltal (Integer) > 0. Svarer til den interne numeriske ID for producenten. + + + part_manufacturer_name + Valgfri + Streng (String) + Skal angives, hvis kolonnen part_manufacturer_id ikke er udfyldt. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Forventede kolonner: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Bemærk: Der sker ingen tilknytning til specifikke komponenter fra kategoristyringen.

                                                      + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.table + + + + + Felt + Betingelse + Datatype + Beskrivelse + + + + + Id + Valgfrit + Heltal (Integer) + Fri opgave. Et entydigt identifikationsnummer for hver komponent. + + + Designator + Valgfrit + Streng (String) + Fri opgave. En entydig referencemarkering for komponenten på PCB'et, fx "R1" for modstand 1.
                                                      Bliver overført til monteringsnavnet på komponentindgangen. + + + Package + Valgfrit + Streng (String) + Fri opgave. Komponentens pakning eller form, fx "0805" for SMD-modstande.
                                                      Bliver ikke overført til komponentindgangen. + + + Quantity + Obligatorisk felt + Heltal (Integer) + Antallet af identiske komponenter, der kræves for at oprette en instans.
                                                      Overtages som antallet af komponentposter. + + + Designation + Obligatorisk felt + Streng (String) + Beskrivelse eller funktion af komponenten, fx modstandsværdi "10kΩ" eller kondensatorværdi "100nF".
                                                      Bliver overført til komponentindgangens navn. + + + Supplier and ref + Valgfrit + Streng (String) + Fri opgave. Kan eksempelvis indeholde en distributørspecifik værdi.
                                                      Bliver overført som en note til komponentindgangen. + + + + ]]> +
                                                      +
                                                      +
                                                      project.bom_import.clear_existing_bom.help @@ -12777,13 +13102,13 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver - + assembly.bom_import.template.csv.exptected_columns Mulige kolonner: - + assembly.bom_import.template.csv.table @@ -12808,7 +13133,7 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver name Valgfrit Streng - Navnet på posten inden for samlingen. + Navnet på posten inden for stykliste. Kolonner, der starter med part_ diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index c25fd7e77..a7137b26a 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -11060,6 +11060,18 @@ Element 1 -> Element 1.2 Typ + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11078,6 +11090,319 @@ Element 1 -> Element 1.2 Wenn diese Option ausgewählt ist, werden alle bereits im Projekt existierenden BOM Einträge gelöscht und mit den importierten BOM Daten überschrieben. + + + project.import_bom.template.header.json + Import-Vorlage JSON + + + + + project.import_bom.template.header.csv + Import-Vorlage CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Import-Vorlage CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Name des Bauteils im Projekt + + + + + project.bom_import.template.entry.part.mpnr + Eindeutige Produktnummer innerhalb des Herstellers + + + + + project.bom_import.template.entry.part.ipn + Eideutige IPN des Bauteils + + + + + project.bom_import.template.entry.part.name + Eindeutiger Name des Bauteils + + + + + project.bom_import.template.entry.part.manufacturer.name + Eindeutiger Name des Herstellers + + + + + project.bom_import.template.json.table + + + + + Feld + Bedingung + Datentyp + Beschreibung + + + + + quantity + Pflichtfeld + Gleitkommazahl (Float) + Muss gegeben sein und enthält einen Gleitkommawert (Float), der größer als 0.0 ist. + + + name + Optional + String + Falls vorhanden, muss es ein nicht-leerer String sein. Name des Eintrags innerhalb der Stückliste. + + + part + Optional + Objekt/Array + + Falls ein Bauteil zugeordnet werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein: +
                                                        +
                                                      • part.id
                                                      • +
                                                      • part.mpnr
                                                      • +
                                                      • part.ipn
                                                      • +
                                                      • part.name
                                                      • +
                                                      + + + + part.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. + + + part.mpnr + Optional + String + Nicht-leerer String, falls keine part.id-, part-ipn- bzw. part.name-Angabe gegeben ist. + + + part.ipn + Optional + String + Nicht-leerer String, falls keine part.id-, part.mpnr bzw. part.name-Angabe gegeben ist. + + + part.name + Optional + String + Nicht-leerer String, falls keine part.id-, part.mpnr- bzw. part.ipn-Angabe gegeben ist. + + + part.manufacturer + Optional + Objekt/Array + + Falls der Hersteller eines Bauteils mit angepasst werden oder das Bauteil anhand der part.mpnr-Angabe eindeutig gesucht werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein: +
                                                        +
                                                      • manufacturer.id
                                                      • +
                                                      • manufacturer.name
                                                      • +
                                                      + + + + manufacturer.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID des Herstellers. + + + manufacturer.name + Optional + String + Nicht-leerer String, falls keine manufacturer.id-Angabe gegeben ist. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.csv.exptected_columns + Mögliche Spalten: + + + + + project.bom_import.template.csv.table + + + + + Spalte + Bedingung + Datentyp + Beschreibung + + + + + quantity + Pflichtfeld + Gleitkommazahl (Float) + Muss gegeben sein und enthält einen Gleitkommawert (Float), der größer als 0.0 ist. + + + name + Optional + String + Name des Eintrags innerhalb der Stückliste. + + + Spalten beginnend mit part_ + + Falls ein Bauteil zugeordnet werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
                                                        +
                                                      • part_id
                                                      • +
                                                      • part_mpnr
                                                      • +
                                                      • part_ipn
                                                      • +
                                                      • part_name
                                                      • +
                                                      + + + + part_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. + + + part_mpnr + Optional + String + Anzugeben, falls keine part_id-, part_ipn- bzw. part_name-Spalte ausgefüllt gegeben ist. + + + part_ipn + Optional + String + Anzugeben, falls keine part_id-, part_mpnr- bzw. part_name-Spalte ausgefüllt gegeben ist. + + + part_name + Optional + String + Anzugeben, falls keine part_id-, part_mpnr- bzw. part_ipn-Spalte ausgefüllt gegeben ist. + + + Spalten beginnend mit part_manufacturer_ + + Falls der Hersteller eines Bauteils mit angepasst werden oder das Bauteil anhand der part_mpnr-Angabe eindeutig gesucht werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
                                                        +
                                                      • part_manufacturer_id
                                                      • +
                                                      • part_manufacturer_name
                                                      • +
                                                      + + + + part_manufacturer_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID des Herstellers. + + + part_manufacturer_name + Optional + String + Anzugeben, falls keine part_manufacturer_id-Spalte ausgefüllt gegeben ist. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Erwartete Spalten: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Hinweis: Es findet keine Zuordnung zu konkreten Bauteilen aus der Kategorie-Verwaltung statt.

                                                      + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.table + + + + + Feld + Bedingung + Datentyp + Beschreibung + + + + + Id + Optional + Ganzzahl (Integer) + Offene Angabe. Eine eindeutige Identifikationsnummer für jedes Bauteil. + + + Designator + Optional + String + Offene Angabe. Ein eindeutiger Referenzbezeichner des Bauteils auf der Leiterplatte, z.B. „R1“ für Widerstand 1.
                                                      Wird in den Bestückungsnamen des Bauteil-Eintrags übernommen. + + + Package + Optional + String + Offene Angabe. Das Gehäuse oder die Bauform des Bauteils, z.B. „0805“ für SMD-Widerstände.
                                                      Wird für ein Bauteil-Eintrag nicht übernommen. + + + Quantity + Pflichtfeld + Ganzzahl (Integer) + Anzahl der identischen Bauteile, die benötigt werden, um eine Instanz zu erstellen.
                                                      Wird als Anzahl des Bauteil-Eintrags übernommen. + + + Designation + Pflichtfeld + String + Beschreibung oder Funktion des Bauteils, z.B. Widerstandswert „10kΩ“ oder Kondensatorwert „100nF“.
                                                      Wird in den Namen des Bauteil-Eintrags übernommen. + + + Supplier and ref + Optional + String + Offene Angabe. Kann z.B. Distributor spezifischen Wert enthalten.
                                                      Wird als Notiz zum Bauteil-Eintrag übernommen. + + + + ]]> +
                                                      +
                                                      +
                                                      project.bom_import.flash.invalid_file @@ -13332,7 +13657,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön name Optional String - Falls vorhanden, muss es ein nicht-leerer String sein. Name des Eintrags innerhalb der Baugruppe. + Falls vorhanden, muss es ein nicht-leerer String sein. Name des Eintrags innerhalb der Stückliste. part @@ -13432,13 +13757,13 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön - + assembly.bom_import.template.csv.exptected_columns Mögliche Spalten: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index a6dda3d47..3707e55dd 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -2116,13 +2116,13 @@ - + assembly.bom_import.template.csv.exptected_columns Δυνατές στήλες: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 763b6aff0..aa82784d9 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -11061,6 +11061,18 @@ Element 1 -> Element 1.2 Type + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11073,6 +11085,319 @@ Element 1 -> Element 1.2 Clear existing BOM entries before importing + + + project.import_bom.template.header.json + JSON Import Template + + + + + project.import_bom.template.header.csv + CSV Import Template + + + + + project.import_bom.template.header.kicad_pcbnew + CSV Import Template (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Component name in the project + + + + + project.bom_import.template.entry.part.mpnr + Unique product number within the manufacturer + + + + + project.bom_import.template.entry.part.ipn + Unique IPN of the component + + + + + project.bom_import.template.entry.part.name + Unique name of the component + + + + + project.bom_import.template.entry.part.manufacturer.name + Unique name of the manufacturer + + + + + project.bom_import.template.json.table + + + + + Field + Condition + Data Type + Description + + + + + quantity + Required + Decimal (Float) + Must be provided and contains a decimal value (Float) greater than 0.0. + + + name + Optional + String + If present, it must be a non-empty string. The name of the entry within the bill of materials. + + + part + Optional + Object/Array + + If a component is to be assigned, it must be an object/array, and at least one of the following fields must be filled in: +
                                                        +
                                                      • part.id
                                                      • +
                                                      • part.mpnr
                                                      • +
                                                      • part.ipn
                                                      • +
                                                      • part.name
                                                      • +
                                                      + + + + part.id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the component in the Part-DB. + + + part.mpnr + Optional + String + A non-empty string if no part.id, part.ipn, or part.name is provided. + + + part.ipn + Optional + String + A non-empty string if no part.id, part.mpnr, or part.name is provided. + + + part.name + Optional + String + A non-empty string if no part.id, part.mpnr, or part.ipn is provided. + + + part.manufacturer + Optional + Object/Array + + If a component's manufacturer is to be adjusted, or the component is to be unambiguously identified based on part.mpnr, it must be an object/array, and at least one of the following fields must be filled in: +
                                                        +
                                                      • manufacturer.id
                                                      • +
                                                      • manufacturer.name
                                                      • +
                                                      + + + + manufacturer.id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the manufacturer. + + + manufacturer.name + Optional + String + A non-empty string if no manufacturer.id is provided. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.csv.exptected_columns + Possible columns: + + + + + project.bom_import.template.csv.table + + + + + Column + Condition + Data Type + Description + + + + + quantity + Required + Floating-point number (Float) + Must be provided and contain a floating-point value (Float) greater than 0.0. + + + name + Optional + String + If available, it must be a non-empty string. The name of the entry within the bill of materials. + + + Columns starting with part_ + + If a component is to be assigned, at least one of the following columns must be provided and filled in: +
                                                        +
                                                      • part_id
                                                      • +
                                                      • part_mpnr
                                                      • +
                                                      • part_ipn
                                                      • +
                                                      • part_name
                                                      • +
                                                      + + + + part_id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the component in the Part-DB. + + + part_mpnr + Optional + String + Must be provided if the part_id, part_ipn, or part_name columns are not filled in. + + + part_ipn + Optional + String + Must be provided if the part_id, part_mpnr, or part_name columns are not filled in. + + + part_name + Optional + String + Must be provided if the part_id, part_mpnr, or part_ipn columns are not filled in. + + + Columns starting with part_manufacturer_ + + If the manufacturer of a component is to be adjusted or if the component is to be uniquely identified based on the part_mpnr, at least one of the following columns must be provided and filled in: +
                                                        +
                                                      • part_manufacturer_id
                                                      • +
                                                      • part_manufacturer_name
                                                      • +
                                                      + + + + part_manufacturer_id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the manufacturer. + + + part_manufacturer_name + Optional + String + Must be provided if the part_manufacturer_id column is not filled in. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Expected columns: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Note: No assignment to specific components from the category management is performed.

                                                      + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.table + + + + + Field + Condition + Data type + Description + + + + + Id + Optional + Integer + Free entry. A unique identification number for each component. + + + Designator + Optional + String + Free entry. A unique reference identifier of the component on the PCB, e.g., "R1" for resistor 1.
                                                      It is adopted into the assembly name of the component entry. + + + Package + Optional + String + Free entry. The housing or package type of the component, e.g., "0805" for SMD resistors.
                                                      It is not adopted into the component entry. + + + Quantity + Mandatory + Integer + The number of identical components required to create an instance.
                                                      It is adopted as the quantity of the entry. + + + Designation + Mandatory + String + Description or function of the component, e.g., resistor value "10kΩ" or capacitor value "100nF".
                                                      It is adopted into the name of the component entry. + + + Supplier and ref + Optional + String + Free entry. Can contain a distributor-specific value, for example.
                                                      It is adopted as a note to the component entry. + + + + ]]> +
                                                      +
                                                      +
                                                      project.bom_import.clear_existing_bom.help @@ -13433,13 +13758,13 @@ Please note, that you can not impersonate a disabled user. If you try you will g - + assembly.bom_import.template.csv.exptected_columns Possible columns: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 0b17cb8a6..1d4f12865 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -11076,6 +11076,18 @@ Elemento 3 Tipo + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11088,6 +11100,319 @@ Elemento 3 Eliminar entradas BOM existentes antes de importar + + + project.import_bom.template.header.json + Plantilla de importación JSON + + + + + project.import_bom.template.header.csv + Plantilla de importación CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Plantilla de importación CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Nombre del componente en el proyecto + + + + + project.bom_import.template.entry.part.mpnr + Número de producto único del fabricante + + + + + project.bom_import.template.entry.part.ipn + IPN único del componente + + + + + project.bom_import.template.entry.part.name + Nombre único del componente + + + + + project.bom_import.template.entry.part.manufacturer.name + Nombre único del fabricante + + + + + project.bom_import.template.json.table + + + + + Campo + Condición + Tipo de Datos + Descripción + + + + + quantity + Requerido + Decimal (Float) + Debe ser proporcionado y contener un valor decimal (Float) mayor que 0.0. + + + name + Opcional + Cadena (String) + Si está presente, debe ser una cadena no vacía. El nombre del elemento dentro de la lista de materiales. + + + part + Opcional + Objeto/Array + + Si se debe asignar un componente, debe ser un objeto/array, y al menos uno de los siguientes campos debe estar cumplimentado: +
                                                        +
                                                      • part.id
                                                      • +
                                                      • part.mpnr
                                                      • +
                                                      • part.ipn
                                                      • +
                                                      • part.name
                                                      • +
                                                      + + + + part.id + Opcional + Entero (Integer) + Entero (Integer) > 0. Corresponde al ID numérico interno del componente en la base de datos de componentes (Part-DB). + + + part.mpnr + Opcional + Cadena (String) + Una cadena no vacía si no se proporciona part.id, part.ipn o part.name. + + + part.ipn + Opcional + Cadena (String) + Una cadena no vacía si no se proporciona part.id, part.mpnr o part.name. + + + part.name + Opcional + Cadena (String) + Una cadena no vacía si no se proporciona part.id, part.mpnr o part.ipn. + + + part.manufacturer + Opcional + Objeto/Array + + Si se debe ajustar el fabricante de un componente, o si el componente debe identificarse de manera unívoca en base a part.mpnr, debe ser un objeto/array, y al menos uno de los siguientes campos debe estar cumplimentado: +
                                                        +
                                                      • manufacturer.id
                                                      • +
                                                      • manufacturer.name
                                                      • +
                                                      + + + + manufacturer.id + Opcional + Entero (Integer) + Entero (Integer) > 0. Corresponde al ID numérico interno del fabricante. + + + manufacturer.name + Opcional + Cadena (String) + Una cadena no vacía si no se proporciona manufacturer.id. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.csv.exptected_columns + Columnas posibles: + + + + + project.bom_import.template.csv.table + + + + + Columna + Condición + Tipo de dato + Descripción + + + + + quantity + Obligatoria + Número decimal (Float) + Debe proporcionarse y contener un valor decimal (Float) mayor que 0.0. + + + name + Optional + String + Si está disponible, debe ser una cadena no vacía. El nombre del elemento dentro de la lista de materiales. + + + Columnas que comienzan con part_ + + Si se va a asignar un componente, al menos una de las siguientes columnas debe proporcionarse y completarse: +
                                                        +
                                                      • part_id
                                                      • +
                                                      • part_mpnr
                                                      • +
                                                      • part_ipn
                                                      • +
                                                      • part_name
                                                      • +
                                                      + + + + part_id + Opcional + Entero + Entero > 0. Corresponde al ID numérico interno del componente en la base de datos de partes (Part-DB). + + + part_mpnr + Opcional + Cadena (String) + Debe proporcionarse si las columnas part_id, part_ipn o part_name no están completas. + + + part_ipn + Opcional + Cadena (String) + Debe proporcionarse si las columnas part_id, part_mpnr o part_name no están completas. + + + part_name + Opcional + Cadena (String) + Debe proporcionarse si las columnas part_id, part_mpnr o part_ipn no están completas. + + + Columnas que comienzan con part_manufacturer_ + + Si el fabricante de un componente debe ajustarse o si el componente debe identificarse de forma única según el valor part_mpnr, al menos una de las siguientes columnas debe proporcionarse y completarse: +
                                                        +
                                                      • part_manufacturer_id
                                                      • +
                                                      • part_manufacturer_name
                                                      • +
                                                      + + + + part_manufacturer_id + Opcional + Entero + Entero > 0. Corresponde al ID numérico interno del fabricante. + + + part_manufacturer_name + Opcional + Cadena (String) + Debe proporcionarse si la columna part_manufacturer_id no está completa. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Columnas esperadas: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Nota: No se realiza ninguna asignación a componentes específicos desde la gestión de categorías.

                                                      + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.table + + + + + Campo + Condición + Tipo de dato + Descripción + + + + + Id + Opcional + Entero + Entrada libre. Un número de identificación único para cada componente. + + + Designator + Opcional + Cadena (String) + Entrada libre. Un identificador de referencia único del componente en el PCB, por ejemplo, "R1" para la resistencia 1.
                                                      Se adopta en el nombre de ensamblaje del registro del componente. + + + Package + Opcional + Cadena (String) + Entrada libre. El encapsulado o tipo de la carcasa del componente, por ejemplo, "0805" para resistencias SMD.
                                                      No se adopta en el registro del componente. + + + Quantity + Obligatorio + Entero + El número de componentes idénticos necesarios para crear una instancia.
                                                      Se toma como la cantidad de la entrada del componente. + + + Designation + Obligatorio + Cadena (String) + Descripción o función del componente, por ejemplo, valor de resistencia "10kΩ" o valor de condensador "100nF".
                                                      Se adopta en el nombre del registro del componente. + + + Supplier and ref + Opcional + Cadena (String) + Entrada libre. Puede contener, por ejemplo, un valor específico del distribuidor.
                                                      Se adopta como una nota en el registro del componente. + + + + ]]> +
                                                      +
                                                      +
                                                      project.bom_import.clear_existing_bom.help @@ -12925,13 +13250,13 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S - + assembly.bom_import.template.csv.exptected_columns Columnas posibles: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 234dee6ef..67480c6a7 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -9481,6 +9481,18 @@ exemple de ville CSV pour un assemblage + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + assembly.bom_import.type.kicad_pcbnew @@ -9678,13 +9690,13 @@ exemple de ville - + assembly.bom_import.template.csv.exptected_columns Colonnes possibles : - + assembly.bom_import.template.csv.table diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 57654f44e..1f56dfc93 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -11078,6 +11078,18 @@ Element 3 Tipo + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11090,6 +11102,319 @@ Element 3 Cancellare le voci della BOM (lista dei materiali) esistenti prima dell'importazione + + + project.import_bom.template.header.json + Modello di importazione JSON + + + + + project.import_bom.template.header.csv + Modello di importazione CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Modello di importazione CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Nome del componente nel progetto + + + + + project.bom_import.template.entry.part.mpnr + Codice prodotto unico del produttore + + + + + project.bom_import.template.entry.part.ipn + IPN unico del componente + + + + + project.bom_import.template.entry.part.name + Nome unico del componente + + + + + project.bom_import.template.entry.part.manufacturer.name + Nome unico del produttore + + + + + project.bom_import.template.json.table + + + + + Campo + Condizione + Tipo di Dati + Descrizione + + + + + quantity + Obbligatorio + Decimale (Float) + Deve essere fornito e contenere un valore decimale (Float) maggiore di 0.0. + + + name + Opzionale + Stringa (String) + Se presente, deve essere una stringa non vuota. Il nome dell'elemento all'interno della distinta materiali. + + + part + Opzionale + Oggetto/Array + + Se un componente deve essere assegnato, deve essere un oggetto/array e almeno uno dei seguenti campi deve essere compilato: +
                                                        +
                                                      • part.id
                                                      • +
                                                      • part.mpnr
                                                      • +
                                                      • part.ipn
                                                      • +
                                                      • part.name
                                                      • +
                                                      + + + + part.id + Opzionale + Intero (Integer) + Intero (Integer) > 0. Corrisponde all'ID numerico interno del componente nel database delle parti (Part-DB). + + + part.mpnr + Opzionale + Stringa (String) + Una stringa non vuota se non sono forniti part.id, part.ipn o part.name. + + + part.ipn + Opzionale + Stringa (String) + Una stringa non vuota se non sono forniti part.id, part.mpnr o part.name. + + + part.name + Opzionale + Stringa (String) + Una stringa non vuota se non sono forniti part.id, part.mpnr o part.ipn. + + + part.manufacturer + Opzionale + Oggetto/Array + + Se il produttore di un componente deve essere modificato o se è necessario identificare univocamente il componente basandosi su part.mpnr, deve essere un oggetto/array e almeno uno dei seguenti campi deve essere compilato: +
                                                        +
                                                      • manufacturer.id
                                                      • +
                                                      • manufacturer.name
                                                      • +
                                                      + + + + manufacturer.id + Opzionale + Intero (Integer) + Intero (Integer) > 0. Corrisponde all'ID numerico interno del produttore. + + + manufacturer.name + Opzionale + Stringa (String) + Una stringa non vuota se non è fornito manufacturer.id. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.csv.exptected_columns + Colonne possibili: + + + + + project.bom_import.template.csv.table + + + + + Colonna + Condizione + Tipo di dato + Descrizione + + + + + quantity + Obbligatoria + Numero decimale (Float) + Deve essere fornita e contenere un valore decimale (Float) maggiore di 0.0. + + + name + Optional + String + Se disponibile, deve essere una stringa non vuota. Il nome della voce all'interno della distinta base. + + + Colonne che iniziano con part_ + + Se un componente deve essere assegnato, almeno una delle seguenti colonne deve essere fornita e compilata: +
                                                        +
                                                      • part_id
                                                      • +
                                                      • part_mpnr
                                                      • +
                                                      • part_ipn
                                                      • +
                                                      • part_name
                                                      • +
                                                      + + + + part_id + Opzionale + Intero (Integer) + Intero > 0. Corrisponde all'ID numerico interno del componente nel database delle parti (Part-DB). + + + part_mpnr + Opzionale + Stringa (String) + Deve essere fornita se le colonne part_id, part_ipn o part_name non sono compilate. + + + part_ipn + Opzionale + Stringa (String) + Deve essere fornita se le colonne part_id, part_mpnr o part_name non sono compilate. + + + part_name + Opzionale + Stringa (String) + Deve essere fornita se le colonne part_id, part_mpnr o part_ipn non sono compilate. + + + Colonne che iniziano con part_manufacturer_ + + Se il produttore di un componente deve essere modificato o il componente deve essere identificato univocamente in base al valore part_mpnr, almeno una delle seguenti colonne deve essere fornita e compilata: +
                                                        +
                                                      • part_manufacturer_id
                                                      • +
                                                      • part_manufacturer_name
                                                      • +
                                                      + + + + part_manufacturer_id + Opzionale + Intero (Integer) + Intero > 0. Corrisponde all'ID numerico interno del produttore. + + + part_manufacturer_name + Opzionale + Stringa (String) + Deve essere fornita se la colonna part_manufacturer_id non è compilata. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Colonne previste: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Nota: Non viene effettuata alcuna associazione con componenti specifici dalla gestione delle categorie.

                                                      + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.table + + + + + Campo + Condizione + Tipo di dato + Descrizione + + + + + Id + Opzionale + Numero intero + Valore libero. Un numero identificativo univoco per ciascun componente. + + + Designator + Opzionale + Stringa + Valore libero. Un identificatore di riferimento univoco del componente sul PCB, ad esempio "R1" per il resistore 1.
                                                      Viene trasferito nel nome di montaggio del record del componente. + + + Package + Opzionale + Stringa + Valore libero. L'involucro o la forma del componente, ad esempio "0805" per i resistori SMD.
                                                      Non viene trasferito nel record del componente. + + + Quantity + Campo obbligatorio + Numero intero + Il numero dei componenti identici necessari per creare un'istanza.
                                                      Registrato come il numero della voce del componente. + + + Designation + Campo obbligatorio + Stringa + Descrizione o funzione del componente, ad esempio valore del resistore "10kΩ" o valore del condensatore "100nF".
                                                      Viene trasferita nel nome del record del componente. + + + Supplier and ref + Opzionale + Stringa + Valore libero. Può contenere ad esempio un valore specifico del distributore.
                                                      Viene trasferito come nota nel record del componente. + + + + ]]> +
                                                      +
                                                      +
                                                      project.bom_import.clear_existing_bom.help @@ -12927,13 +13252,13 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a - + assembly.bom_import.template.csv.exptected_columns Colonne possibili: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 2ff7a7446..8991fc170 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -9403,13 +9403,13 @@ Exampletown - + assembly.bom_import.template.csv.exptected_columns 可能なカラム: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index be8728ce5..9525ba29b 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1341,13 +1341,13 @@ - + assembly.bom_import.template.csv.exptected_columns Mogelijke kolommen: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index d48e0d930..c221767bb 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -11081,6 +11081,18 @@ Element 3 Typ + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11093,6 +11105,319 @@ Element 3 Wyczyść istniejące wpisy BOM przed importem + + + project.import_bom.template.header.json + Szablon importu JSON + + + + + project.import_bom.template.header.csv + Szablon importu CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Szablon importu CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Nazwa komponentu w projekcie + + + + + project.bom_import.template.entry.part.mpnr + Unikalny numer produktu producenta + + + + + project.bom_import.template.entry.part.ipn + Unikalny IPN komponentu + + + + + project.bom_import.template.entry.part.name + Unikalna nazwa komponentu + + + + + project.bom_import.template.entry.part.manufacturer.name + Unikalna nazwa producenta + + + + + project.bom_import.template.json.table + + + + + Pole + Warunek + Typ Danych + Opis + + + + + quantity + Wymagane + Dziesiętny (Float) + Musi być podane i zawierać wartość dziesiętną (Float) większą niż 0.0. + + + name + Opcjonalne + Ciąg (String) + Jeśli jest obecny, musi być niepustym ciągiem znaków. Nazwa elementu w wykazie materiałów. + + + part + Opcjonalne + Obiekt/Tablica + + Jeśli komponent musi być przypisany, musi być obiektem/tablą i co najmniej jedno z następujących pól musi zostać wypełnione: +
                                                        +
                                                      • part.id
                                                      • +
                                                      • part.mpnr
                                                      • +
                                                      • part.ipn
                                                      • +
                                                      • part.name
                                                      • +
                                                      + + + + part.id + Opcjonalne + Całkowity (Integer) + Całkowity (Integer) > 0. Odpowiada wewnętrznemu numerycznemu identyfikatorowi komponentu w bazie danych części (Part-DB). + + + part.mpnr + Opcjonalne + Ciag (String) + Niepusty ciąg, jeśli part.id, part.ipn ani part.name nie zostały podane. + + + part.ipn + Opcjonalne + Ciag (String) + Niepusty ciąg, jeśli part.id, part.mpnr ani part.name nie zostały podane. + + + part.name + Opcjonalne + Ciag (String) + Niepusty ciąg, jeśli part.id, part.mpnr ani part.ipn nie zostały podane. + + + part.manufacturer + Opcjonalne + Obiekt/Tablica + + Jeśli producent komponentu musi zostać dostosowany lub komponent musi zostać jednoznacznie zidentyfikowany na podstawie part.mpnr, musi być obiektem/tablą, a co najmniej jedno z następujących pól musi zostać wypełnione: +
                                                        +
                                                      • manufacturer.id
                                                      • +
                                                      • manufacturer.name
                                                      • +
                                                      + + + + manufacturer.id + Opcjonalne + Całkowity (Integer) + Całkowity (Integer) > 0. Odpowiada wewnętrznemu numerycznemu identyfikatorowi producenta. + + + manufacturer.name + Opcjonalne + Ciag (String) + Niepusty ciąg, jeśli manufacturer.id nie został podany. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.csv.exptected_columns + Możliwe kolumny: + + + + + project.bom_import.template.csv.table + + + + + Kolumna + Warunek + Typ danych + Opis + + + + + quantity + Wymagana + Liczba zmiennoprzecinkowa (Float) + Liczba identycznych komponentów potrzebnych do utworzenia instancji.
                                                      Traktowane jako liczba wpisów komponentu. + + + name + Optional + String + Jeśli dostępny, musi być niepustym ciągiem znaków. Nazwa elementu w wykazie materiałów. + + + Kolumny zaczynające się od part_ + + Jeśli ma zostać przypisany komponent, co najmniej jedna z poniższych kolumn musi zostać podana i uzupełniona: +
                                                        +
                                                      • part_id
                                                      • +
                                                      • part_mpnr
                                                      • +
                                                      • part_ipn
                                                      • +
                                                      • part_name
                                                      • +
                                                      + + + + part_id + Opcjonalna + Liczba całkowita (Integer) + Liczba całkowita > 0. Odpowiada wewnętrznemu ID numerycznemu komponentu w Part-DB. + + + part_mpnr + Opcjonalna + Cišg znaków (String) + Musi być podana, jeśli kolumny part_id, part_ipn ani part_name nie są podane. + + + part_ipn + Opcjonalna + Cišg znaków (String) + Musi być podana, jeśli kolumny part_id, part_mpnr ani part_name nie są podane. + + + part_name + Opcjonalna + Cišg znaków (String) + Musi być podana, jeśli kolumny part_id, part_mpnr ani part_ipn nie są podane. + + + Kolumny zaczynające się od part_manufacturer_ + + Jeśli producent komponentu ma zostać zmieniony lub komponent ma zostać jednoznacznie zidentyfikowany na podstawie wartości part_mpnr, co najmniej jedna z poniższych kolumn musi zostać podana i uzupełniona: +
                                                        +
                                                      • part_manufacturer_id
                                                      • +
                                                      • part_manufacturer_name
                                                      • +
                                                      + + + + part_manufacturer_id + Opcjonalna + Liczba całkowita (Integer) + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID producenta. + + + part_manufacturer_name + Opcjonalna + Cišg znaków (String) + Musi być podana, jeśli kolumna part_manufacturer_id nie jest uzupełniona. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Oczekiwane kolumny: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Uwaga: Nie następuje przypisanie do konkretnych komponentów z zarządzania kategoriami.

                                                      + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.table + + + + + Pole + Warunek + Typ danych + Opis + + + + + Id + Opcjonalne + Liczba całkowita (Integer) + Dowolna wartość. Unikalny numer identyfikacyjny dla każdego komponentu. + + + Designator + Opcjonalne + String + Dowolna wartość. Unikalny identyfikator referencyjny komponentu na płytce PCB, np. „R1” dla rezystora 1.
                                                      Zostaje przeniesiony do nazwy montażowej wpisu komponentu. + + + Package + Opcjonalne + String + Dowolna wartość. Obudowa lub typ komponentu, np. „0805” dla rezystorów SMD.
                                                      Nie zostaje przeniesiony do wpisu komponentu. + + + Quantity + Pole obowiązkowe + Liczba całkowita (Integer) + Liczba identycznych komponentów potrzebnych do stworzenia instancji zestawu.
                                                      Zostaje przeniesiona jako ilość wpisu komponentu. + + + Designation + Pole obowiązkowe + String + Opis lub funkcja komponentu, np. wartość rezystora „10kΩ” lub wartość kondensatora „100nF”.
                                                      Zostaje przeniesiony do nazwy wpisu komponentu. + + + Supplier and ref + Opcjonalne + String + Dowolna wartość. Może zawierać np. wartość specyficzną dla dystrybutora.
                                                      Zostaje przeniesiona jako notatka do wpisu komponentu. + + + + ]]> +
                                                      +
                                                      +
                                                      project.bom_import.clear_existing_bom.help @@ -12804,13 +13129,13 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli - + assembly.bom_import.template.csv.exptected_columns Możliwe kolumny: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index f27cd8586..5d4c8885f 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -11085,6 +11085,18 @@ Тип + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11097,6 +11109,319 @@ Удалить существующие записи BOM перед импортом. + + + project.import_bom.template.header.json + Шаблон импорта JSON + + + + + project.import_bom.template.header.csv + Шаблон импорта CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Шаблон импорта CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Название компонента в проекте + + + + + project.bom_import.template.entry.part.mpnr + Уникальный номер продукта производителя + + + + + project.bom_import.template.entry.part.ipn + Уникальный IPN компонента + + + + + project.bom_import.template.entry.part.name + Уникальное название компонента + + + + + project.bom_import.template.entry.part.manufacturer.name + Уникальное название производителя + + + + + project.bom_import.template.json.table + + + + + Поле + Условие + Тип данных + Описание + + + + + quantity + Обязательно + Дробное число (Float) + Должно быть указано и содержать дробное значение (Float), большее 0.0. + + + name + Опционально + Строка (String) + Если присутствует, должно быть непустой строкой. Название элемента в спецификации материалов. + + + part + Опционально + Объект/Массив + + Если необходимо назначить компонент, он должен быть объектом/массивом, и должно быть заполнено хотя бы одно из следующих полей: +
                                                        +
                                                      • part.id
                                                      • +
                                                      • part.mpnr
                                                      • +
                                                      • part.ipn
                                                      • +
                                                      • part.name
                                                      • +
                                                      + + + + part.id + Опционально + Целое число (Integer) + Целое число (Integer) > 0. Соответствует внутреннему числовому идентификатору компонента в базе данных компонентов (Part-DB). + + + part.mpnr + Опционально + Строка (String) + Непустая строка, если не указаны part.id, part.ipn или part.name. + + + part.ipn + Опционально + Строка (String) + Непустая строка, если не указаны part.id, part.mpnr или part.name. + + + part.name + Опционально + Строка (String) + Непустая строка, если не указаны part.id, part.mpnr или part.ipn. + + + part.manufacturer + Опционально + Объект/Массив + + Если необходимо указать производителя компонента или однозначно идентифицировать компонент на основе part.mpnr, он должен быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: +
                                                        +
                                                      • manufacturer.id
                                                      • +
                                                      • manufacturer.name
                                                      • +
                                                      + + + + manufacturer.id + Опционально + Целое число (Integer) + Целое число (Integer) > 0. Соответствует внутреннему числовому идентификатору производителя. + + + manufacturer.name + Опционально + Строка (String) + Непустая строка, если manufacturer.id не указан. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.csv.exptected_columns + Возможные колонки: + + + + + project.bom_import.template.csv.table + + + + + Колонка + Условие + Тип данных + Описание + + + + + quantity + Обязательная + Число с плавающей запятой (Float) + Количество идентичных компонентов, необходимых для создания экземпляра.
                                                      Считается количеством записей компонента. + + + name + Optional + String + Если доступно, должна быть непустая строка. Название элемента в спецификации материалов. + + + Колонки, начинающиеся с part_ + + Если нужно назначить компонент, должна быть указана и заполнена по крайней мере одна из следующих колонок: +
                                                        +
                                                      • part_id
                                                      • +
                                                      • part_mpnr
                                                      • +
                                                      • part_ipn
                                                      • +
                                                      • part_name
                                                      • +
                                                      + + + + part_id + Необязательная + Целое число (Integer) + Целое > 0. Соответствует внутреннему числовому ID компонента в базе данных компонентов (Part-DB). + + + part_mpnr + Необязательная + Строка (String) + Должна быть указана, если колонки part_id, part_ipn или part_name не заполнены. + + + part_ipn + Необязательная + Строка (String) + Должна быть указана, если колонки part_id, part_mpnr или part_name не заполнены. + + + part_name + Необязательная + Строка (String) + Должна быть указана, если колонки part_id, part_mpnr или part_ipn не заполнены. + + + Колонки, начинающиеся с part_manufacturer_ + + Если требуется указать производителя компонента или уникально идентифицировать компонент на основе значения part_mpnr, должна быть указана и заполнена по крайней мере одна из следующих колонок: +
                                                        +
                                                      • part_manufacturer_id
                                                      • +
                                                      • part_manufacturer_name
                                                      • +
                                                      + + + + part_manufacturer_id + Необязательная + Целое число (Integer) + Целое > 0. Соответствует внутреннему числовому ID производителя. + + + part_manufacturer_name + Необязательная + Строка (String) + Должна быть указана, если колонка part_manufacturer_id не заполнена. + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Ожидаемые столбцы: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Примечание: Не выполняется привязка к конкретным компонентам из управления категориями.

                                                      + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.table + + + + + Поле + Условие + Тип данных + Описание + + + + + Id + Необязательно + Целое число (Integer) + Свободный ввод. Уникальный идентификационный номер для каждого компонента. + + + Designator + Необязательно + Строка (String) + Свободный ввод. Уникальный идентификатор компонента на печатной плате, например, «R1» для резистора 1.
                                                      Добавляется в название сборочного узла записи компонента. + + + Package + Необязательно + Строка (String) + Свободный ввод. Корпус или тип компонента, например, «0805» для SMD резисторов.
                                                      Не добавляется в запись компонента. + + + Quantity + Обязательно + Целое число (Integer) + Число идентичных компонентов, необходимых для создания экземпляра сборки.
                                                      Добавляется как количество записи компонента. + + + Designation + Обязательно + Строка (String) + Описание или функция компонента, например, значение резистора «10kΩ» или значение конденсатора «100nF».
                                                      Добавляется в название записи компонента. + + + Supplier and ref + Необязательно + Строка (String) + Свободный ввод. Может содержать дистрибьюторское значение, например.
                                                      Добавляется как примечание к записи компонента. + + + + ]]> +
                                                      +
                                                      +
                                                      project.bom_import.clear_existing_bom.help @@ -12904,13 +13229,13 @@ - + assembly.bom_import.template.csv.exptected_columns Возможные столбцы: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 9fd6855a0..3a6f53755 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -11084,6 +11084,18 @@ Element 3 Type + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11096,6 +11108,319 @@ Element 3 导入前删除现有BOM条目 + + + project.import_bom.template.header.json + JSON导入模板 + + + + + project.import_bom.template.header.csv + CSV导入模板 + + + + + project.import_bom.template.header.kicad_pcbnew + CSV导入模板(KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + 项目中的组件名称 + + + + + project.bom_import.template.entry.part.mpnr + 制造商的唯一产品编号 + + + + + project.bom_import.template.entry.part.ipn + 唯一的组件IPN + + + + + project.bom_import.template.entry.part.name + 组件唯一名称 + + + + + project.bom_import.template.entry.part.manufacturer.name + 制造商唯一名称 + + + + + project.bom_import.template.json.table + + + + + 字段 + 条件 + 数据类型 + 描述 + + + + + quantity + 必填 + 小数 (Float) + 必须提供,并包含大于 0.0 的小数值 (Float)。 + + + name + 可选 + 字符串 (String) + 如果存在,必须是非空字符串。物料清单中元素的名称。 + + + part + 可选 + 对象/数组 + + 如果需要分配组件,则必须是对象/数组,并且以下字段中的至少一个必须填写: +
                                                        +
                                                      • part.id
                                                      • +
                                                      • part.mpnr
                                                      • +
                                                      • part.ipn
                                                      • +
                                                      • part.name
                                                      • +
                                                      + + + + part.id + 可选 + 整数 (Integer) + 整数 (Integer) > 0。对应于零件数据库 (Part-DB) 中组件的内部数字 ID。 + + + part.mpnr + 可选 + 字符串 (String) + 如果未提供 part.id、part.ipn 或 part.name,则为非空字符串。 + + + part.ipn + 可选 + 字符串 (String) + 如果未提供 part.id、part.mpnr 或 part.name,则为非空字符串。 + + + part.name + 可选 + 字符串 (String) + 如果未提供 part.id、part.mpnr 或 part.ipn,则为非空字符串。 + + + part.manufacturer + 可选 + 对象/数组 + + 如果需要调整组件的制造商,或者需要基于 part.mpnr 唯一标识组件,则必须是对象/数组,并且以下字段中的至少一个必须填写: +
                                                        +
                                                      • manufacturer.id
                                                      • +
                                                      • manufacturer.name
                                                      • +
                                                      + + + + manufacturer.id + 可选 + 整数 (Integer) + 整数 (Integer) > 0。对应于制造商的内部数字 ID。 + + + manufacturer.name + 可选 + 字符串 (String) + 如果未提供 manufacturer.id,则为非空字符串。 + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.csv.exptected_columns + 可能的列: + + + + + project.bom_import.template.csv.table + + + + + 列 + 条件 + 数据类型 + 描述 + + + + + quantity + 必填 + 浮点数 (Float) + 创建一个实例所需的相同组件数量。
                                                      记录为组件条目的数量。 + + + name + Optional + String + 如果可用,则必须是非空字符串。材料清单中项目的名称。 + + + 以 part_ 开头的列 + + 如果需要分配一个组件,必须提供并填写以下列之一: +
                                                        +
                                                      • part_id
                                                      • +
                                                      • part_mpnr
                                                      • +
                                                      • part_ipn
                                                      • +
                                                      • part_name
                                                      • +
                                                      + + + + part_id + 可选 + 整数 (Integer) + 整数 > 0。对应于零件数据库 (Part-DB) 中组件的内部数字 ID。 + + + part_mpnr + 可选 + 字符串 (String) + 如果 part_id、part_ipn 或 part_name 列未填写,则必须提供此列。 + + + part_ipn + 可选 + 字符串 (String) + 如果 part_id、part_mpnr 或 part_name 列未填写,则必须提供此列。 + + + part_name + 可选 + 字符串 (String) + 如果 part_id、part_mpnr 或 part_ipn 列未填写,则必须提供此列。 + + + 以 part_manufacturer_ 开头的列 + + 如果需要调整组件的制造商,或者组件需要根据 part_mpnr 值唯一标识,必须提供并填写以下列之一: +
                                                        +
                                                      • part_manufacturer_id
                                                      • +
                                                      • part_manufacturer_name
                                                      • +
                                                      + + + + part_manufacturer_id + 可选 + 整数 (Integer) + 整数 > 0。对应于制造商的内部数字 ID。 + + + part_manufacturer_name + 可选 + 字符串 (String) + 如果 part_manufacturer_id 列未填写,则必须提供此列。 + + + + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.exptected_columns + 预期的列: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + 注意:分类管理中不会执行与特定组件的映射。

                                                      + ]]> +
                                                      +
                                                      +
                                                      + + + project.bom_import.template.kicad_pcbnew.table + + + + + 字段 + 条件 + 数据类型 + 描述 + + + + + Id + 可选 + 整数 + 自由输入。每个组件的唯一标识编号。 + + + Designator + 可选 + 字符串 (String) + 自由输入。PCB 上组件的唯一参考标识符,例如“R1”代表电阻 1。
                                                      会被采用到组件记录的装配名称中。 + + + Package + 可选 + 字符串 (String) + 自由输入。组件的封装或形状,例如 "0805" 表示 SMD 电阻。
                                                      不会被采用到组件记录中。 + + + Quantity + 必填 + 整数 + 创建组件实例所需的相同组件的数量。
                                                      会被采用为组件记录的数量。 + + + Designation + 必填 + 字符串 (String) + 组件的描述或功能,例如电阻值 “10kΩ” 或电容值 “100nF”。
                                                      会被采用到组件记录的名称中。 + + + Supplier and ref + 可选 + 字符串 (String) + 自由输入。例如,可以包含供应商的特定值。
                                                      会被采用为组件记录的备注。 + + + + ]]> +
                                                      +
                                                      +
                                                      project.bom_import.clear_existing_bom.help @@ -12789,13 +13114,13 @@ Element 3 - + assembly.bom_import.template.csv.exptected_columns 可用列: - + assembly.bom_import.template.csv.table From 90678b42fb8ac1546eddeb918636b0207be3e1f0 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 24 Jun 2025 11:07:20 +0200 Subject: [PATCH 14/83] =?UTF-8?q?Part-=C3=9Cbersicht=20sowie=20-Detailansi?= =?UTF-8?q?cht=20um=20Assembly=20Information=20erweitern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DataTables/PartsDataTable.php | 29 +++++++++++++++++ templates/parts/info/_assemblies.html.twig | 31 +++++++++++++++++++ templates/parts/info/show_part_info.html.twig | 27 ++++++++++------ translations/messages.cs.xlf | 6 ++++ translations/messages.da.xlf | 6 ++++ translations/messages.de.xlf | 6 ++++ translations/messages.el.xlf | 6 ++++ translations/messages.en.xlf | 6 ++++ translations/messages.es.xlf | 6 ++++ translations/messages.fr.xlf | 6 ++++ translations/messages.it.xlf | 6 ++++ translations/messages.ja.xlf | 6 ++++ translations/messages.nl.xlf | 6 ++++ translations/messages.pl.xlf | 6 ++++ translations/messages.ru.xlf | 6 ++++ translations/messages.zh.xlf | 6 ++++ 16 files changed, 156 insertions(+), 9 deletions(-) create mode 100644 templates/parts/info/_assemblies.html.twig diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index f0decf271..b98c97629 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -39,6 +39,7 @@ use App\DataTables\Helpers\ColumnSortHelper; use App\DataTables\Helpers\PartDataTableHelper; use App\Doctrine\Helpers\FieldHelper; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; @@ -238,6 +239,34 @@ public function configure(DataTable $dataTable, array $options): void ]); } + //Add a assembly column to list where the part is used, when the user has the permission to see the assemblies + if ($this->security->isGranted('read', Assembly::class)) { + $this->csh->add('assemblies', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.labelp'), + 'render' => function ($value, Part $context): string { + //Only show the first 5 assembly names + $assemblies = $context->getAssemblies(); + $tmp = ""; + + $max = 5; + + for ($i = 0; $i < min($max, count($assemblies)); $i++) { + $url = $this->urlGenerator->infoURL($assemblies[$i]); + $tmp .= sprintf('%s', $url, htmlspecialchars($assemblies[$i]->getName())); + if ($i < count($assemblies) - 1) { + $tmp .= ", "; + } + } + + if (count($assemblies) > $max) { + $tmp .= ", + ".(count($assemblies) - $max); + } + + return $tmp; + } + ]); + } + $this->csh ->add('edit', IconLinkColumn::class, [ 'label' => $this->translator->trans('part.table.edit'), diff --git a/templates/parts/info/_assemblies.html.twig b/templates/parts/info/_assemblies.html.twig new file mode 100644 index 000000000..d4996c592 --- /dev/null +++ b/templates/parts/info/_assemblies.html.twig @@ -0,0 +1,31 @@ +{% import "components/attachments.macro.html.twig" as attachments %} +{% import "helper.twig" as helper %} + + + + + + + + + + + + + {% for bom_entry in part.assemblyBomEntries %} + {# @var bom_entry App\Entity\Assembly\AssemblyBOMEntry #} + + + {# Name #} + {# Description #} + + + {% endfor %} + +
                                                      {% trans %}entity.info.name{% endtrans %}{% trans %}description.label{% endtrans %}{% trans %}assembly.bom.quantity{% endtrans %}
                                                      {% if bom_entry.assembly.masterPictureAttachment is not null %}{{ attachments.attachment_icon(bom_entry.assembly.masterPictureAttachment, attachment_manager) }}{% endif %}{{ bom_entry.assembly.name }}{{ bom_entry.assembly.description|format_markdown }}{{ bom_entry.quantity | format_amount(part.partUnit) }}
                                                      + + + + {% trans %}part.info.add_part_to_assembly{% endtrans %} + \ No newline at end of file diff --git a/templates/parts/info/show_part_info.html.twig b/templates/parts/info/show_part_info.html.twig index 96b5e2091..cd7b4ce7a 100644 --- a/templates/parts/info/show_part_info.html.twig +++ b/templates/parts/info/show_part_info.html.twig @@ -109,15 +109,20 @@ {% trans %}vendor.partinfo.history{% endtrans %} - {% if part.projectBomEntries is not empty %} - - {% endif %} + +
+
+ {% include "parts/info/_assemblies.html.twig" %} +
+
{% include "parts/info/_history.html.twig" %}
diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 72331a795..d617263a2 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -13648,6 +13648,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Přidat součásti do sestavy + + + part.info.add_part_to_assembly + Přidat tuto součástku do sestavy + + assembly.bom.project diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 108df80eb..cb08bf9a7 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -12809,6 +12809,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Tilføj dele til samlingen + + + part.info.add_part_to_assembly + Tilføj denne del til en samling + + assembly.bom.project diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index a7137b26a..9e79e5b1e 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -13464,6 +13464,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Bauteile zur Baugruppe hinzufügen + + + part.info.add_part_to_assembly + Dieses Bauteil zu einer Baugruppe hinzufügen + + assembly.bom.project diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 3707e55dd..fc03f08be 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1823,6 +1823,12 @@ Προσθήκη εξαρτημάτων στη συναρμολόγηση + + + part.info.add_part_to_assembly + Προσθέστε αυτό το εξάρτημα σε μια συναρμολόγηση + + assembly.bom.project diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index aa82784d9..8d09a014a 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13465,6 +13465,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Add parts to assembly + + + part.info.add_part_to_assembly + Add this part to an assembly + + assembly.bom.project diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 1d4f12865..3de5518ca 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -12957,6 +12957,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Añadir piezas al ensamblaje + + + part.info.add_part_to_assembly + Agregar esta parte a un ensamblaje + + assembly.bom.project diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 67480c6a7..4bb77d8eb 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -9385,6 +9385,12 @@ exemple de ville Ajouter des pièces à l'assemblage + + + part.info.add_part_to_assembly + Ajouter cette pièce à un assemblage + + assembly.bom.project diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 1f56dfc93..55d963caf 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -12959,6 +12959,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Aggiungi componenti al gruppo + + + part.info.add_part_to_assembly + Aggiungi questa parte a un assemblaggio + + assembly.bom.project diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 8991fc170..b61b585f3 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -9122,6 +9122,12 @@ Exampletown アセンブリに部品を追加 + + + part.info.add_part_to_assembly + このパーツをアセンブリに追加 + + assembly.bom.project diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 9525ba29b..c52a4733a 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1012,6 +1012,12 @@ Onderdelen toevoegen aan assemblage + + + part.info.add_part_to_assembly + Dit onderdeel aan een assemblage toevoegen + + assembly.bom.project diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index c221767bb..6ae246be0 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -12836,6 +12836,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Dodaj części do zespołu + + + part.info.add_part_to_assembly + Dodaj tę część do zespołu + + assembly.bom.project diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 5d4c8885f..5f6584221 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -12936,6 +12936,12 @@ Добавить детали в сборку + + + part.info.add_part_to_assembly + Добавить эту часть в сборку + + assembly.bom.project diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 3a6f53755..e4b236db7 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -12821,6 +12821,12 @@ Element 3 添加零件到组件 + + + part.info.add_part_to_assembly + 将此零件添加到装配体中 + + assembly.bom.project From 8cbbbb357b56a872c9c4d08179b3667e6737ca16 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 26 Jun 2025 14:43:25 +0200 Subject: [PATCH 15/83] =?UTF-8?q?Assembly=20um=20IPN-Eingabem=C3=B6glichke?= =?UTF-8?q?it=20und=20Automatismus=20zur=20Name-Angabe=20erweitern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/parameters.yaml | 2 + config/services.yaml | 4 + docs/configuration.md | 1 + migrations/Version20250304081039.php | 7 ++ migrations/Version20250624095045.php | 84 +++++++++++++++++++ .../AdminPages/BaseAdminController.php | 19 +++++ src/Entity/AssemblySystem/Assembly.php | 33 +++++++- src/Form/AdminPages/AssemblyAdminForm.php | 17 ++++ src/Form/AdminPages/BaseEntityAdminForm.php | 8 +- templates/admin/assembly_admin.html.twig | 1 + translations/messages.cs.xlf | 6 ++ translations/messages.da.xlf | 6 ++ translations/messages.de.xlf | 6 ++ translations/messages.en.xlf | 6 ++ translations/messages.es.xlf | 6 ++ translations/messages.it.xlf | 6 ++ translations/messages.pl.xlf | 6 ++ translations/messages.ru.xlf | 6 ++ translations/messages.zh.xlf | 6 ++ translations/validators.cs.xlf | 6 ++ translations/validators.da.xlf | 6 ++ translations/validators.de.xlf | 6 ++ translations/validators.el.xlf | 6 ++ translations/validators.en.xlf | 6 ++ translations/validators.hr.xlf | 6 ++ translations/validators.it.xlf | 6 ++ translations/validators.pl.xlf | 6 ++ translations/validators.ru.xlf | 6 ++ translations/validators.zh.xlf | 6 ++ 29 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 migrations/Version20250624095045.php diff --git a/config/parameters.yaml b/config/parameters.yaml index a993d2d5e..fc2400ce6 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -14,6 +14,8 @@ parameters: partdb.db.emulate_natural_sort: '%env(bool:DATABASE_EMULATE_NATURAL_SORT)%' # If this is set to true, natural sorting is emulated on platforms that do not support it natively. This can be slow on large datasets. + partdb.create_assembly_use_ipn_placeholder_in_name: '%env(bool:CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME)%' # Use an %%ipn%% placeholder in the name of an assembly. Placeholder is replaced with the ipn input while saving. + ###################################################################################################################### # Users and Privacy ###################################################################################################################### diff --git a/config/services.yaml b/config/services.yaml index 80ca69771..88a729fa4 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -164,6 +164,10 @@ services: arguments: $saml_enabled: '%partdb.saml.enabled%' + App\Form\AdminPages\AssemblyAdminForm: + arguments: + $useAssemblyIpnPlaceholder: '%partdb.create_assembly_use_ipn_placeholder_in_name%' + #################################################################################################################### # Table settings #################################################################################################################### diff --git a/docs/configuration.md b/docs/configuration.md index efa3efd34..19899c061 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -141,6 +141,7 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns are: `name`, `id`, `quantity`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `mountnames`, `instockAmount`, `storageLocations`, `addedDate`, `lastModified`. +* `CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME`: Use an %%ipn%% placeholder in the name of a assembly. Placeholder is replaced with the ipn input while saving. ### History/Eventlog-related settings diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php index c0fc08d96..ae2d6261b 100644 --- a/migrations/Version20250304081039.php +++ b/migrations/Version20250304081039.php @@ -120,6 +120,7 @@ public function sqLiteUp(Schema $schema): void last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, status VARCHAR(64) DEFAULT NULL, + ipn VARCHAR(100) DEFAULT NULL, description CLOB NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, @@ -132,6 +133,12 @@ public function sqLiteUp(Schema $schema): void $this->addSql(<<<'SQL' CREATE INDEX IDX_5F3832C0EA7100A1 ON assemblies (id_preview_attachment) SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_5F3832C03D721C14 ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX assembly_idx_ipn ON assemblies (ipn) + SQL); $this->addSql(<<<'SQL' CREATE TABLE assembly_bom_entries ( diff --git a/migrations/Version20250624095045.php b/migrations/Version20250624095045.php new file mode 100644 index 000000000..d875f1d80 --- /dev/null +++ b/migrations/Version20250624095045.php @@ -0,0 +1,84 @@ +addSql(<<<'SQL' + ALTER TABLE assemblies ADD ipn VARCHAR(100) DEFAULT NULL AFTER status + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_5F3832C03D721C14 ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX assembly_idx_ipn ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e2f180363 TO IDX_8C74887E4AD2039E + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_5F3832C03D721C14 ON assemblies + SQL); + $this->addSql(<<<'SQL' + DROP INDEX assembly_idx_ipn ON assemblies + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP ipn + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e4ad2039e TO IDX_8C74887E2F180363 + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function sqLiteDown(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD ipn VARCHAR(100) DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_5F3832C03D721C14 ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX assembly_idx_ipn ON assemblies (ipn) + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_5F3832C03D721C14 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX assembly_idx_ipn + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP ipn + SQL); + } +} diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index 8c8d7520d..c6d9cf20c 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -23,6 +23,7 @@ namespace App\Controller\AdminPages; use App\DataTables\LogDataTable; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentUpload; @@ -193,6 +194,15 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit $entity->setMasterPictureAttachment(null); } + if ($entity instanceof Assembly) { + /* Replace ipn placeholder with the IPN information if applicable. + * The '%%ipn%%' placeholder is automatically inserted into the Name property, + * depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one, + * to avoid having to insert it manually */ + + $entity->setName(str_ireplace('%%ipn%%', $entity->getIpn(), $entity->getName())); + } + $this->commentHelper->setMessage($form['log_comment']->getData()); $em->persist($entity); @@ -287,6 +297,15 @@ protected function _new(Request $request, EntityManagerInterface $em, EntityImpo $new_entity->setMasterPictureAttachment(null); } + if ($new_entity instanceof Assembly) { + /* Replace ipn placeholder with the IPN information if applicable. + * The '%%ipn%%' placeholder is automatically inserted into the Name property, + * depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one, + * to avoid having to insert it manually */ + + $new_entity->setName(str_ireplace('%%ipn%%', $new_entity->getIpn(), $new_entity->getName())); + } + $this->commentHelper->setMessage($form['log_comment']->getData()); $em->persist($new_entity); $em->flush(); diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 54305a6fa..7897ea36b 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -47,8 +47,10 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use InvalidArgumentException; +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\Length; use Symfony\Component\Validator\Context\ExecutionContextInterface; /** @@ -58,6 +60,8 @@ */ #[ORM\Entity] #[ORM\Table(name: 'assemblies')] +#[UniqueEntity(fields: ['ipn'], message: 'assembly.ipn.must_be_unique')] +#[ORM\Index(columns: ['ipn'], name: 'assembly_idx_ipn')] #[ApiResource( operations: [ new Get(security: 'is_granted("read", object)'), @@ -83,7 +87,7 @@ normalizationContext: ['groups' => ['assembly:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] )] #[ApiFilter(PropertyFilter::class)] -#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "ipn"])] #[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] class Assembly extends AbstractStructuralDBElement { @@ -122,6 +126,14 @@ class Assembly extends AbstractStructuralDBElement #[ORM\Column(type: Types::STRING, length: 64, nullable: true)] protected ?string $status = null; + /** + * @var string|null The internal ipn number of the assembly + */ + #[Assert\Length(max: 100)] + #[Groups(['extended', 'full', 'project:read', 'project:write', 'import'])] + #[ORM\Column(type: Types::STRING, length: 100, unique: true, nullable: true)] + #[Length(max: 100)] + protected ?string $ipn = null; /** * @var Part|null The (optional) part that represents the builds of this assembly in the stock @@ -301,6 +313,25 @@ public function setStatus(?string $status): void $this->status = $status; } + /** + * Returns the internal part number of the assembly. + * @return string + */ + public function getIpn(): ?string + { + return $this->ipn; + } + + /** + * Sets the internal part number of the assembly. + * @param string $ipn The new IPN of the assembly + */ + public function setIpn(?string $ipn): Assembly + { + $this->ipn = $ipn; + return $this; + } + /** * Checks if this assembly has an associated part representing the builds of this assembly in the stock. */ diff --git a/src/Form/AdminPages/AssemblyAdminForm.php b/src/Form/AdminPages/AssemblyAdminForm.php index be1564d21..0512f64a4 100644 --- a/src/Form/AdminPages/AssemblyAdminForm.php +++ b/src/Form/AdminPages/AssemblyAdminForm.php @@ -25,11 +25,22 @@ use App\Entity\Base\AbstractNamedDBElement; use App\Form\AssemblySystem\AssemblyBOMEntryCollectionType; use App\Form\Type\RichTextEditorType; +use App\Services\LogSystem\EventCommentNeededHelper; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; class AssemblyAdminForm extends BaseEntityAdminForm { + public function __construct( + protected Security $security, + protected EventCommentNeededHelper $eventCommentNeededHelper, + protected bool $useAssemblyIpnPlaceholder = false + ) { + parent::__construct($security, $eventCommentNeededHelper, $useAssemblyIpnPlaceholder); + } + protected function additionalFormElements(FormBuilderInterface $builder, array $options, AbstractNamedDBElement $entity): void { $builder->add('description', RichTextEditorType::class, [ @@ -60,5 +71,11 @@ protected function additionalFormElements(FormBuilderInterface $builder, array $ 'assembly.status.archived' => 'archived', ], ]); + + $builder->add('ipn', TextType::class, [ + 'required' => false, + 'empty_data' => null, + 'label' => 'assembly.edit.ipn', + ]); } } diff --git a/src/Form/AdminPages/BaseEntityAdminForm.php b/src/Form/AdminPages/BaseEntityAdminForm.php index e5d69b35c..bbc437e3a 100644 --- a/src/Form/AdminPages/BaseEntityAdminForm.php +++ b/src/Form/AdminPages/BaseEntityAdminForm.php @@ -48,8 +48,11 @@ class BaseEntityAdminForm extends AbstractType { - public function __construct(protected Security $security, protected EventCommentNeededHelper $eventCommentNeededHelper) - { + public function __construct( + protected Security $security, + protected EventCommentNeededHelper $eventCommentNeededHelper, + protected bool $useAssemblyIpnPlaceholder = false + ) { } public function configureOptions(OptionsResolver $resolver): void @@ -70,6 +73,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('name', TextType::class, [ 'empty_data' => '', 'label' => 'name.label', + 'data' => $is_new && $entity instanceof Assembly && $this->useAssemblyIpnPlaceholder ? '%%ipn%%' : $entity->getName(), 'attr' => [ 'placeholder' => 'part.name.placeholder', ], diff --git a/templates/admin/assembly_admin.html.twig b/templates/admin/assembly_admin.html.twig index 57dde7d15..e6a90dc09 100644 --- a/templates/admin/assembly_admin.html.twig +++ b/templates/admin/assembly_admin.html.twig @@ -29,6 +29,7 @@ {% block additional_controls %} {{ form_row(form.description) }} {{ form_row(form.status) }} + {{ form_row(form.ipn) }} {% if entity.id %}
diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index d617263a2..bd4ec699b 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -9870,6 +9870,12 @@ Element 3 Stav + + + assembly.edit.ipn + Interní číslo dílu (IPN) + + assembly.status.draft diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index cb08bf9a7..4cec94054 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -9896,6 +9896,12 @@ Element 3 Status + + + assembly.edit.ipn + Internt Partnummer (IPN) + + assembly.status.draft diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 9e79e5b1e..3140a1368 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -9884,6 +9884,12 @@ Element 1 -> Element 1.2 Status + + + assembly.edit.ipn + Internal Part Number (IPN) + + assembly.status.draft diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 8d09a014a..85942ddf5 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -9885,6 +9885,12 @@ Element 1 -> Element 1.2 Project status + + + assembly.edit.ipn + Internal Part Number (IPN) + + assembly.status.draft diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 3de5518ca..7ce3fe062 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -9900,6 +9900,12 @@ Elemento 3 Estatus + + + assembly.edit.ipn + Número de Componente Interno (IPN) + + assembly.status.draft diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 55d963caf..e7628984a 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -9902,6 +9902,12 @@ Element 3 Stato + + + assembly.edit.ipn + Codice interno (IPN) + + assembly.status.draft diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 6ae246be0..fc6eba18d 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -9905,6 +9905,12 @@ Element 3 Status + + + assembly.edit.ipn + Internal Part Number (IPN) + + assembly.status.draft diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 5f6584221..252b857bd 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -9909,6 +9909,12 @@ Статус + + + assembly.edit.ipn + Внутренний номер компонента (IPN) + + assembly.status.draft diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index e4b236db7..9c0b5a76f 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -9908,6 +9908,12 @@ Element 3 状态 + + + assembly.edit.ipn + 内部零件号 (IPN) + + assembly.status.draft diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index ee69c98c5..607b8a3b6 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -239,6 +239,12 @@ Interní číslo dílu musí být jedinečné. {{ value }} se již používá! + + + assembly.ipn.must_be_unique + Interní číslo dílu musí být jedinečné. {{ value }} se již používá! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 8494e436e..f25712719 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -239,6 +239,12 @@ Det interne partnummer skal være unikt. {{ value }} værdien er allerede i brug! + + + assembly.ipn.must_be_unique + Det interne partnummer skal være unikt. {{ value }} værdien er allerede i brug! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 23794c3a4..abd191d9e 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -239,6 +239,12 @@ Die Internal Part Number (IPN) muss einzigartig sein. Der Wert {{value}} wird bereits benutzt! + + + assembly.ipn.must_be_unique + Die Internal Part Number (IPN) muss einzigartig sein. Der Wert {{value}} wird bereits benutzt! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index e04b9dae3..580c91275 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -7,6 +7,12 @@ Ο εσωτερικός αριθμός εξαρτήματος πρέπει να είναι μοναδικός. {{ value }} χρησιμοποιείται ήδη! + + + assembly.ipn.must_be_unique + Ο εσωτερικός αριθμός εξαρτήματος πρέπει να είναι μοναδικός. {{ value }} χρησιμοποιείται ήδη! + + validator.project.bom_entry.only_part_or_assembly_allowed diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index ef4606ba6..e20e7ff8c 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -239,6 +239,12 @@ The internal part number must be unique. {{ value }} is already in use! + + + assembly.ipn.must_be_unique + The internal part number must be unique. {{ value }} is already in use! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 4df9c735b..c0af03354 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -239,6 +239,12 @@ Internal part number (IPN) mora biti jedinstven. {{ value }} je već u uporabi! + + + assembly.ipn.must_be_unique + Internal part number (IPN) mora biti jedinstven. {{ value }} je već u uporabi! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index cbc331d57..88369640f 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -239,6 +239,12 @@ Il codice interno (IPN) deve essere univoco. Il valore {{value}} è già in uso! + + + assembly.ipn.must_be_unique + Il codice interno (IPN) deve essere univoco. Il valore {{value}} è già in uso! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 1ef74c8eb..60713fa02 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -239,6 +239,12 @@ Wewnętrzny numer części musi być unikalny. {{value }} jest już w użyciu! + + + assembly.ipn.must_be_unique + Wewnętrzny numer części musi być unikalny. {{value }} jest już w użyciu! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index a878cc931..32540f8c7 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -239,6 +239,12 @@ Внутренний номер детали (IPN) должен быть уникальным. Значение {{value}} уже используется! + + + assembly.ipn.must_be_unique + Внутренний номер детали (IPN) должен быть уникальным. Значение {{value}} уже используется! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 3ac139f16..4a0ec79e8 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -239,6 +239,12 @@ 内部部件号是唯一的。{{ value }} 已被使用! + + + assembly.ipn.must_be_unique + 内部部件号是唯一的。{{ value }} 已被使用! + + validator.project.bom_entry.name_or_part_needed From 77bb5f35288a0672fa99c4249330d7e52dd60dc8 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 3 Jul 2025 13:38:51 +0200 Subject: [PATCH 16/83] =?UTF-8?q?Baugruppen=20St=C3=BCckliste=20um=20refer?= =?UTF-8?q?enzierte=20Baugruppe=20erweitern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/parameters.yaml | 3 +- config/services.yaml | 7 +- docs/configuration.md | 5 +- migrations/Version20250304081039.php | 9 +- migrations/Version20250627130848.php | 78 +++++++++++ .../AdminPages/BaseAdminController.php | 22 +++- src/Controller/TypeaheadController.php | 40 ++++++ src/Entity/AssemblySystem/Assembly.php | 25 +++- .../AssemblySystem/AssemblyBOMEntry.php | 23 ++++ .../AssemblySystem/AssemblyAddPartsType.php | 7 +- .../AssemblySystem/AssemblyBOMEntryType.php | 5 + src/Form/Type/AssemblySelectType.php | 124 ++++++++++++++++++ src/Repository/AssemblyRepository.php | 69 ++++++++++ .../AssemblySystem/AssemblyBuildHelper.php | 12 +- .../Attachments/AssemblyPreviewGenerator.php | 93 +++++++++++++ src/Services/Trees/TreeViewGenerator.php | 9 ++ src/Twig/AssemblyTwigExtension.php | 4 + .../UniqueReferencedAssembly.php | 34 +++++ .../UniqueReferencedAssemblyValidator.php | 48 +++++++ templates/assemblies/build/_form.html.twig | 2 + .../info/_attachments_info.html.twig | 91 +++++++++++++ templates/assemblies/info/info.html.twig | 2 +- ...collection_types_layout_assembly.html.twig | 17 +-- translations/messages.cs.xlf | 32 ++++- translations/messages.da.xlf | 32 ++++- translations/messages.de.xlf | 32 ++++- translations/messages.el.xlf | 32 ++++- translations/messages.en.xlf | 32 ++++- translations/messages.es.xlf | 32 ++++- translations/messages.fr.xlf | 32 ++++- translations/messages.it.xlf | 32 ++++- translations/messages.ja.xlf | 26 +++- translations/messages.nl.xlf | 32 ++++- translations/messages.pl.xlf | 32 ++++- translations/messages.ru.xlf | 32 ++++- translations/messages.zh.xlf | 32 ++++- translations/validators.cs.xlf | 14 +- translations/validators.da.xlf | 18 ++- translations/validators.de.xlf | 18 ++- translations/validators.el.xlf | 18 ++- translations/validators.en.xlf | 18 ++- translations/validators.fr.xlf | 18 ++- translations/validators.hr.xlf | 18 ++- translations/validators.it.xlf | 18 ++- translations/validators.ja.xlf | 18 ++- translations/validators.pl.xlf | 18 ++- translations/validators.ru.xlf | 18 ++- translations/validators.zh.xlf | 18 ++- 48 files changed, 1202 insertions(+), 149 deletions(-) create mode 100644 migrations/Version20250627130848.php create mode 100644 src/Form/Type/AssemblySelectType.php create mode 100644 src/Repository/AssemblyRepository.php create mode 100644 src/Services/Attachments/AssemblyPreviewGenerator.php create mode 100644 src/Validator/Constraints/AssemblySystem/UniqueReferencedAssembly.php create mode 100644 src/Validator/Constraints/AssemblySystem/UniqueReferencedAssemblyValidator.php create mode 100644 templates/assemblies/info/_attachments_info.html.twig diff --git a/config/parameters.yaml b/config/parameters.yaml index fc2400ce6..c6ef90e03 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -48,7 +48,8 @@ parameters: ###################################################################################################################### # Table settings ###################################################################################################################### - partdb.table.assemblies.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_DEFAULT_COLUMNS)%' # The default columns in assembly tables and their order + partdb.table.assemblies.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_DEFAULT_COLUMNS)%' # The default columns in assembly tables and their order + partdb.table.assemblies_bom.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS)%' # The default columns in assembly bom tables and their order ###################################################################################################################### # Miscellaneous diff --git a/config/services.yaml b/config/services.yaml index 88a729fa4..f787a5be3 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -174,9 +174,12 @@ services: App\DataTables\Helpers\ColumnSortHelper: shared: false # Service has a state so not share it between different tables + App\DataTables\AssemblyDataTable: + arguments: + $visible_columns: '%partdb.table.assemblies.default_columns%' App\DataTables\AssemblyBomEntriesDataTable: - arguments: - $visible_columns: '%partdb.table.assemblies.default_columns%' + arguments: + $visible_columns: '%partdb.table.assemblies_bom.default_columns%' #################################################################################################################### # Label system diff --git a/docs/configuration.md b/docs/configuration.md index 19899c061..498308b00 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -140,7 +140,10 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept * `TABLE_ASSEMBLIES_DEFAULT_COLUMNS`: The columns in assemblies tables, which are visible by default (when loading table for first time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns - are: `name`, `id`, `quantity`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `mountnames`, `instockAmount`, `storageLocations`, `addedDate`, `lastModified`. + are: `name`, `id`, `ipn`, `description`, `referencedAssemblies`, `edit`, `addedDate`, `lastModified`. +* `TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS`: The columns in assemblies bom tables, which are visible by default (when loading table for first time). + Also specify the default order of the columns. This is a comma separated list of column names. Available columns + are: `quantity`, `name`, `id`, `ipn`, `description`, `addedDate`, `lastModified`. * `CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME`: Use an %%ipn%% placeholder in the name of a assembly. Placeholder is replaced with the ipn input while saving. ### History/Eventlog-related settings diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php index ae2d6261b..ccdb24ac8 100644 --- a/migrations/Version20250304081039.php +++ b/migrations/Version20250304081039.php @@ -146,6 +146,7 @@ public function sqLiteUp(Schema $schema): void id_assembly INTEGER DEFAULT NULL, id_part INTEGER DEFAULT NULL, id_project INTEGER DEFAULT NULL, + id_referenced_assembly INTEGER DEFAULT NULL, price_currency_id INTEGER DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames CLOB NOT NULL, @@ -156,8 +157,9 @@ public function sqLiteUp(Schema $schema): void datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_8C74887E4AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE - CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887E22522999 FOREIGN KEY (id_referenced_assembly) REFERENCES assemblies (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE ) SQL); $this->addSql(<<<'SQL' @@ -169,6 +171,9 @@ public function sqLiteUp(Schema $schema): void $this->addSql(<<<'SQL' CREATE INDEX IDX_8C74887EF12E799E ON assembly_bom_entries (id_project) SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E22522999 ON assembly_bom_entries (id_referenced_assembly) + SQL); $this->addSql(<<<'SQL' CREATE INDEX IDX_8C74887E3FFDCD60 ON assembly_bom_entries (price_currency_id) SQL); diff --git a/migrations/Version20250627130848.php b/migrations/Version20250627130848.php new file mode 100644 index 000000000..16a6f3185 --- /dev/null +++ b/migrations/Version20250627130848.php @@ -0,0 +1,78 @@ +addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD id_referenced_assembly INT DEFAULT NULL AFTER id_project + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E22522999 FOREIGN KEY (id_referenced_assembly) REFERENCES assemblies (id) ON DELETE SET NULL + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E22522999 ON assembly_bom_entries (id_referenced_assembly) + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E22522999 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_8C74887E22522999 ON assembly_bom_entries + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP id_referenced_assembly + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function sqLiteDown(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD id_referenced_assembly INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E22522999 FOREIGN KEY (id_referenced_assembly) REFERENCES assemblies (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E22522999 ON assembly_bom_entries (id_referenced_assembly) + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887E22522999 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_8C74887E22522999 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP id_referenced_assembly + SQL); + } +} diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index c6d9cf20c..039751ea6 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -200,7 +200,7 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit * depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one, * to avoid having to insert it manually */ - $entity->setName(str_ireplace('%%ipn%%', $entity->getIpn(), $entity->getName())); + $entity->setName(str_ireplace('%%ipn%%', $entity->getIpn() ?? '', $entity->getName())); } $this->commentHelper->setMessage($form['log_comment']->getData()); @@ -233,6 +233,13 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit $repo = $this->entityManager->getRepository($this->entity_class); + $showParameters = true; + if ($this instanceof AssemblyAdminController) { + //currently not needed for assemblies + + $showParameters = false; + } + return $this->render($this->twig_template, [ 'entity' => $entity, 'form' => $form, @@ -242,7 +249,7 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit 'timeTravel' => $timeTravel_timestamp, 'repo' => $repo, 'partsContainingElement' => $repo instanceof PartsContainingRepositoryInterface, - 'showParameters' => !($this instanceof AssemblyAdminController), + 'showParameters' => $showParameters, ]); } @@ -303,7 +310,7 @@ protected function _new(Request $request, EntityManagerInterface $em, EntityImpo * depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one, * to avoid having to insert it manually */ - $new_entity->setName(str_ireplace('%%ipn%%', $new_entity->getIpn(), $new_entity->getName())); + $new_entity->setName(str_ireplace('%%ipn%%', $new_entity->getIpn() ?? '', $new_entity->getName())); } $this->commentHelper->setMessage($form['log_comment']->getData()); @@ -396,13 +403,20 @@ protected function _new(Request $request, EntityManagerInterface $em, EntityImpo } } + $showParameters = true; + if ($this instanceof AssemblyAdminController) { + //currently not needed for assemblies + + $showParameters = false; + } + return $this->render($this->twig_template, [ 'entity' => $new_entity, 'form' => $form, 'import_form' => $import_form, 'mass_creation_form' => $mass_creation_form, 'route_base' => $this->route_base, - 'showParameters' => !($this instanceof AssemblyAdminController), + 'showParameters' => $showParameters, ]); } diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 4335492e8..ca6ed8639 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -22,8 +22,10 @@ namespace App\Controller; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Parameters\AbstractParameter; use App\Entity\ProjectSystem\Project; +use App\Services\Attachments\AssemblyPreviewGenerator; use App\Services\Attachments\ProjectPreviewGenerator; use Symfony\Component\HttpFoundation\Response; use App\Entity\Attachments\Attachment; @@ -195,6 +197,44 @@ public function projects( return new JsonResponse($result); } + #[Route(path: '/assemblies/search/{query}', name: 'typeahead_assemblies')] + public function assemblies( + EntityManagerInterface $entityManager, + AssemblyPreviewGenerator $assemblyPreviewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" + ): JsonResponse { + $this->denyAccessUnlessGranted('@assemblies.read'); + + $result = []; + + $assemblyRepository = $entityManager->getRepository(Assembly::class); + + $assemblies = $assemblyRepository->autocompleteSearch($query, 100); + + foreach ($assemblies as $assembly) { + $preview_attachment = $assemblyPreviewGenerator->getTablePreviewAttachment($assembly); + + if($preview_attachment instanceof Attachment) { + $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); + } else { + $preview_url = ''; + } + + /** @var Assembly $assembly */ + $result[] = [ + 'id' => $assembly->getID(), + 'name' => $assembly->getName(), + 'category' => '', + 'footprint' => '', + 'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'), + 'image' => $preview_url, + ]; + } + + return new JsonResponse($result); + } + #[Route(path: '/parameters/{type}/search/{query}', name: 'typeahead_parameters', requirements: ['type' => '.+'])] public function parameters(string $type, EntityManagerInterface $entityManager, string $query = ""): JsonResponse { diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 7897ea36b..5991b9e1d 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -22,6 +22,7 @@ namespace App\Entity\AssemblySystem; +use App\Repository\AssemblyRepository; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; @@ -38,6 +39,7 @@ use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Attachments\Attachment; use App\Validator\Constraints\UniqueObjectCollection; +use App\Validator\Constraints\AssemblySystem\UniqueReferencedAssembly; use Doctrine\DBAL\Types\Types; use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Base\AbstractStructuralDBElement; @@ -58,7 +60,7 @@ * * @extends AbstractStructuralDBElement */ -#[ORM\Entity] +#[ORM\Entity(repositoryClass: AssemblyRepository::class)] #[ORM\Table(name: 'assemblies')] #[UniqueEntity(fields: ['ipn'], message: 'assembly.ipn.must_be_unique')] #[ORM\Index(columns: ['ipn'], name: 'assembly_idx_ipn')] @@ -109,8 +111,9 @@ class Assembly extends AbstractStructuralDBElement */ #[Assert\Valid] #[Groups(['extended', 'full', 'import'])] - #[ORM\OneToMany(mappedBy: 'assembly', targetEntity: AssemblyBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OneToMany(targetEntity: AssemblyBOMEntry::class, mappedBy: 'assembly', cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])] + #[UniqueReferencedAssembly] #[UniqueObjectCollection(message: 'assembly.bom_entry.project_already_in_bom', fields: ['project'])] #[UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name'])] protected Collection $bom_entries; @@ -386,4 +389,22 @@ public function validate(ExecutionContextInterface $context, $payload): void } } } + + /** + * Get all referenced assemblies which uses this assembly. + * + * @return Assembly[] all referenced assemblies which uses this assembly as a one-dimensional array of assembly objects + */ + public function getReferencedAssemblies(): array + { + $assemblies = []; + + foreach($this->bom_entries as $entry) { + if ($entry->getAssembly() !== null) { + $assemblies[] = $entry->getReferencedAssembly(); + } + } + + return $assemblies; + } } diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index f6ede2194..820fc2f5c 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -133,6 +133,18 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt #[Groups(['bom_entry:read', 'bom_entry:write', 'full'])] protected ?Part $part = null; + /** + * @var Assembly|null The associated assembly + */ + #[Assert\Expression( + '(this.getPart() === null or this.getReferencedAssembly() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))', + message: 'validator.assembly.bom_entry.only_part_or_assembly_allowed' + )] + #[ORM\ManyToOne(targetEntity: Assembly::class)] + #[ORM\JoinColumn(name: 'id_referenced_assembly', nullable: true, onDelete: 'SET NULL')] + #[Groups(['bom_entry:read', 'bom_entry:write', ])] + protected ?Assembly $referencedAssembly = null; + /** * @var Project|null The associated project */ @@ -237,6 +249,17 @@ public function setPart(?Part $part): AssemblyBOMEntry return $this; } + public function getReferencedAssembly(): ?Assembly + { + return $this->referencedAssembly; + } + + public function setReferencedAssembly(?Assembly $referencedAssembly): AssemblyBOMEntry + { + $this->referencedAssembly = $referencedAssembly; + return $this; + } + public function getProject(): ?Project { return $this->project; diff --git a/src/Form/AssemblySystem/AssemblyAddPartsType.php b/src/Form/AssemblySystem/AssemblyAddPartsType.php index 4d84881f0..1fa671266 100644 --- a/src/Form/AssemblySystem/AssemblyAddPartsType.php +++ b/src/Form/AssemblySystem/AssemblyAddPartsType.php @@ -51,14 +51,17 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder->add('bom_entries', AssemblyBOMEntryCollectionType::class, [ 'entry_options' => [ 'constraints' => [ - new UniqueEntity(fields: ['part', 'assembly'], message: 'assembly.bom_entry.part_already_in_bom', + new UniqueEntity(fields: ['part'], message: 'assembly.bom_entry.part_already_in_bom', entityClass: AssemblyBOMEntry::class), - new UniqueEntity(fields: ['name', 'assembly'], message: 'assembly.bom_entry.name_already_in_bom', + new UniqueEntity(fields: ['referencedAssembly'], message: 'assembly.bom_entry.assembly_already_in_bom', + entityClass: AssemblyBOMEntry::class), + new UniqueEntity(fields: ['name'], message: 'assembly.bom_entry.name_already_in_bom', entityClass: AssemblyBOMEntry::class, ignoreNull: true), ] ], 'constraints' => [ new UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part']), + new UniqueObjectCollection(message: 'assembly.bom_entry.assembly_already_in_bom', fields: ['referencedAssembly']), new UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name']), ] ]); diff --git a/src/Form/AssemblySystem/AssemblyBOMEntryType.php b/src/Form/AssemblySystem/AssemblyBOMEntryType.php index 42d463bd9..b86b2fd1b 100644 --- a/src/Form/AssemblySystem/AssemblyBOMEntryType.php +++ b/src/Form/AssemblySystem/AssemblyBOMEntryType.php @@ -5,6 +5,7 @@ namespace App\Form\AssemblySystem; use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Form\Type\AssemblySelectType; use App\Form\Type\BigDecimalNumberType; use App\Form\Type\CurrencyEntityType; use App\Form\Type\PartSelectType; @@ -42,6 +43,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'assembly.bom.project', 'required' => false, ]) + ->add('referencedAssembly', AssemblySelectType::class, [ + 'label' => 'assembly.bom.referencedAssembly', + 'required' => false, + ]) ->add('name', TextType::class, [ 'label' => 'assembly.bom.name', 'required' => false, diff --git a/src/Form/Type/AssemblySelectType.php b/src/Form/Type/AssemblySelectType.php new file mode 100644 index 000000000..10e858f26 --- /dev/null +++ b/src/Form/Type/AssemblySelectType.php @@ -0,0 +1,124 @@ +addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { + $form = $event->getForm(); + $config = $form->getConfig()->getOptions(); + $data = $event->getData() ?? []; + + $config['compound'] = false; + $config['choices'] = is_iterable($data) ? $data : [$data]; + $config['error_bubbling'] = true; + + $form->add('autocomplete', EntityType::class, $config); + }); + + //After form submit, we have to add the selected element as choice, otherwise the form will not accept this element + $builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) { + $data = $event->getData(); + $form = $event->getForm(); + $options = $form->get('autocomplete')->getConfig()->getOptions(); + + + if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) { + $options['choices'] = []; + } else { + //Extract the ID from the submitted data + $id = $data['autocomplete']; + //Find the element in the database + $element = $this->em->find($options['class'], $id); + + //Add the element as choice + $options['choices'] = [$element]; + $options['error_bubbling'] = true; + $form->add('autocomplete', EntityType::class, $options); + } + }); + + $builder->setDataMapper($this); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'class' => Assembly::class, + 'choice_label' => 'name', + 'compound' => true, + 'error_bubbling' => false, + ]); + + error_log($this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__'])); + + $resolver->setDefaults([ + 'attr' => [ + 'data-controller' => 'elements--assembly-select', + 'data-autocomplete' => $this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']), + 'autocomplete' => 'off', + ], + ]); + + $resolver->setDefaults([ + //Prefill the selected choice with the needed data, so the user can see it without an additional Ajax request + 'choice_attr' => ChoiceList::attr($this, function (?Assembly $assembly) { + if($assembly instanceof Assembly) { + //Determine the picture to show: + $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($assembly); + if ($preview_attachment instanceof Attachment) { + $preview_url = $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, + 'thumbnail_sm'); + } else { + $preview_url = ''; + } + } + + return $assembly instanceof Assembly ? [ + 'data-description' => $assembly->getDescription() ? mb_strimwidth($assembly->getDescription(), 0, 127, '...') : '', + 'data-category' => '', + 'data-footprint' => '', + 'data-image' => $preview_url, + ] : []; + }) + ]); + } + + public function mapDataToForms($data, \Traversable $forms): void + { + $form = current(iterator_to_array($forms, false)); + $form->setData($data); + } + + public function mapFormsToData(\Traversable $forms, &$data): void + { + $form = current(iterator_to_array($forms, false)); + $data = $form->getData(); + } + +} diff --git a/src/Repository/AssemblyRepository.php b/src/Repository/AssemblyRepository.php new file mode 100644 index 000000000..031e6e82b --- /dev/null +++ b/src/Repository/AssemblyRepository.php @@ -0,0 +1,69 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 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 . + */ + +namespace App\Repository; + +use App\Entity\AssemblySystem\Assembly; + +/** + * @template TEntityClass of Assembly + * @extends DBElementRepository + */ +class AssemblyRepository extends StructuralDBElementRepository +{ + /** + * @return Assembly[] + */ + public function autocompleteSearch(string $query, int $max_limits = 50): array + { + $qb = $this->createQueryBuilder('assembly'); + $qb->select('assembly') + ->where('ILIKE(assembly.name, :query) = TRUE') + ->orWhere('ILIKE(assembly.description, :query) = TRUE'); + + $qb->setParameter('query', '%'.$query.'%'); + + $qb->setMaxResults($max_limits); + $qb->orderBy('NATSORT(assembly.name)', 'ASC'); + + return $qb->getQuery()->getResult(); + } +} \ No newline at end of file diff --git a/src/Services/AssemblySystem/AssemblyBuildHelper.php b/src/Services/AssemblySystem/AssemblyBuildHelper.php index 3fb3221ac..b7f2df3ce 100644 --- a/src/Services/AssemblySystem/AssemblyBuildHelper.php +++ b/src/Services/AssemblySystem/AssemblyBuildHelper.php @@ -67,6 +67,7 @@ public function getMaximumBuildableCountForBOMEntry(AssemblyBOMEntry $assemblyBO public function getMaximumBuildableCount(Assembly $assembly): int { $maximum_buildable_count = PHP_INT_MAX; + /** @var AssemblyBOMEntry $bom_entry */ foreach ($assembly->getBomEntries() as $bom_entry) { //Skip BOM entries without a part (as we can not determine that) if (!$bom_entry->isPartBomEntry() && $bom_entry->getProject() === null) { @@ -76,8 +77,8 @@ public function getMaximumBuildableCount(Assembly $assembly): int //The maximum buildable count for the whole project is the minimum of all BOM entries if ($bom_entry->getPart() !== null) { $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); - } elseif ($bom_entry->getProject() !== null) { - $maximum_buildable_count = min($maximum_buildable_count, $this->projectBuildHelper->getMaximumBuildableCount($bom_entry->getProject())); + } elseif ($bom_entry->getReferencedAssembly() !== null) { + $maximum_buildable_count = min($maximum_buildable_count, $this->projectBuildHelper->getMaximumBuildableCount($bom_entry->getReferencedAssembly())); } } @@ -117,11 +118,12 @@ public function getNonBuildableAssemblyBomEntries(Assembly $assembly, int $numbe $nonBuildableEntries = []; + /** @var AssemblyBOMEntry $bomEntry */ foreach ($assembly->getBomEntries() as $bomEntry) { $part = $bomEntry->getPart(); //Skip BOM entries without a part (as we can not determine that) - if (!$part instanceof Part && $bomEntry->getAssembly() === null) { + if (!$part instanceof Part && $bomEntry->getReferencedAssembly() === null) { continue; } @@ -131,8 +133,8 @@ public function getNonBuildableAssemblyBomEntries(Assembly $assembly, int $numbe if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { $nonBuildableEntries[] = $bomEntry; } - } elseif ($bomEntry->getAssembly() !== null) { - $nonBuildableAssemblyEntries = $this->projectBuildHelper->getNonBuildableProjectBomEntries($bomEntry->getProject(), $number_of_builds); + } elseif ($bomEntry->getReferencedAssembly() !== null) { + $nonBuildableAssemblyEntries = $this->getNonBuildableAssemblyBomEntries($bomEntry->getReferencedAssembly(), $number_of_builds); $nonBuildableEntries = array_merge($nonBuildableEntries, $nonBuildableAssemblyEntries); } } diff --git a/src/Services/Attachments/AssemblyPreviewGenerator.php b/src/Services/Attachments/AssemblyPreviewGenerator.php new file mode 100644 index 000000000..9ecbbd070 --- /dev/null +++ b/src/Services/Attachments/AssemblyPreviewGenerator.php @@ -0,0 +1,93 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\Attachments; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\Attachment; + +class AssemblyPreviewGenerator +{ + public function __construct(protected AttachmentManager $attachmentHelper) + { + } + + /** + * Returns a list of attachments that can be used for previewing the assembly ordered by priority. + * + * @param Assembly $assembly the assembly for which the attachments should be determined + * + * @return (Attachment|null)[] + * + * @psalm-return list + */ + public function getPreviewAttachments(Assembly $assembly): array + { + $list = []; + + //Master attachment has top priority + $attachment = $assembly->getMasterPictureAttachment(); + if ($this->isAttachmentValidPicture($attachment)) { + $list[] = $attachment; + } + + //Then comes the other images of the assembly + foreach ($assembly->getAttachments() as $attachment) { + //Dont show the master attachment twice + if ($this->isAttachmentValidPicture($attachment) && $attachment !== $assembly->getMasterPictureAttachment()) { + $list[] = $attachment; + } + } + + return $list; + } + + /** + * Determines what attachment should be used for previewing a assembly (especially in assembly table). + * The returned attachment is guaranteed to be existing and be a picture. + * + * @param Assembly $assembly The assembly for which the attachment should be determined + */ + public function getTablePreviewAttachment(Assembly $assembly): ?Attachment + { + $attachment = $assembly->getMasterPictureAttachment(); + if ($this->isAttachmentValidPicture($attachment)) { + return $attachment; + } + + return null; + } + + /** + * Checks if a attachment is exising and a valid picture. + * + * @param Attachment|null $attachment the attachment that should be checked + * + * @return bool true if the attachment is valid + */ + protected function isAttachmentValidPicture(?Attachment $attachment): bool + { + return $attachment instanceof Attachment + && $attachment->isPicture() + && $this->attachmentHelper->isFileExisting($attachment); + } +} diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index fa9935c8f..3a0979028 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -188,6 +188,15 @@ private function getTreeViewUncached( $root_node->setExpanded($this->rootNodeExpandedByDefault); $root_node->setIcon($this->entityClassToRootNodeIcon($class)); + $generic = [$root_node]; + } elseif ($mode === 'assemblies' && $this->rootNodeEnabled) { + //We show the root node as a link to the list of all assemblies + $show_all_parts_url = $this->router->generate('assemblies_list'); + + $root_node = new TreeViewNode($this->entityClassToRootNodeString($class), $show_all_parts_url, $generic); + $root_node->setExpanded($this->rootNodeExpandedByDefault); + $root_node->setIcon($this->entityClassToRootNodeIcon($class)); + $generic = [$root_node]; } diff --git a/src/Twig/AssemblyTwigExtension.php b/src/Twig/AssemblyTwigExtension.php index d43c201e8..a8ca7719a 100644 --- a/src/Twig/AssemblyTwigExtension.php +++ b/src/Twig/AssemblyTwigExtension.php @@ -2,6 +2,7 @@ namespace App\Twig; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; @@ -14,6 +15,9 @@ public function getFunctions(): array ]; } + /** + * @param AssemblyBOMEntry[] $bomEntries + */ public function hasProject(array $bomEntries): bool { foreach ($bomEntries as $entry) { diff --git a/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssembly.php b/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssembly.php new file mode 100644 index 000000000..55a31440a --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssembly.php @@ -0,0 +1,34 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use Symfony\Component\Validator\Constraint; + +/** + * This constraint checks that the given UniqueReferencedAssembly is valid. + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class UniqueReferencedAssembly extends Constraint +{ + public string $message = 'assembly.bom_entry.assembly_already_in_bom'; +} \ No newline at end of file diff --git a/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssemblyValidator.php b/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssemblyValidator.php new file mode 100644 index 000000000..0e58c0663 --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssemblyValidator.php @@ -0,0 +1,48 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; + +class UniqueReferencedAssemblyValidator extends ConstraintValidator +{ + public function validate($value, Constraint $constraint) + { + $assemblies = []; + foreach ($value as $entry) { + $referencedAssemblyId = $entry->getReferencedAssembly()?->getId(); + if ($referencedAssemblyId === null) { + continue; + } + + if (isset($assemblies[$referencedAssemblyId])) { + $this->context->buildViolation($constraint->message) + ->atPath('referencedAssembly') + ->addViolation(); + return; + } + $assemblies[$referencedAssemblyId] = true; + } + } +} \ No newline at end of file diff --git a/templates/assemblies/build/_form.html.twig b/templates/assemblies/build/_form.html.twig index 0123ab010..97cace564 100644 --- a/templates/assemblies/build/_form.html.twig +++ b/templates/assemblies/build/_form.html.twig @@ -28,6 +28,8 @@ {% if bom_entry.part %} {{ bom_entry.part.name }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} + {% elseif bom_entry.referencedAssembly %} + {{ 'assembly.build.form.referencedAssembly'|trans({'%name%': bom_entry.referencedAssembly.name}) }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} {% else %} {{ bom_entry.name }} {% endif %} diff --git a/templates/assemblies/info/_attachments_info.html.twig b/templates/assemblies/info/_attachments_info.html.twig new file mode 100644 index 000000000..747426c3a --- /dev/null +++ b/templates/assemblies/info/_attachments_info.html.twig @@ -0,0 +1,91 @@ +{% import "helper.twig" as helper %} + + + + + + + + + + + + + + + + + {% for attachment in assembly.attachments %} + + + + + + + + + + {% endfor %} + + + +
{% trans %}attachment.name{% endtrans %}{% trans %}attachment.attachment_type{% endtrans %}{% trans %}attachment.file_name{% endtrans %}{% trans %}attachment.file_size{% endtrans %}
+ {% import "components/attachments.macro.html.twig" as attachments %} + {{ attachments.attachment_icon(attachment, attachment_manager) }} + {{ attachment.name }}{{ attachment.attachmentType.fullPath }} + {% if attachment.hasInternal() %} + {{ attachment.filename }} + {% endif %} + + {% if not attachment.hasInternal() %} + + {% trans %}attachment.external_only{% endtrans %} + + {% elseif attachment_manager.internalFileExisting(attachment) %} + + {{ attachment_manager.humanFileSize(attachment) }} + + {% else %} + + {% trans %}attachment.file_not_found{% endtrans %} + + {% endif %} + {% if attachment.secure %} +
+ {% trans %}attachment.secure{% endtrans %} + + {% endif %} + {% if attachment == assembly.masterPictureAttachment %} +
+ + {% trans %}attachment.preview{% endtrans %} + + {% endif %} +
+ + + + + + + + + + +
+ + +
+
\ No newline at end of file diff --git a/templates/assemblies/info/info.html.twig b/templates/assemblies/info/info.html.twig index 166535a64..d787ea086 100644 --- a/templates/assemblies/info/info.html.twig +++ b/templates/assemblies/info/info.html.twig @@ -108,7 +108,7 @@ {% include "assemblies/info/_builds.html.twig" %}
- {% include "parts/info/_attachments_info.html.twig" with {"part": assembly} %} + {% include "assemblies/info/_attachments_info.html.twig" with {"assembly": assembly} %}
diff --git a/templates/form/collection_types_layout_assembly.html.twig b/templates/form/collection_types_layout_assembly.html.twig index 24964801a..6dc6d49a0 100644 --- a/templates/form/collection_types_layout_assembly.html.twig +++ b/templates/form/collection_types_layout_assembly.html.twig @@ -6,7 +6,7 @@ {# expand button #} {% trans %}assembly.bom.quantity{% endtrans %} - {% trans %}assembly.bom.partOrProject{% endtrans %} + {% trans %}assembly.bom.partOrAssembly{% endtrans %} {% trans %}assembly.bom.name{% endtrans %} {# Remove button #} @@ -44,18 +44,9 @@ {{ form_row(form.part) }} {{ form_errors(form.part) }} - - {% if form.vars.value is not null and form.vars.value.assembly is not null %} - {% if is_granted("@projects.read") or has_project(form.vars.value.assembly.bomEntries.toArray) %} -
- {{ form_widget(form.project) }} - {{ form_errors(form.project) }} - {% endif %} - {% elseif is_granted("@projects.read") %} -
- {{ form_widget(form.project) }} - {{ form_errors(form.project) }} - {% endif %} +
+ {{ form_widget(form.referencedAssembly) }} + {{ form_errors(form.referencedAssembly) }} {{ form_widget(form.name) }} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index bd4ec699b..ce79aaced 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -9867,7 +9867,7 @@ Element 3 assembly.edit.status - Stav + Stav sestavy @@ -13408,6 +13408,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz %value% (Součást) + + + part.table.name.value.for_assembly + %value% (Sestava) + + part.table.name.value.for_project @@ -13444,6 +13450,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Sestavy + + + assembly.referencedAssembly.labelp + Odkazované sestavy + + assembly.edit @@ -13612,6 +13624,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Sestavit + + + assembly.build.form.referencedAssembly + Sestava "%name%" + + assembly.builds.no_stocked_builds @@ -13666,6 +13684,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Projekt + + + assembly.bom.referencedAssembly + Sestava + + assembly.bom.name @@ -13702,10 +13726,10 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Importovat součásti do sestavy - + - assembly.bom.partOrProject - Součást + assembly.bom.partOrAssembly + Součást nebo sestava diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 4cec94054..c68acdd4b 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -9893,7 +9893,7 @@ Element 3 assembly.edit.status - Status + Samlingens status @@ -12569,6 +12569,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver %value% (Del) + + + part.table.name.value.for_assembly + %value% (Samling) + + part.table.name.value.for_project @@ -12605,6 +12611,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Samlinger + + + assembly.referencedAssembly.labelp + Refererede samlinger + + assembly.edit @@ -12773,6 +12785,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Byg + + + assembly.build.form.referencedAssembly + Samling "%name%" + + assembly.builds.no_stocked_builds @@ -12827,6 +12845,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Projekt + + + assembly.bom.referencedAssembly + Sammenstilling + + assembly.bom.name @@ -12863,10 +12887,10 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Importer dele til samling - + - assembly.bom.partOrProject - Del + assembly.bom.partOrAssembly + Del eller samling diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 3140a1368..bee171d41 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -4746,6 +4746,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr %value% (Bauteil) + + + part.table.name.value.for_assembly + %value% (Baugruppe) + + part.table.name.value.for_project @@ -9881,7 +9887,7 @@ Element 1 -> Element 1.2 assembly.edit.status - Status + Status Baugruppe @@ -13260,6 +13266,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Baugruppen + + + assembly.referencedAssembly.labelp + Referenzierte Baugruppen + + assembly.edit @@ -13428,6 +13440,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Bauen + + + asssembly.build.form.referencedAssembly + Baugruppe "%name%" + + assembly.builds.no_stocked_builds @@ -13482,6 +13500,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Projekt + + + assembly.bom.referencedAssembly + Baugruppe + + assembly.bom.name @@ -13518,10 +13542,10 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Importiere Parts für Baugruppe - + - assembly.bom.partOrProject - Bauteil oder Projekt + assembly.bom.partOrAssembly + Bauteil oder Baugruppe diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index fc03f08be..97b27f53f 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1541,6 +1541,12 @@ %value% (Μέρος) + + + part.table.name.value.for_assembly + %value% (Συναρμολόγηση) + + part.table.name.value.for_project @@ -1550,7 +1556,7 @@ assembly.edit.status - Κατάσταση + Κατάσταση συναρμολόγησης @@ -1613,6 +1619,12 @@ Συναρμολογήσεις + + + assembly.referencedAssembly.labelp + Αναφερόμενες συναρμολογήσεις + + assembly.edit @@ -1781,6 +1793,12 @@ Κατασκευή + + + assembly.build.form.referencedAssembly + Συναρμολόγηση "%name%" + + assembly.builds.no_stocked_builds @@ -1835,6 +1853,12 @@ έργο + + + assembly.bom.referencedAssembly + Συναρμολόγηση + + assembly.bom.name @@ -1871,10 +1895,10 @@ Εισαγωγή εξαρτημάτων συναρμολόγησης - + - assembly.bom.partOrProject - Εξάρτημα + assembly.bom.partOrAssembly + Μέρος ή συναρμολόγηση diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 85942ddf5..4f398d6b0 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -4747,6 +4747,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can %value% (Part) + + + part.table.name.value.for_assembly + %value% (Assembly) + + part.table.name.value.for_project @@ -9882,7 +9888,7 @@ Element 1 -> Element 1.2 assembly.edit.status - Project status + Assembly status @@ -13261,6 +13267,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Assemblies + + + assembly.referencedAssembly.labelp + Referenced assemblies + + assembly.edit @@ -13429,6 +13441,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Build + + + assembly.build.form.referencedAssembly + Assembly "%name%" + + assembly.builds.no_stocked_builds @@ -13483,6 +13501,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Project + + + assembly.bom.referencedAssembly + Assembly + + assembly.bom.name @@ -13519,10 +13543,10 @@ Please note, that you can not impersonate a disabled user. If you try you will g Import part list for assembly - + - assembly.bom.partOrProject - Part + assembly.bom.partOrAssembly + Part or assembly diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 7ce3fe062..b67198a78 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -4746,6 +4746,12 @@ Subelementos serán desplazados hacia arriba. %value% (Componente) + + + part.table.name.value.for_assembly + %value% (Ensamblaje) + + part.table.name.value.for_project @@ -9897,7 +9903,7 @@ Elemento 3 assembly.edit.status - Estatus + Estado del ensamblaje @@ -12753,6 +12759,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Ensamblajes + + + assembly.referencedAssembly.labelp + Conjuntos referenciados + + assembly.edit @@ -12921,6 +12933,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Construir + + + assembly.build.form.referencedAssembly + Ensamblaje "%name%" + + assembly.builds.no_stocked_builds @@ -12975,6 +12993,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Proyecto + + + assembly.bom.referencedAssembly + Ensamblaje + + assembly.bom.name @@ -13011,10 +13035,10 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Importar piezas para ensamblaje - + - assembly.bom.partOrProject - Pieza + assembly.bom.partOrAssembly + Parte o conjunto diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 4bb77d8eb..ecfcf5928 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -4709,6 +4709,12 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia %value% (Componente) + + + part.table.name.value.for_assembly + %value% (Assemblage) + + part.table.name.value.for_project @@ -9112,7 +9118,7 @@ exemple de ville assembly.edit.status - Statut + Statut de l'assemblage @@ -9175,6 +9181,12 @@ exemple de ville Assemblages + + + assembly.referencedAssembly.labelp + Assemblages référencés + + assembly.edit @@ -9343,6 +9355,12 @@ exemple de ville Construire + + + assembly.build.form.referencedAssembly + Assemblage "%name%" + + assembly.builds.no_stocked_builds @@ -9397,6 +9415,12 @@ exemple de ville Projet + + + assembly.bom.referencedAssembly + Assemblage + + assembly.bom.name @@ -9433,10 +9457,10 @@ exemple de ville Importer des pièces pour l'assemblage - + - assembly.bom.partOrProject - Pièce + assembly.bom.partOrAssembly + Pièce ou assemblage diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index e7628984a..12e20ca2c 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -4748,6 +4748,12 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi %value% (Componente) + + + part.table.name.value.for_assembly + %value% (Assemblaggio) + + part.table.name.value.for_project @@ -9899,7 +9905,7 @@ Element 3 assembly.edit.status - Stato + Stato dell'assemblaggio @@ -12755,6 +12761,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Assemblaggi + + + assembly.referencedAssembly.labelp + Assembly referenziati + + assembly.edit @@ -12923,6 +12935,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Costruire + + + assembly.build.form.referencedAssembly + Gruppo "%name%" + + assembly.builds.no_stocked_builds @@ -12977,6 +12995,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Progetto + + + assembly.bom.referencedAssembly + Assemblaggio + + assembly.bom.name @@ -13013,10 +13037,10 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Importa componenti per il gruppo - + - assembly.bom.partOrProject - Componente + assembly.bom.partOrAssembly + Parte o assieme diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index b61b585f3..b7be7490d 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -4709,6 +4709,12 @@ %value%(部品) + + + part.table.name.value.for_assembly + %value%(アセンブリ) + + part.table.name.value.for_project @@ -8849,7 +8855,7 @@ Exampletown assembly.edit.status - ステータス + アセンブリのステータス @@ -8912,6 +8918,12 @@ Exampletown アセンブリ一覧 + + + assembly.referencedAssembly.labelp + 参照されたアセンブリ + + assembly.edit @@ -9080,6 +9092,12 @@ Exampletown ビルド + + + assembly.build.form.referencedAssembly + アセンブリ「%name%」 + + assembly.builds.no_stocked_builds @@ -9134,6 +9152,12 @@ Exampletown プロジェクト + + + assembly.bom.referencedAssembly + アセンブリ + + assembly.bom.name diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index c52a4733a..fe17c3aca 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -730,6 +730,12 @@ %value% (Onderdeel) + + + part.table.name.value.for_assembly + %value% (Assemblage) + + part.table.name.value.for_project @@ -739,7 +745,7 @@ assembly.edit.status - Κατάσταση + Montagestatus @@ -802,6 +808,12 @@ Assemblages + + + assembly.referencedAssembly.labelp + Gerefereerde assemblages + + assembly.edit @@ -970,6 +982,12 @@ Bouwen + + + assembly.build.form.referencedAssembly + Assemblage "%name%" + + assembly.builds.no_stocked_builds @@ -1024,6 +1042,12 @@ Project + + + assembly.bom.referencedAssembly + Assemblage + + assembly.bom.name @@ -1060,10 +1084,10 @@ Importeer onderdelen voor assemblage - + - assembly.bom.partOrProject - Onderdeel + assembly.bom.partOrAssembly + Onderdeel of samenstelling diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index fc6eba18d..73b6e4fcc 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -4751,6 +4751,12 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo %value%(部品) + + + part.table.name.value.for_assembly + %value% (Złożenie) + + part.table.name.value.for_project @@ -9902,7 +9908,7 @@ Element 3 assembly.edit.status - Status + Status montażu @@ -12632,6 +12638,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Zespoły + + + assembly.referencedAssembly.labelp + Odwołane zestawy + + assembly.edit @@ -12800,6 +12812,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Zbuduj + + + assembly.build.form.referencedAssembly + Zespół "%name%" + + assembly.builds.no_stocked_builds @@ -12854,6 +12872,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Projekt + + + assembly.bom.referencedAssembly + Złożenie + + assembly.bom.name @@ -12890,10 +12914,10 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Importuj części dla zespołu - + - assembly.bom.partOrProject - Część + assembly.bom.partOrAssembly + Część lub zespół diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 252b857bd..751ff35f1 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -4757,6 +4757,12 @@ %value% (Часть) + + + part.table.name.value.for_assembly + %value% (Сборка) + + part.table.name.value.for_project @@ -9906,7 +9912,7 @@ assembly.edit.status - Статус + Статус сборки @@ -12732,6 +12738,12 @@ Сборки + + + assembly.referencedAssembly.labelp + Ссылочные сборки + + assembly.edit @@ -12900,6 +12912,12 @@ Собрать + + + assembly.build.form.referencedAssembly + Сборка "%name%" + + assembly.builds.no_stocked_builds @@ -12954,6 +12972,12 @@ Проект + + + assembly.bom.referencedAssembly + Сборка + + assembly.bom.name @@ -12990,10 +13014,10 @@ Импортировать детали для сборки - + - assembly.bom.partOrProject - Компонент + assembly.bom.partOrAssembly + Часть или сборка diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 9c0b5a76f..396feae3e 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -4755,6 +4755,12 @@ %value%(部件) + + + part.table.name.value.for_assembly + %value%(装配) + + part.table.name.value.for_project @@ -9905,7 +9911,7 @@ Element 3 assembly.edit.status - 状态 + 装配状态 @@ -12617,6 +12623,12 @@ Element 3 装配列表 + + + assembly.referencedAssembly.labelp + 引用的程序集 + + assembly.edit @@ -12785,6 +12797,12 @@ Element 3 构建 + + + assembly.build.form.referencedAssembly + 组件“%name%” + + assembly.builds.no_stocked_builds @@ -12839,6 +12857,12 @@ Element 3 项目 + + + assembly.bom.referencedAssembly + 组件 + + assembly.bom.name @@ -12875,10 +12899,10 @@ Element 3 导入组件的零件 - + - assembly.bom.partOrProject - 零件 + assembly.bom.partOrAssembly + 部件或组件 diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 607b8a3b6..f053743f3 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -257,7 +257,7 @@ Je povoleno vybrat pouze jednu součástku nebo sestavu. Upravit prosím svůj výběr! - + project.bom_entry.name_already_in_bom Již existuje položka BOM s tímto názvem! @@ -395,6 +395,12 @@ Tato součást již existuje ve skupině! + + + assembly.bom_entry.assembly_already_in_bom + Tato sestava již existuje jako položka v seznamu materiálů! + + assembly.bom_entry.project_already_in_bom @@ -413,6 +419,12 @@ Musíte vybrat součást nebo nastavit název pro nesoučást! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Je povoleno vybrat pouze jednu součástku nebo sestavu. Upravit prosím svůj výběr! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index f25712719..239e3572a 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -251,12 +251,6 @@ Du skal vælge en komponent eller angive et navn til en ikke-komponent styklistepost! - - - validator.project.bom_entry.only_part_or_assembly_allowed - Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! - - project.bom_entry.name_already_in_bom @@ -371,6 +365,12 @@ Denne del eksisterer allerede i gruppen! + + + assembly.bom_entry.assembly_already_in_bom + Denne samling findes allerede som en post! + + assembly.bom_entry.project_already_in_bom @@ -389,6 +389,12 @@ Du skal vælge en del eller sætte et navn for en ikke-del! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index abd191d9e..86b31e583 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -251,12 +251,6 @@ Sie müssen ein Bauteil bzw. eine Baugruppe auswählen, oder einen Namen für ein nicht-Bauteil BOM-Eintrag setzen! - - - validator.project.bom_entry.only_part_or_assembly_allowed - Es darf nur ein Bauteil oder eine Baugruppe ausgewählt werden. Bitte passen Sie Ihre Auswahl an! - - project.bom_entry.name_already_in_bom @@ -395,6 +389,12 @@ Dieses Bauteil existiert bereits in der Gruppe! + + + assembly.bom_entry.assembly_already_in_bom + Diese Baugruppe existiert bereits als Eintrag! + + assembly.bom_entry.project_already_in_bom @@ -413,6 +413,12 @@ Sie müssen ein Bauteil auswählen, oder einen Namen für den Eintrag setzen! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Es darf nur ein Bauteil oder eine Baugruppe ausgewählt werden. Bitte passen Sie Ihre Auswahl an! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index 580c91275..4e4278daf 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -13,12 +13,6 @@ Ο εσωτερικός αριθμός εξαρτήματος πρέπει να είναι μοναδικός. {{ value }} χρησιμοποιείται ήδη! - - - validator.project.bom_entry.only_part_or_assembly_allowed - Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! - - validator.bom_importer.invalid_import_type @@ -37,6 +31,12 @@ Αυτό το εξάρτημα υπάρχει ήδη στην ομάδα! + + + assembly.bom_entry.assembly_already_in_bom + Αυτή η συναρμολόγηση υπάρχει ήδη ως εγγραφή! + + assembly.bom_entry.project_already_in_bom @@ -55,6 +55,12 @@ Πρέπει να επιλέξετε ένα εξάρτημα ή να βάλετε ένα όνομα για ένα μη εξάρτημα! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index e20e7ff8c..59cabf552 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -251,12 +251,6 @@ You have to select a part or assembly, or set a name for a non-component Bom entry! - - - validator.project.bom_entry.only_part_or_assembly_allowed - Only one part or assembly may be selected. Please modify your selection! - - project.bom_entry.name_already_in_bom @@ -395,6 +389,12 @@ This part already exists in the list! + + + assembly.bom_entry.assembly_already_in_bom + This assembly already exists as an entry! + + assembly.bom_entry.project_already_in_bom @@ -413,6 +413,12 @@ You must select a part or set a name for the entry! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Only one part or assembly may be selected. Please modify your selection! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index e603bdaf8..aff68a185 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -203,12 +203,6 @@ L'emplacement de stockage a été marqué comme "Composant seul", par conséquent aucun nouveau composant ne peut être ajouté. - - - validator.project.bom_entry.only_part_or_assembly_allowed - Seule une pièce ou un assemblage peut être sélectionné. Veuillez ajuster votre sélection! - - validator.bom_importer.invalid_import_type @@ -227,6 +221,12 @@ Cette pièce existe déjà dans le groupe! + + + assembly.bom_entry.assembly_already_in_bom + Cet assemblage existe déjà en tant qu'entrée ! + + assembly.bom_entry.project_already_in_bom @@ -245,6 +245,12 @@ Vous devez sélectionner une pièce ou attribuer un nom pour un non-élément! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Seule une pièce ou un assemblage peut être sélectionné. Veuillez ajuster votre sélection! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index c0af03354..1ee5c06fe 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -251,12 +251,6 @@ Morate odabrati dio za unos u BOM ili postaviti naziv za unos koji nije dio. - - - validator.project.bom_entry.only_part_or_assembly_allowed - Dozvoljeno je odabrati samo jednu komponentu ili sklop. Molimo prilagodite svoj odabir! - - project.bom_entry.name_already_in_bom @@ -389,6 +383,12 @@ Ovaj dio već postoji u grupi! + + + assembly.bom_entry.assembly_already_in_bom + Ova se montaža već nalazi kao zapis! + + assembly.bom_entry.project_already_in_bom @@ -407,6 +407,12 @@ Morate odabrati dio ili unijeti naziv za nedio! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Dozvoljeno je odabrati samo jednu komponentu ili sklop. Molimo prilagodite svoj odabir! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index 88369640f..ac57a2cc4 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -251,12 +251,6 @@ È necessario selezionare un componente o assegnare un nome ad una voce BOM che non indica un componente! - - - validator.project.bom_entry.only_part_or_assembly_allowed - È consentito selezionare solo una parte o un assieme. Si prega di modificare la selezione! - - project.bom_entry.name_already_in_bom @@ -389,6 +383,12 @@ Questa parte è già presente nel gruppo! + + + assembly.bom_entry.assembly_already_in_bom + Questo assemblaggio è già presente come voce! + + assembly.bom_entry.project_already_in_bom @@ -407,6 +407,12 @@ È necessario selezionare una parte o inserire un nome per un non-parte! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + È consentito selezionare solo una parte o un assieme. Si prega di modificare la selezione! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index 070281ccc..a316707ad 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -203,12 +203,6 @@ 新しい部品を追加できません。保管場所は「1つの部品のみ」とマークされています。 - - - validator.project.bom_entry.only_part_or_assembly_allowed - 部品またはアセンブリのみ選択可能です。選択内容を調整してください! - - validator.bom_importer.invalid_import_type @@ -227,6 +221,12 @@ この部品はすでにグループに存在します! + + + assembly.bom_entry.assembly_already_in_bom + このアセンブリはすでにエントリとして存在します! + + assembly.bom_entry.project_already_in_bom @@ -245,6 +245,12 @@ 部品を選択するか、非部品の名前を入力する必要があります! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + 部品またはアセンブリのみ選択可能です。選択内容を調整してください! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 60713fa02..95c44ab4d 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -251,12 +251,6 @@ Należy wybrać część dla wpisu BOM części lub ustawić nazwę dla wpisu BOM niebędącego częścią. - - - validator.project.bom_entry.only_part_or_assembly_allowed - Można wybrać tylko jedną część lub zespół. Proszę dostosować swój wybór! - - project.bom_entry.name_already_in_bom @@ -389,6 +383,12 @@ Ten element już istnieje w grupie! + + + assembly.bom_entry.assembly_already_in_bom + To zestawienie jest już dodane jako wpis! + + assembly.bom_entry.project_already_in_bom @@ -407,6 +407,12 @@ Musisz wybrać element lub przypisać nazwę dla elementu niestandardowego! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Można wybrać tylko jedną część lub zespół. Proszę dostosować swój wybór! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 32540f8c7..425ede5f1 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -251,12 +251,6 @@ Вам необходимо выбрать компонент или задать имя для BOM, не относящейся к компоненту! - - - validator.project.bom_entry.only_part_or_assembly_allowed - Можно выбрать только деталь или сборку. Пожалуйста, измените ваш выбор! - - project.bom_entry.name_already_in_bom @@ -389,6 +383,12 @@ Эта деталь уже существует в группе! + + + assembly.bom_entry.assembly_already_in_bom + Этот сборочный узел уже добавлен как запись! + + assembly.bom_entry.project_already_in_bom @@ -407,6 +407,12 @@ Необходимо выбрать деталь или ввести название для недетали! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Можно выбрать только деталь или сборку. Пожалуйста, измените ваш выбор! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 4a0ec79e8..4a02523bb 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -251,12 +251,6 @@ 您必须为 BOM 条目选择部件,或为非部件 BOM 条目设置名称。 - - - validator.project.bom_entry.only_part_or_assembly_allowed - 只能选择一个零件或组件。请修改您的选择! - - project.bom_entry.name_already_in_bom @@ -377,6 +371,12 @@ 此零件已存在于组中! + + + assembly.bom_entry.assembly_already_in_bom + 此装配已经作为条目存在! + + assembly.bom_entry.project_already_in_bom @@ -395,6 +395,12 @@ 必须选择零件或为非零件指定名称! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + 只能选择一个零件或组件。请修改您的选择! + + validator.bom_importer.json_csv.quantity.required From 4deab8ecf3e965f4dc9c9442541431fcf9263397 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 3 Jul 2025 13:43:55 +0200 Subject: [PATCH 17/83] AssemblyBomEntriesDataTable anpassen --- .../AssemblyBomEntriesDataTable.php | 82 ++++++------------- 1 file changed, 23 insertions(+), 59 deletions(-) diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php index b31c72bc0..fed6850f4 100644 --- a/src/DataTables/AssemblyBomEntriesDataTable.php +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -22,12 +22,13 @@ */ namespace App\DataTables; -use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Helpers\AssemblyDataTableHelper; use App\DataTables\Helpers\ProjectDataTableHelper; use App\DataTables\Helpers\ColumnSortHelper; use App\DataTables\Helpers\PartDataTableHelper; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; use App\Entity\AssemblySystem\AssemblyBOMEntry; @@ -45,20 +46,20 @@ class AssemblyBomEntriesDataTable implements DataTableTypeInterface { public function __construct( - protected TranslatorInterface $translator, - protected PartDataTableHelper $partDataTableHelper, - protected ProjectDataTableHelper $projectDataTableHelper, - protected EntityURLGenerator $entityURLGenerator, - protected AmountFormatter $amountFormatter, - private string $visible_columns, - private ColumnSortHelper $csh + protected TranslatorInterface $translator, + protected PartDataTableHelper $partDataTableHelper, + protected ProjectDataTableHelper $projectDataTableHelper, + protected AssemblyDataTableHelper $assemblyDataTableHelper, + protected EntityURLGenerator $entityURLGenerator, + protected AmountFormatter $amountFormatter, + private string $visible_columns, + private ColumnSortHelper $csh ){ } public function configure(DataTable $dataTable, array $options): void { $this->csh - //->add('select', SelectColumn::class) ->add('picture', TextColumn::class, [ 'label' => '', 'className' => 'no-colvis', @@ -89,7 +90,7 @@ public function configure(DataTable $dataTable, array $options): void 'label' => $this->translator->trans('part.table.name'), 'orderField' => 'NATSORT(part.name)', 'render' => function ($value, AssemblyBOMEntry $context) { - if(!$context->getPart() instanceof Part && !$context->getProject() instanceof Project) { + if(!$context->getPart() instanceof Part && !$context->getReferencedAssembly() instanceof Assembly && !$context->getProject() instanceof Project) { return htmlspecialchars((string) $context->getName()); } @@ -97,6 +98,13 @@ public function configure(DataTable $dataTable, array $options): void $tmp = $this->partDataTableHelper->renderName($context->getPart()); $tmp = $this->translator->trans('part.table.name.value.for_part', ['%value%' => $tmp]); + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
'.htmlspecialchars($context->getName()).''; + } + } elseif ($context->getReferencedAssembly() !== null) { + $tmp = $this->assemblyDataTableHelper->renderName($context->getReferencedAssembly()); + $tmp = $this->translator->trans('part.table.name.value.for_assembly', ['%value%' => $tmp]); + if($context->getName() !== null && $context->getName() !== '') { $tmp .= '
'.htmlspecialchars($context->getName()).''; } @@ -127,59 +135,15 @@ public function configure(DataTable $dataTable, array $options): void ->add('description', MarkdownColumn::class, [ 'label' => $this->translator->trans('part.table.description'), 'data' => function (AssemblyBOMEntry $context) { - if($context->getPart() instanceof Part) { + if ($context->getPart() instanceof Part) { return $context->getPart()->getDescription(); + } elseif ($context->getReferencedAssembly() instanceof Assembly) { + return $context->getReferencedAssembly()->getDescription(); } //For non-part BOM entries show the comment field return $context->getComment(); }, ]) - ->add('category', EntityColumn::class, [ - 'label' => $this->translator->trans('part.table.category'), - 'property' => 'part.category', - 'orderField' => 'NATSORT(category.name)', - ]) - ->add('footprint', EntityColumn::class, [ - 'property' => 'part.footprint', - 'label' => $this->translator->trans('part.table.footprint'), - 'orderField' => 'NATSORT(footprint.name)', - ]) - ->add('manufacturer', EntityColumn::class, [ - 'property' => 'part.manufacturer', - 'label' => $this->translator->trans('part.table.manufacturer'), - 'orderField' => 'NATSORT(manufacturer.name)', - ]) - ->add('mountnames', TextColumn::class, [ - 'label' => 'assembly.bom.mountnames', - 'render' => function ($value, AssemblyBOMEntry $context) { - $html = ''; - - foreach (explode(',', $context->getMountnames()) as $mountname) { - $html .= sprintf('%s ', htmlspecialchars($mountname)); - } - return $html; - }, - ]) - ->add('instockAmount', TextColumn::class, [ - 'label' => 'assembly.bom.instockAmount', - 'render' => function ($value, AssemblyBOMEntry $context) { - if ($context->getPart() !== null) { - return $this->partDataTableHelper->renderAmount($context->getPart()); - } - - return ''; - } - ]) - ->add('storageLocations', TextColumn::class, [ - 'label' => 'part.table.storeLocations', - 'render' => function ($value, AssemblyBOMEntry $context) { - if ($context->getPart() !== null) { - return $this->partDataTableHelper->renderStorageLocations($context->getPart()); - } - - return ''; - } - ]) ->add('addedDate', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.addedDate'), ]) @@ -189,8 +153,7 @@ public function configure(DataTable $dataTable, array $options): void ; //Apply the user configured order and visibility and add the columns to the table - $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->visible_columns, - "TABLE_ASSEMBLIES_DEFAULT_COLUMNS"); + $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->visible_columns,"TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS"); $dataTable->addOrderBy('name'); @@ -214,6 +177,7 @@ private function getQuery(QueryBuilder $builder, array $options): void ->addSelect('part') ->from(AssemblyBOMEntry::class, 'bom_entry') ->leftJoin('bom_entry.part', 'part') + ->leftJoin('bom_entry.referencedAssembly', 'referencedAssembly') ->leftJoin('part.category', 'category') ->leftJoin('part.footprint', 'footprint') ->leftJoin('part.manufacturer', 'manufacturer') From b7cb51d48c08b09672c6db4bd16bd90df32ad8b7 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 3 Jul 2025 13:47:20 +0200 Subject: [PATCH 18/83] =?UTF-8?q?Assembly=20Listen=C3=BCbersicht=20umsetze?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/app/images.css | 5 + docs/configuration.md | 3 +- src/Controller/AssemblyController.php | 74 ++++++ src/DataTables/AssemblyDataTable.php | 249 ++++++++++++++++++ src/DataTables/Filters/AssemblyFilter.php | 68 +++++ .../Filters/AssemblySearchFilter.php | 183 +++++++++++++ .../Helpers/AssemblyDataTableHelper.php | 77 ++++++ .../Helpers/ProjectDataTableHelper.php | 2 +- src/Form/Filters/AssemblyFilterType.php | 114 ++++++++ .../assemblies/lists/_action_bar.html.twig | 6 + templates/assemblies/lists/_filter.html.twig | 62 +++++ templates/assemblies/lists/all_list.html.twig | 30 +++ templates/assemblies/lists/data.html.twig | 3 + translations/messages.cs.xlf | 126 +++++++++ translations/messages.da.xlf | 126 +++++++++ translations/messages.de.xlf | 126 +++++++++ translations/messages.el.xlf | 126 +++++++++ translations/messages.en.xlf | 126 +++++++++ translations/messages.es.xlf | 126 +++++++++ translations/messages.fr.xlf | 126 +++++++++ translations/messages.it.xlf | 126 +++++++++ translations/messages.ja.xlf | 126 +++++++++ translations/messages.nl.xlf | 126 +++++++++ translations/messages.pl.xlf | 126 +++++++++ translations/messages.ru.xlf | 126 +++++++++ translations/messages.zh.xlf | 126 +++++++++ 26 files changed, 2511 insertions(+), 3 deletions(-) create mode 100644 src/DataTables/AssemblyDataTable.php create mode 100644 src/DataTables/Filters/AssemblyFilter.php create mode 100644 src/DataTables/Filters/AssemblySearchFilter.php create mode 100644 src/DataTables/Helpers/AssemblyDataTableHelper.php create mode 100644 src/Form/Filters/AssemblyFilterType.php create mode 100644 templates/assemblies/lists/_action_bar.html.twig create mode 100644 templates/assemblies/lists/_filter.html.twig create mode 100644 templates/assemblies/lists/all_list.html.twig create mode 100644 templates/assemblies/lists/data.html.twig diff --git a/assets/css/app/images.css b/assets/css/app/images.css index 0212a85b7..132cab99b 100644 --- a/assets/css/app/images.css +++ b/assets/css/app/images.css @@ -61,3 +61,8 @@ .object-fit-cover { object-fit: cover; } + +.assembly-table-image { + max-height: 40px; + object-fit: contain; +} diff --git a/docs/configuration.md b/docs/configuration.md index 498308b00..242164bf5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -137,8 +137,7 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`. -* `TABLE_ASSEMBLIES_DEFAULT_COLUMNS`: The columns in assemblies tables, which are visible by default (when loading table for first - time). +* `TABLE_ASSEMBLIES_DEFAULT_COLUMNS`: The columns in assemblies tables, which are visible by default (when loading table for first time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns are: `name`, `id`, `ipn`, `description`, `referencedAssemblies`, `edit`, `addedDate`, `lastModified`. * `TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS`: The columns in assemblies bom tables, which are visible by default (when loading table for first time). diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php index 9106f6775..a1ba7fa65 100644 --- a/src/Controller/AssemblyController.php +++ b/src/Controller/AssemblyController.php @@ -23,15 +23,22 @@ namespace App\Controller; use App\DataTables\AssemblyBomEntriesDataTable; +use App\DataTables\AssemblyDataTable; +use App\DataTables\ErrorDataTable; +use App\DataTables\Filters\AssemblyFilter; use App\Entity\AssemblySystem\Assembly; use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Parts\Part; +use App\Exceptions\InvalidRegexException; use App\Form\AssemblySystem\AssemblyAddPartsType; use App\Form\AssemblySystem\AssemblyBuildType; +use App\Form\Filters\AssemblyFilterType; use App\Helpers\Assemblies\AssemblyBuildRequest; use App\Services\ImportExportSystem\BOMImporter; use App\Services\AssemblySystem\AssemblyBuildHelper; +use App\Services\Trees\NodesListBuilder; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\DBAL\Exception\DriverException; use Doctrine\ORM\EntityManagerInterface; use League\Csv\SyntaxError; use Omines\DataTablesBundle\DataTableFactory; @@ -54,9 +61,76 @@ class AssemblyController extends AbstractController public function __construct( private readonly DataTableFactory $dataTableFactory, private readonly TranslatorInterface $translator, + private readonly NodesListBuilder $nodesListBuilder ) { } + #[Route(path: '/list', name: 'assemblies_list')] + public function showAll(Request $request): Response + { + return $this->showListWithFilter($request,'assemblies/lists/all_list.html.twig'); + } + + /** + * Common implementation for the part list pages. + * @param Request $request The request to parse + * @param string $template The template that should be rendered + * @param callable|null $filter_changer A function that is called with the filter object as parameter. This function can be used to customize the filter + * @param callable|null $form_changer A function that is called with the form object as parameter. This function can be used to customize the form + * @param array $additonal_template_vars Any additional template variables that should be passed to the template + * @param array $additional_table_vars Any additional variables that should be passed to the table creation + */ + protected function showListWithFilter(Request $request, string $template, ?callable $filter_changer = null, ?callable $form_changer = null, array $additonal_template_vars = [], array $additional_table_vars = []): Response + { + $this->denyAccessUnlessGranted('@assemblies.read'); + + $formRequest = clone $request; + $formRequest->setMethod('GET'); + $filter = new AssemblyFilter($this->nodesListBuilder); + if($filter_changer !== null){ + $filter_changer($filter); + } + + $filterForm = $this->createForm(AssemblyFilterType::class, $filter, ['method' => 'GET']); + if($form_changer !== null) { + $form_changer($filterForm); + } + + $filterForm->handleRequest($formRequest); + + $table = $this->dataTableFactory->createFromType( + AssemblyDataTable::class, + array_merge(['filter' => $filter], $additional_table_vars), + ['lengthMenu' => AssemblyDataTable::LENGTH_MENU] + ) + ->handleRequest($request); + + if ($table->isCallback()) { + try { + try { + return $table->getResponse(); + } catch (DriverException $driverException) { + if ($driverException->getCode() === 1139) { + //Convert the driver exception to InvalidRegexException so it has the same handler as for SQLite + throw InvalidRegexException::fromDriverException($driverException); + } else { + throw $driverException; + } + } + } catch (InvalidRegexException $exception) { + $errors = $this->translator->trans('assembly.table.invalid_regex').': '.$exception->getReason(); + $request->request->set('order', []); + + return ErrorDataTable::errorTable($this->dataTableFactory, $request, $errors); + } + } + + return $this->render($template, array_merge([ + 'datatable' => $table, + 'filterForm' => $filterForm->createView(), + ], $additonal_template_vars)); + } + #[Route(path: '/{id}/info', name: 'assembly_info', requirements: ['id' => '\d+'])] public function info(Assembly $assembly, Request $request, AssemblyBuildHelper $buildHelper): Response { diff --git a/src/DataTables/AssemblyDataTable.php b/src/DataTables/AssemblyDataTable.php new file mode 100644 index 000000000..f3854ebca --- /dev/null +++ b/src/DataTables/AssemblyDataTable.php @@ -0,0 +1,249 @@ +. + */ + +declare(strict_types=1); + +namespace App\DataTables; + +use App\DataTables\Adapters\TwoStepORMAdapter; +use App\DataTables\Column\IconLinkColumn; +use App\DataTables\Column\LocaleDateTimeColumn; +use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Column\SelectColumn; +use App\DataTables\Filters\AssemblyFilter; +use App\DataTables\Filters\AssemblySearchFilter; +use App\DataTables\Helpers\AssemblyDataTableHelper; +use App\DataTables\Helpers\ColumnSortHelper; +use App\Doctrine\Helpers\FieldHelper; +use App\Entity\AssemblySystem\Assembly; +use App\Services\EntityURLGenerator; +use Doctrine\ORM\AbstractQuery; +use Doctrine\ORM\QueryBuilder; +use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; +use Omines\DataTablesBundle\Column\TextColumn; +use Omines\DataTablesBundle\DataTable; +use Omines\DataTablesBundle\DataTableTypeInterface; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; + +final class AssemblyDataTable implements DataTableTypeInterface +{ + const LENGTH_MENU = [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]; + + public function __construct( + private readonly EntityURLGenerator $urlGenerator, + private readonly TranslatorInterface $translator, + private readonly AssemblyDataTableHelper $assemblyDataTableHelper, + private readonly Security $security, + private readonly string $visible_columns, + private readonly ColumnSortHelper $csh, + ) { + } + + public function configureOptions(OptionsResolver $optionsResolver): void + { + $optionsResolver->setDefaults([ + 'filter' => null, + 'search' => null + ]); + + $optionsResolver->setAllowedTypes('filter', [AssemblyFilter::class, 'null']); + $optionsResolver->setAllowedTypes('search', [AssemblySearchFilter::class, 'null']); + } + + public function configure(DataTable $dataTable, array $options): void + { + $resolver = new OptionsResolver(); + $this->configureOptions($resolver); + $options = $resolver->resolve($options); + + $this->csh + ->add('select', SelectColumn::class, visibility_configurable: false) + ->add('picture', TextColumn::class, [ + 'label' => '', + 'className' => 'no-colvis', + 'render' => fn($value, Assembly $context) => $this->assemblyDataTableHelper->renderPicture($context), + ], visibility_configurable: false) + ->add('name', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.table.name'), + 'render' => fn($value, Assembly $context) => $this->assemblyDataTableHelper->renderName($context), + 'orderField' => 'NATSORT(assembly.name)' + ]) + ->add('id', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.table.id'), + ]) + ->add('ipn', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.table.ipn'), + 'orderField' => 'NATSORT(assembly.ipn)' + ]) + ->add('description', MarkdownColumn::class, [ + 'label' => $this->translator->trans('assembly.table.description'), + ]) + ->add('addedDate', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('assembly.table.addedDate'), + ]) + ->add('lastModified', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('assembly.table.lastModified'), + ]); + + //Add a assembly column to list where the assembly is used as referenced assembly as bom-entry, when the user has the permission to see the assemblies + if ($this->security->isGranted('read', Assembly::class)) { + $this->csh->add('referencedAssemblies', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.referencedAssembly.labelp'), + 'render' => function ($value, Assembly $context): string { + $assemblies = $context->getReferencedAssemblies(); + + $max = 5; + $tmp = ""; + + for ($i = 0; $i < min($max, count($assemblies)); $i++) { + $url = $this->urlGenerator->infoURL($assemblies[$i]); + $tmp .= sprintf('%s', $url, htmlspecialchars($assemblies[$i]->getName())); + if ($i < count($assemblies) - 1) { + $tmp .= ", "; + } + } + + if (count($assemblies) > $max) { + $tmp .= ", + ".(count($assemblies) - $max); + } + + return $tmp; + } + ]); + } + + $this->csh + ->add('edit', IconLinkColumn::class, [ + 'label' => $this->translator->trans('assembly.table.edit'), + 'href' => fn($value, Assembly $context) => $this->urlGenerator->editURL($context), + 'disabled' => fn($value, Assembly $context) => !$this->security->isGranted('edit', $context), + 'title' => $this->translator->trans('assembly.table.edit.title'), + ]); + + //Apply the user configured order and visibility and add the columns to the table + $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->visible_columns, "TABLE_ASSEMBLIES_DEFAULT_COLUMNS"); + + $dataTable->addOrderBy('name') + ->createAdapter(TwoStepORMAdapter::class, [ + 'filter_query' => $this->getFilterQuery(...), + 'detail_query' => $this->getDetailQuery(...), + 'entity' => Assembly::class, + 'hydrate' => AbstractQuery::HYDRATE_OBJECT, + //Use the simple total query, as we just want to get the total number of assemblies without any conditions + //For this the normal query would be pretty slow + 'simple_total_query' => true, + 'criteria' => [ + function (QueryBuilder $builder) use ($options): void { + $this->buildCriteria($builder, $options); + }, + new SearchCriteriaProvider(), + ], + 'query_modifier' => $this->addJoins(...), + ]); + } + + + private function getFilterQuery(QueryBuilder $builder): void + { + /* In the filter query we only select the IDs. The fetching of the full entities is done in the detail query. + * We only need to join the entities here, so we can filter by them. + * The filter conditions are added to this QB in the buildCriteria method. + * + * The amountSum field and the joins are dynamically added by the addJoins method, if the fields are used in the query. + * This improves the performance, as we do not need to join all tables, if we do not need them. + */ + $builder + ->select('assembly.id') + ->from(Assembly::class, 'assembly') + + //The other group by fields, are dynamically added by the addJoins method + ->addGroupBy('assembly'); + } + + private function getDetailQuery(QueryBuilder $builder, array $filter_results): void + { + $ids = array_map(static fn($row) => $row['id'], $filter_results); + + /* + * In this query we take the IDs which were filtered, paginated and sorted in the filter query, and fetch the + * full entities. + * We can do complex fetch joins, as we do not need to filter or sort here (which would kill the performance). + * The only condition should be for the IDs. + * It is important that elements are ordered the same way, as the IDs are passed, or ordering will be wrong. + * + * We do not require the subqueries like amountSum here, as it is not used to render the table (and only for sorting) + */ + $builder + ->select('assembly') + ->addSelect('master_picture_attachment') + ->addSelect('attachments') + ->from(Assembly::class, 'assembly') + ->leftJoin('assembly.master_picture_attachment', 'master_picture_attachment') + ->leftJoin('assembly.attachments', 'attachments') + ->where('assembly.id IN (:ids)') + ->setParameter('ids', $ids) + ->addGroupBy('assembly') + ->addGroupBy('master_picture_attachment') + ->addGroupBy('attachments'); + + //Get the results in the same order as the IDs were passed + FieldHelper::addOrderByFieldParam($builder, 'assembly.id', 'ids'); + } + + /** + * This function is called right before the filter query is executed. + * We use it to dynamically add joins to the query, if the fields are used in the query. + * @param QueryBuilder $builder + * @return QueryBuilder + */ + private function addJoins(QueryBuilder $builder): QueryBuilder + { + //Check if the query contains certain conditions, for which we need to add additional joins + //The join fields get prefixed with an underscore, so we can check if they are used in the query easy without confusing them for a assembly subfield + $dql = $builder->getDQL(); + + if (str_contains($dql, '_master_picture_attachment')) { + $builder->leftJoin('assembly.master_picture_attachment', '_master_picture_attachment'); + $builder->addGroupBy('_master_picture_attachment'); + } + if (str_contains($dql, '_attachments')) { + $builder->leftJoin('assembly.attachments', '_attachments'); + } + + return $builder; + } + + private function buildCriteria(QueryBuilder $builder, array $options): void + { + //Apply the search criterias first + if ($options['search'] instanceof AssemblySearchFilter) { + $search = $options['search']; + $search->apply($builder); + } + + //We do the most stuff here in the filter class + if ($options['filter'] instanceof AssemblyFilter) { + $filter = $options['filter']; + $filter->apply($builder); + } + } +} diff --git a/src/DataTables/Filters/AssemblyFilter.php b/src/DataTables/Filters/AssemblyFilter.php new file mode 100644 index 000000000..d8d07a1ec --- /dev/null +++ b/src/DataTables/Filters/AssemblyFilter.php @@ -0,0 +1,68 @@ +. + */ +namespace App\DataTables\Filters; + +use App\DataTables\Filters\Constraints\DateTimeConstraint; +use App\DataTables\Filters\Constraints\EntityConstraint; +use App\DataTables\Filters\Constraints\IntConstraint; +use App\DataTables\Filters\Constraints\TextConstraint; +use App\Entity\Attachments\AttachmentType; +use App\Services\Trees\NodesListBuilder; +use Doctrine\ORM\QueryBuilder; + +class AssemblyFilter implements FilterInterface +{ + + use CompoundFilterTrait; + + public readonly IntConstraint $dbId; + public readonly TextConstraint $ipn; + public readonly TextConstraint $name; + public readonly TextConstraint $description; + public readonly TextConstraint $comment; + public readonly DateTimeConstraint $lastModified; + public readonly DateTimeConstraint $addedDate; + + public readonly IntConstraint $attachmentsCount; + public readonly EntityConstraint $attachmentType; + public readonly TextConstraint $attachmentName; + + public function __construct(NodesListBuilder $nodesListBuilder) + { + $this->name = new TextConstraint('assembly.name'); + $this->description = new TextConstraint('assembly.description'); + $this->comment = new TextConstraint('assembly.comment'); + $this->dbId = new IntConstraint('assembly.id'); + $this->ipn = new TextConstraint('assembly.ipn'); + $this->addedDate = new DateTimeConstraint('assembly.addedDate'); + $this->lastModified = new DateTimeConstraint('assembly.lastModified'); + $this->attachmentsCount = new IntConstraint('COUNT(_attachments)'); + $this->attachmentType = new EntityConstraint($nodesListBuilder, AttachmentType::class, '_attachments.attachment_type'); + $this->attachmentName = new TextConstraint('_attachments.name'); + } + + public function apply(QueryBuilder $queryBuilder): void + { + $this->applyAllChildFilters($queryBuilder); + } +} diff --git a/src/DataTables/Filters/AssemblySearchFilter.php b/src/DataTables/Filters/AssemblySearchFilter.php new file mode 100644 index 000000000..1627cc612 --- /dev/null +++ b/src/DataTables/Filters/AssemblySearchFilter.php @@ -0,0 +1,183 @@ +. + */ +namespace App\DataTables\Filters; +use Doctrine\ORM\QueryBuilder; + +class AssemblySearchFilter implements FilterInterface +{ + + /** @var boolean Whether to use regex for searching */ + protected bool $regex = false; + + /** @var bool Use name field for searching */ + protected bool $name = true; + + /** @var bool Use description for searching */ + protected bool $description = true; + + /** @var bool Use comment field for searching */ + protected bool $comment = true; + + /** @var bool Use ordernr for searching */ + protected bool $ordernr = true; + + /** @var bool Use Internal part number for searching */ + protected bool $ipn = true; + + public function __construct( + /** @var string The string to query for */ + protected string $keyword + ) + { + } + + protected function getFieldsToSearch(): array + { + $fields_to_search = []; + + if($this->name) { + $fields_to_search[] = 'assembly.name'; + } + if($this->description) { + $fields_to_search[] = 'assembly.description'; + } + if ($this->comment) { + $fields_to_search[] = 'assembly.comment'; + } + if ($this->ipn) { + $fields_to_search[] = 'assembly.ipn'; + } + + return $fields_to_search; + } + + public function apply(QueryBuilder $queryBuilder): void + { + $fields_to_search = $this->getFieldsToSearch(); + + //If we have nothing to search for, do nothing + if ($fields_to_search === [] || $this->keyword === '') { + return; + } + + //Convert the fields to search to a list of expressions + $expressions = array_map(function (string $field): string { + if ($this->regex) { + return sprintf("REGEXP(%s, :search_query) = TRUE", $field); + } + + return sprintf("ILIKE(%s, :search_query) = TRUE", $field); + }, $fields_to_search); + + //Add Or concatenation of the expressions to our query + $queryBuilder->andWhere( + $queryBuilder->expr()->orX(...$expressions) + ); + + //For regex, we pass the query as is, for like we add % to the start and end as wildcards + if ($this->regex) { + $queryBuilder->setParameter('search_query', $this->keyword); + } else { + $queryBuilder->setParameter('search_query', '%' . $this->keyword . '%'); + } + } + + public function getKeyword(): string + { + return $this->keyword; + } + + public function setKeyword(string $keyword): AssemblySearchFilter + { + $this->keyword = $keyword; + return $this; + } + + public function isRegex(): bool + { + return $this->regex; + } + + public function setRegex(bool $regex): AssemblySearchFilter + { + $this->regex = $regex; + return $this; + } + + public function isName(): bool + { + return $this->name; + } + + public function setName(bool $name): AssemblySearchFilter + { + $this->name = $name; + return $this; + } + + public function isCategory(): bool + { + return $this->category; + } + + public function setCategory(bool $category): AssemblySearchFilter + { + $this->category = $category; + return $this; + } + + public function isDescription(): bool + { + return $this->description; + } + + public function setDescription(bool $description): AssemblySearchFilter + { + $this->description = $description; + return $this; + } + + public function isIPN(): bool + { + return $this->ipn; + } + + public function setIPN(bool $ipn): AssemblySearchFilter + { + $this->ipn = $ipn; + return $this; + } + + public function isComment(): bool + { + return $this->comment; + } + + public function setComment(bool $comment): AssemblySearchFilter + { + $this->comment = $comment; + return $this; + } + + +} diff --git a/src/DataTables/Helpers/AssemblyDataTableHelper.php b/src/DataTables/Helpers/AssemblyDataTableHelper.php new file mode 100644 index 000000000..dda563ea4 --- /dev/null +++ b/src/DataTables/Helpers/AssemblyDataTableHelper.php @@ -0,0 +1,77 @@ +. + */ + +namespace App\DataTables\Helpers; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\Attachment; +use App\Services\Attachments\AssemblyPreviewGenerator; +use App\Services\Attachments\AttachmentURLGenerator; +use App\Services\EntityURLGenerator; + +/** + * A helper service which contains common code to render columns for assembly related tables + */ +class AssemblyDataTableHelper +{ + public function __construct( + private readonly EntityURLGenerator $entityURLGenerator, + private readonly AssemblyPreviewGenerator $previewGenerator, + private readonly AttachmentURLGenerator $attachmentURLGenerator + ) { + } + + public function renderName(Assembly $context): string + { + $icon = ''; + + return sprintf( + '%s%s', + $this->entityURLGenerator->infoURL($context), + $icon, + htmlspecialchars($context->getName()) + ); + } + + public function renderPicture(Assembly $context): string + { + $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($context); + if (!$preview_attachment instanceof Attachment) { + return ''; + } + + $title = htmlspecialchars($preview_attachment->getName()); + if ($preview_attachment->getFilename()) { + $title .= ' ('.htmlspecialchars($preview_attachment->getFilename()).')'; + } + + return sprintf( + '%s', + 'Assembly image', + $this->attachmentURLGenerator->getThumbnailURL($preview_attachment), + $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_md'), + 'hoverpic assembly-table-image', + $title + ); + } +} diff --git a/src/DataTables/Helpers/ProjectDataTableHelper.php b/src/DataTables/Helpers/ProjectDataTableHelper.php index 0118d5d56..baa0e24e1 100644 --- a/src/DataTables/Helpers/ProjectDataTableHelper.php +++ b/src/DataTables/Helpers/ProjectDataTableHelper.php @@ -27,7 +27,7 @@ use App\Services\EntityURLGenerator; /** - * A helper service which contains common code to render columns for assembly related tables + * A helper service which contains common code to render columns for project related tables */ class ProjectDataTableHelper { diff --git a/src/Form/Filters/AssemblyFilterType.php b/src/Form/Filters/AssemblyFilterType.php new file mode 100644 index 000000000..acfbb1a8e --- /dev/null +++ b/src/Form/Filters/AssemblyFilterType.php @@ -0,0 +1,114 @@ +. + */ +namespace App\Form\Filters; + +use App\DataTables\Filters\AssemblyFilter; +use App\Entity\Attachments\AttachmentType; +use App\Form\Filters\Constraints\DateTimeConstraintType; +use App\Form\Filters\Constraints\NumberConstraintType; +use App\Form\Filters\Constraints\StructuralEntityConstraintType; +use App\Form\Filters\Constraints\TextConstraintType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ResetType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class AssemblyFilterType extends AbstractType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => AssemblyFilter::class, + 'csrf_protection' => false, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + /* + * Common tab + */ + + $builder->add('name', TextConstraintType::class, [ + 'label' => 'assembly.filter.name', + ]); + + $builder->add('description', TextConstraintType::class, [ + 'label' => 'assembly.filter.description', + ]); + + $builder->add('comment', TextConstraintType::class, [ + 'label' => 'assembly.filter.comment' + ]); + + /* + * Advanced tab + */ + + $builder->add('dbId', NumberConstraintType::class, [ + 'label' => 'assembly.filter.dbId', + 'min' => 1, + 'step' => 1, + ]); + + $builder->add('ipn', TextConstraintType::class, [ + 'label' => 'assembly.filter.ipn', + ]); + + $builder->add('lastModified', DateTimeConstraintType::class, [ + 'label' => 'lastModified' + ]); + + $builder->add('addedDate', DateTimeConstraintType::class, [ + 'label' => 'createdAt' + ]); + + /** + * Attachments count + */ + $builder->add('attachmentsCount', NumberConstraintType::class, [ + 'label' => 'assembly.filter.attachments_count', + 'step' => 1, + 'min' => 0, + ]); + + $builder->add('attachmentType', StructuralEntityConstraintType::class, [ + 'label' => 'attachment.attachment_type', + 'entity_class' => AttachmentType::class + ]); + + $builder->add('attachmentName', TextConstraintType::class, [ + 'label' => 'assembly.filter.attachmentName', + ]); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'filter.submit', + ]); + + $builder->add('discard', ResetType::class, [ + 'label' => 'filter.discard', + ]); + } +} diff --git a/templates/assemblies/lists/_action_bar.html.twig b/templates/assemblies/lists/_action_bar.html.twig new file mode 100644 index 000000000..37289812a --- /dev/null +++ b/templates/assemblies/lists/_action_bar.html.twig @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/templates/assemblies/lists/_filter.html.twig b/templates/assemblies/lists/_filter.html.twig new file mode 100644 index 000000000..11be7bc24 --- /dev/null +++ b/templates/assemblies/lists/_filter.html.twig @@ -0,0 +1,62 @@ +
+
+ +
+
+
+ + + {{ form_start(filterForm, {"attr": {"data-controller": "helpers--form-cleanup", "data-action": "helpers--form-cleanup#submit"}}) }} + +
+
+ {{ form_row(filterForm.name) }} + {{ form_row(filterForm.description) }} + {{ form_row(filterForm.comment) }} +
+ +
+ {{ form_row(filterForm.dbId) }} + {{ form_row(filterForm.ipn) }} + {{ form_row(filterForm.lastModified) }} + {{ form_row(filterForm.addedDate) }} +
+ +
+ {{ form_row(filterForm.attachmentsCount) }} + {{ form_row(filterForm.attachmentType) }} + {{ form_row(filterForm.attachmentName) }} +
+
+ + {{ form_row(filterForm.submit) }} + {{ form_row(filterForm.discard) }} + +
+
+ +
+
+ + {# Retain the query parameters of the search form if it is existing #} + {% if searchFilter is defined %} + {% for property, value in searchFilter|to_array %} + + {% endfor %} + + {% endif %} + + {{ form_end(filterForm) }} +
+
+
\ No newline at end of file diff --git a/templates/assemblies/lists/all_list.html.twig b/templates/assemblies/lists/all_list.html.twig new file mode 100644 index 000000000..70d75ad40 --- /dev/null +++ b/templates/assemblies/lists/all_list.html.twig @@ -0,0 +1,30 @@ +{% extends "base.html.twig" %} + +{% block title %} + {% trans %}assembly_list.all.title{% endtrans %} +{% endblock %} + +{% block content %} + +
+
+
+ +
+
+
+ +
+
+
+ + {% include "assemblies/lists/_filter.html.twig" %} +
+ + {% include "assemblies/lists/_action_bar.html.twig" with {'url_options': {}} %} + {% include "assemblies/lists/data.html.twig" %} + +{% endblock %} diff --git a/templates/assemblies/lists/data.html.twig b/templates/assemblies/lists/data.html.twig new file mode 100644 index 000000000..69e13e4f5 --- /dev/null +++ b/templates/assemblies/lists/data.html.twig @@ -0,0 +1,3 @@ +{% import "components/datatables.macro.html.twig" as datatables %} + +{{ datatables.partsDatatableWithForm(datatable) }} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index ce79aaced..a1ede652a 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -14655,5 +14655,131 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Minimální šířka náhledu (px)
+ + + assembly_list.all.title + Všechny sestavy + + + + + assembly.edit.tab.common + Obecné + + + + + assembly.edit.tab.advanced + Pokročilé možnosti + + + + + assembly.edit.tab.attachments + Přílohy + + + + + assembly.filter.dbId + ID databáze + + + + + assembly.filter.ipn + Interní číslo dílu (IPN) + + + + + assembly.filter.name + Název + + + + + assembly.filter.description + Popis + + + + + assembly.filter.comment + Poznámky + + + + + assembly.filter.attachments_count + Počet příloh + + + + + assembly.filter.attachmentName + Název přílohy + + + + + assemblies.create.btn + Vytvořit novou sestavu + + + + + assembly.table.id + ID + + + + + assembly.table.name + Název + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Popis + + + + + assembly.table.addedDate + Přidáno + + + + + assembly.table.lastModified + Naposledy upraveno + + + + + assembly.table.edit + Upravit + + + + + assembly.table.edit.title + Upravit sestavu + + + + + assembly.table.invalid_regex + Neplatný regulární výraz (regex) + + diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index c68acdd4b..5901e9f5a 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -13336,5 +13336,131 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
+ + + assembly_list.all.title + Alle samlinger + + + + + assembly.edit.tab.common + Generelt + + + + + assembly.edit.tab.advanced + Avancerede indstillinger + + + + + assembly.edit.tab.attachments + Vedhæftede filer + + + + + assembly.filter.dbId + Database-ID + + + + + assembly.filter.ipn + Internt delnummer (IPN) + + + + + assembly.filter.name + Navn + + + + + assembly.filter.description + Beskrivelse + + + + + assembly.filter.comment + Kommentarer + + + + + assembly.filter.attachments_count + Antal vedhæftninger + + + + + assembly.filter.attachmentName + Vedhæftningens navn + + + + + assemblies.create.btn + Opret ny samling + + + + + assembly.table.id + ID + + + + + assembly.table.name + Navn + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Beskrivelse + + + + + assembly.table.addedDate + Tilføjet + + + + + assembly.table.lastModified + Sidst ændret + + + + + assembly.table.edit + Rediger + + + + + assembly.table.edit.title + Rediger samling + + + + + assembly.table.invalid_regex + Ugyldigt regulært udtryk (regex) + + diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index bee171d41..a7ece41a9 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -14621,5 +14621,131 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Min. Vorschaubilde-Breite (px)
+ + + assembly_list.all.title + Alle Baugruppen + + + + + assembly.edit.tab.common + Allgemein + + + + + assembly.edit.tab.advanced + Erweiterte Optionen + + + + + assembly.edit.tab.attachments + Dateianhänge + + + + + assembly.filter.dbId + Datenbank ID + + + + + assembly.filter.ipn + Internal Part Number (IPN) + + + + + assembly.filter.name + Name + + + + + assembly.filter.description + Beschreibung + + + + + assembly.filter.comment + Notizen + + + + + assembly.filter.attachments_count + Anzahl der Anhänge + + + + + assembly.filter.attachmentName + Name des Anhangs + + + + + assemblies.create.btn + Neue Baugruppe anlegen + + + + + assembly.table.id + ID + + + + + assembly.table.name + Name + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Beschreibung + + + + + assembly.table.addedDate + Hinzugefügt + + + + + assembly.table.lastModified + Zuletzt bearbeitet + + + + + assembly.table.edit + Ändern + + + + + assembly.table.edit.title + Baugruppe ändern + + + + + assembly.table.invalid_regex + Ungültiger regulärer Ausdruck (regex) + + diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 97b27f53f..fd5114539 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -2344,5 +2344,131 @@
+ + + assembly_list.all.title + Όλες οι συναρμολογήσεις + + + + + assembly.edit.tab.common + Γενικά + + + + + assembly.edit.tab.advanced + Προηγμένες επιλογές + + + + + assembly.edit.tab.attachments + Συνημμένα + + + + + assembly.filter.dbId + Αναγνωριστικό βάσης δεδομένων + + + + + assembly.filter.ipn + Εσωτερικός αριθμός εξαρτήματος (IPN) + + + + + assembly.filter.name + Όνομα + + + + + assembly.filter.description + Περιγραφή + + + + + assembly.filter.comment + Σχόλια + + + + + assembly.filter.attachments_count + Αριθμός συνημμένων + + + + + assembly.filter.attachmentName + Όνομα συνημμένου + + + + + assemblies.create.btn + Δημιουργία νέας συναρμολόγησης + + + + + assembly.table.id + Αναγνωριστικό + + + + + assembly.table.name + Όνομα + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Περιγραφή + + + + + assembly.table.addedDate + Προστέθηκε + + + + + assembly.table.lastModified + Τελευταία επεξεργασία + + + + + assembly.table.edit + Επεξεργασία + + + + + assembly.table.edit.title + Επεξεργασία συναρμολόγησης + + + + + assembly.table.invalid_regex + Μη έγκυρη κανονική έκφραση (regex) + + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 4f398d6b0..c4f7c60f9 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -14622,5 +14622,131 @@ Please note, that you can not impersonate a disabled user. If you try you will g Preview image min width (px)
+ + + assembly_list.all.title + All assemblies + + + + + assembly.edit.tab.common + General + + + + + assembly.edit.tab.advanced + Advanced options + + + + + assembly.edit.tab.attachments + Attachments + + + + + assembly.filter.dbId + Database ID + + + + + assembly.filter.ipn + Internal Part Number (IPN) + + + + + assembly.filter.name + Name + + + + + assembly.filter.description + Description + + + + + assembly.filter.comment + Comments + + + + + assembly.filter.attachments_count + Number of attachments + + + + + assembly.filter.attachmentName + Attachment name + + + + + assemblies.create.btn + Create new assembly + + + + + assembly.table.id + ID + + + + + assembly.table.name + Name + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Description + + + + + assembly.table.addedDate + Added + + + + + assembly.table.lastModified + Last modified + + + + + assembly.table.edit + Edit + + + + + assembly.table.edit.title + Edit assembly + + + + + assembly.table.invalid_regex + Invalid regular expression (regex) + + diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index b67198a78..461e7a7b1 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -13508,5 +13508,131 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Este componente contiene más de un stock. Cambie la ubicación manualmente para seleccionar el stock deseado.
+ + + assembly_list.all.title + Todas las ensamblajes + + + + + assembly.edit.tab.common + General + + + + + assembly.edit.tab.advanced + Opciones avanzadas + + + + + assembly.edit.tab.attachments + Archivos adjuntos + + + + + assembly.filter.dbId + ID de la base de datos + + + + + assembly.filter.ipn + Número interno de pieza (IPN) + + + + + assembly.filter.name + Nombre + + + + + assembly.filter.description + Descripción + + + + + assembly.filter.comment + Comentarios + + + + + assembly.filter.attachments_count + Cantidad de adjuntos + + + + + assembly.filter.attachmentName + Nombre del adjunto + + + + + assemblies.create.btn + Crear una nueva ensamblaje + + + + + assembly.table.id + ID + + + + + assembly.table.name + Nombre + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Descripción + + + + + assembly.table.addedDate + Añadido + + + + + assembly.table.lastModified + Última modificación + + + + + assembly.table.edit + Editar + + + + + assembly.table.edit.title + Editar ensamblaje + + + + + assembly.table.invalid_regex + Expresión regular no válida (regex) + + diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index ecfcf5928..fe470e449 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -9918,5 +9918,131 @@ exemple de ville
+ + + assembly_list.all.title + Toutes les assemblages + + + + + assembly.edit.tab.common + Général + + + + + assembly.edit.tab.advanced + Options avancées + + + + + assembly.edit.tab.attachments + Pièces jointes + + + + + assembly.filter.dbId + ID de la base de données + + + + + assembly.filter.ipn + Numéro de pièce interne (IPN) + + + + + assembly.filter.name + Nom + + + + + assembly.filter.description + Description + + + + + assembly.filter.comment + Commentaires + + + + + assembly.filter.attachments_count + Nombre de pièces jointes + + + + + assembly.filter.attachmentName + Nom de la pièce jointe + + + + + assemblies.create.btn + Créer un nouvel assemblage + + + + + assembly.table.id + ID + + + + + assembly.table.name + Nom + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Description + + + + + assembly.table.addedDate + Ajouté + + + + + assembly.table.lastModified + Dernière modification + + + + + assembly.table.edit + Modifier + + + + + assembly.table.edit.title + Modifier l'assemblage + + + + + assembly.table.invalid_regex + Expression régulière invalide (regex) + + diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 12e20ca2c..c727eef82 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -13510,5 +13510,131 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Questo componente contiene più di uno stock. Cambia manualmente la posizione per selezionare quale stock scegliere.
+ + + assembly_list.all.title + Tutti gli assiemi + + + + + assembly.edit.tab.common + Generale + + + + + assembly.edit.tab.advanced + Opzioni avanzate + + + + + assembly.edit.tab.attachments + Allegati + + + + + assembly.filter.dbId + ID del database + + + + + assembly.filter.ipn + Numero interno di parte (IPN) + + + + + assembly.filter.name + Nome + + + + + assembly.filter.description + Descrizione + + + + + assembly.filter.comment + Commenti + + + + + assembly.filter.attachments_count + Numero di allegati + + + + + assembly.filter.attachmentName + Nome dell'allegato + + + + + assemblies.create.btn + Crea un nuovo assieme + + + + + assembly.table.id + ID + + + + + assembly.table.name + Nome + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Descrizione + + + + + assembly.table.addedDate + Aggiunto + + + + + assembly.table.lastModified + Ultima modifica + + + + + assembly.table.edit + Modifica + + + + + assembly.table.edit.title + Modifica l'assieme + + + + + assembly.table.invalid_regex + Espressione regolare non valida (regex) + + diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index b7be7490d..157b1cf26 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -9631,5 +9631,131 @@ Exampletown
+ + + assembly_list.all.title + すべてのアセンブリ + + + + + assembly.edit.tab.common + 一般 + + + + + assembly.edit.tab.advanced + 詳細オプション + + + + + assembly.edit.tab.attachments + 添付ファイル + + + + + assembly.filter.dbId + データベースID + + + + + assembly.filter.ipn + 内部部品番号(IPN) + + + + + assembly.filter.name + 名前 + + + + + assembly.filter.description + 説明 + + + + + assembly.filter.comment + コメント + + + + + assembly.filter.attachments_count + 添付ファイルの数 + + + + + assembly.filter.attachmentName + 添付ファイル名 + + + + + assemblies.create.btn + 新しいアセンブリを作成 + + + + + assembly.table.id + ID + + + + + assembly.table.name + 名前 + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + 説明 + + + + + assembly.table.addedDate + 追加日 + + + + + assembly.table.lastModified + 最終変更 + + + + + assembly.table.edit + 編集 + + + + + assembly.table.edit.title + アセンブリを編集 + + + + + assembly.table.invalid_regex + 無効な正規表現(regex) + + diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index fe17c3aca..44a48dcb3 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1569,5 +1569,131 @@
+ + + assembly_list.all.title + Alle assemblages + + + + + assembly.edit.tab.common + Algemeen + + + + + assembly.edit.tab.advanced + Geavanceerde opties + + + + + assembly.edit.tab.attachments + Bijlagen + + + + + assembly.filter.dbId + Database-ID + + + + + assembly.filter.ipn + Intern partnummer (IPN) + + + + + assembly.filter.name + Naam + + + + + assembly.filter.description + Beschrijving + + + + + assembly.filter.comment + Opmerkingen + + + + + assembly.filter.attachments_count + Aantal bijlagen + + + + + assembly.filter.attachmentName + Naam van de bijlage + + + + + assemblies.create.btn + Nieuwe assemblage aanmaken + + + + + assembly.table.id + ID + + + + + assembly.table.name + Naam + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Beschrijving + + + + + assembly.table.addedDate + Toegevoegd + + + + + assembly.table.lastModified + Laatst gewijzigd + + + + + assembly.table.edit + Bewerken + + + + + assembly.table.edit.title + Assemblage bewerken + + + + + assembly.table.invalid_regex + Ongeldige reguliere expressie (regex) + + diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 73b6e4fcc..171c585d6 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -13363,5 +13363,131 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli
+ + + assembly_list.all.title + Wszystkie zespoły + + + + + assembly.edit.tab.common + Ogólne + + + + + assembly.edit.tab.advanced + Zaawansowane + + + + + assembly.edit.tab.attachments + Załączniki + + + + + assembly.filter.dbId + ID bazy danych + + + + + assembly.filter.ipn + Wewnętrzny numer części (IPN) + + + + + assembly.filter.name + Nazwa + + + + + assembly.filter.description + Opis + + + + + assembly.filter.comment + Komentarze + + + + + assembly.filter.attachments_count + Liczba załączników + + + + + assembly.filter.attachmentName + Nazwa załącznika + + + + + assemblies.create.btn + Utwórz nowy zespół + + + + + assembly.table.id + ID + + + + + assembly.table.name + Nazwa + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Opis + + + + + assembly.table.addedDate + Dodano + + + + + assembly.table.lastModified + Ostatnia modyfikacja + + + + + assembly.table.edit + Edytuj + + + + + assembly.table.edit.title + Edytuj zespół + + + + + assembly.table.invalid_regex + Nieprawidłowe wyrażenie regularne (regex) + + diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 751ff35f1..7bd0f3bc4 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -13463,5 +13463,131 @@
+ + + assembly_list.all.title + Все сборки + + + + + assembly.edit.tab.common + Общие + + + + + assembly.edit.tab.advanced + Дополнительные параметры + + + + + assembly.edit.tab.attachments + Вложения + + + + + assembly.filter.dbId + ID базы данных + + + + + assembly.filter.ipn + Внутренний номер детали (IPN) + + + + + assembly.filter.name + Название + + + + + assembly.filter.description + Описание + + + + + assembly.filter.comment + Комментарии + + + + + assembly.filter.attachments_count + Количество вложений + + + + + assembly.filter.attachmentName + Имя вложения + + + + + assemblies.create.btn + Создать новую сборку + + + + + assembly.table.id + ID + + + + + assembly.table.name + Название + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Описание + + + + + assembly.table.addedDate + Добавлено + + + + + assembly.table.lastModified + Последнее изменение + + + + + assembly.table.edit + Редактировать + + + + + assembly.table.edit.title + Редактировать сборку + + + + + assembly.table.invalid_regex + Неверное регулярное выражение (regex) + + diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 396feae3e..9e8d8aa1f 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -13348,5 +13348,131 @@ Element 3
+ + + assembly_list.all.title + 所有组件 + + + + + assembly.edit.tab.common + 通用 + + + + + assembly.edit.tab.advanced + 高级选项 + + + + + assembly.edit.tab.attachments + 附件 + + + + + assembly.filter.dbId + 数据库ID + + + + + assembly.filter.ipn + 内部零件编号(IPN) + + + + + assembly.filter.name + 名称 + + + + + assembly.filter.description + 描述 + + + + + assembly.filter.comment + 评论 + + + + + assembly.filter.attachments_count + 附件数量 + + + + + assembly.filter.attachmentName + 附件名称 + + + + + assemblies.create.btn + 创建新组件 + + + + + assembly.table.id + ID + + + + + assembly.table.name + 名称 + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + 描述 + + + + + assembly.table.addedDate + 添加日期 + + + + + assembly.table.lastModified + 最后修改 + + + + + assembly.table.edit + 编辑 + + + + + assembly.table.edit.title + 编辑组件 + + + + + assembly.table.invalid_regex + 无效的正则表达式(regex) + + From 6dd91521a0d6b1bfd4cf82074b36b5ae801bc840 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 3 Jul 2025 13:50:33 +0200 Subject: [PATCH 19/83] =?UTF-8?q?Assembly=20Konstanten=20in=20.env=20einf?= =?UTF-8?q?=C3=BCgen=20bzw.=20anpassen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.env b/.env index 201633989..c530f312c 100644 --- a/.env +++ b/.env @@ -35,6 +35,9 @@ DATABASE_EMULATE_NATURAL_SORT=0 # This must end with a slash! DEFAULT_URI="https://partdb.changeme.invalid/" +# Use an %%ipn%% placeholder in the name of a assembly. Placeholder is replaced with the ipn input while saving. +CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME=0 + ################################################################################### # Email settings ################################################################################### @@ -66,8 +69,9 @@ ERROR_PAGE_SHOW_HELP=1 # Configure which columns will be visible by default in the specific table (and in which order). # This is a comma separated list of column names. See documentation for available values. -TABLE_ASSEMBLIES_DEFAULT_COLUMNS=quantity,manufacturer,name,description,category - +TABLE_PARTS_DEFAULT_COLUMNS=name,description,category,footprint,manufacturer,storage_location,amount +TABLE_ASSEMBLIES_DEFAULT_COLUMNS=id,ipn,name,description,referencedAssemblies,edit +TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS=quantity,id,ipn,name,description ################################################################################### # SAML Single sign on-settings From 657f9c935647183df4903ebedff75a224b6ef930 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 3 Jul 2025 14:47:19 +0200 Subject: [PATCH 20/83] Assembly getReferencedAssemblies korrigieren --- src/Entity/AssemblySystem/Assembly.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 5991b9e1d..9593dbb53 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -400,7 +400,7 @@ public function getReferencedAssemblies(): array $assemblies = []; foreach($this->bom_entries as $entry) { - if ($entry->getAssembly() !== null) { + if ($entry->getReferencedAssembly() !== null) { $assemblies[] = $entry->getReferencedAssembly(); } } From 6f8f745f41ec181e6b7b054758d37b243de43268 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 7 Jul 2025 10:08:19 +0200 Subject: [PATCH 21/83] =?UTF-8?q?F=C3=BCge=20Unterst=C3=BCtzung=20f=C3=BCr?= =?UTF-8?q?=20Datenquellen-Synonyme=20hinzu.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ermöglicht benutzerdefinierte Synonyme für Datenquellen basierend auf Locale. Synonyme werden in verschiedenen Bereichen wie Bäumen, Übersetzungen und Vorlagen genutzt, um anpassbare Namen anzuzeigen. --- config/parameters.yaml | 14 ++++++ config/services.yaml | 21 +++++++++ src/Controller/AssemblyController.php | 1 + src/Services/Trees/ToolsTreeBuilder.php | 43 +++++++++++++++---- src/Services/Trees/TreeViewGenerator.php | 36 +++++++++++++--- src/Twig/DataSourceNameExtension.php | 42 ++++++++++++++++++ templates/admin/assembly_admin.html.twig | 4 +- templates/admin/category_admin.html.twig | 4 +- templates/admin/footprint_admin.html.twig | 4 +- templates/admin/manufacturer_admin.html.twig | 4 +- templates/admin/project_admin.html.twig | 4 +- templates/admin/storelocation_admin.html.twig | 4 +- templates/admin/supplier_admin.html.twig | 4 +- templates/components/tree_macros.html.twig | 24 ++++++----- templates/form/permission_layout.html.twig | 23 +++++++++- translations/messages.cs.xlf | 6 +++ translations/messages.da.xlf | 6 +++ translations/messages.de.xlf | 6 +++ translations/messages.el.xlf | 6 +++ translations/messages.en.xlf | 6 +++ translations/messages.es.xlf | 6 +++ translations/messages.fr.xlf | 6 +++ translations/messages.it.xlf | 6 +++ translations/messages.ja.xlf | 6 +++ translations/messages.nl.xlf | 6 +++ translations/messages.pl.xlf | 6 +++ translations/messages.ru.xlf | 6 +++ translations/messages.zh.xlf | 6 +++ 28 files changed, 274 insertions(+), 36 deletions(-) create mode 100644 src/Twig/DataSourceNameExtension.php diff --git a/config/parameters.yaml b/config/parameters.yaml index c6ef90e03..9a590ec87 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -16,6 +16,20 @@ parameters: partdb.create_assembly_use_ipn_placeholder_in_name: '%env(bool:CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME)%' # Use an %%ipn%% placeholder in the name of an assembly. Placeholder is replaced with the ipn input while saving. + partdb.data_sources.synonyms: # Define your own synonyms for the given data sources + # Possible datasources: category, storagelocation, footprint, manufacturer, supplier, project, assembly + # Possible locales like the ones in 'partdb.locale_menu': en, de, it, fr, ru, ja, cs, da, zh, pl + #category: + #de: 'Bauteil Kategorien' + #en: 'Part categories' + #project: + #de: 'Geräte' + #en: 'Devices' + #assembly: + #de: 'Zusammengestellte Baugruppe' + #en: 'Combined assembly' + + ###################################################################################################################### # Users and Privacy ###################################################################################################################### diff --git a/config/services.yaml b/config/services.yaml index f787a5be3..3a13a7136 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -198,6 +198,27 @@ services: $fontDirectory: '%kernel.project_dir%/var/dompdf/fonts/' $tmpDirectory: '%kernel.project_dir%/var/dompdf/tmp/' + #################################################################################################################### + # Trees + #################################################################################################################### + App\Services\Trees\TreeViewGenerator: + arguments: + $rootNodeExpandedByDefault: '%partdb.sidebar.root_expanded%' + $rootNodeEnabled: '%partdb.sidebar.root_node_enable%' + $dataSourceSynonyms: '%partdb.data_sources.synonyms%' + App\Services\Trees\ToolsTreeBuilder: + arguments: + $dataSourceSynonyms: '%partdb.data_sources.synonyms%' + + #################################################################################################################### + # Twig Extensions + #################################################################################################################### + + App\Twig\DataSourceNameExtension: + arguments: + $dataSourceSynonyms: '%partdb.data_sources.synonyms%' + tags: [ 'twig.extension' ] + #################################################################################################################### # Part info provider system #################################################################################################################### diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php index a1ba7fa65..94e129643 100644 --- a/src/Controller/AssemblyController.php +++ b/src/Controller/AssemblyController.php @@ -29,6 +29,7 @@ use App\Entity\AssemblySystem\Assembly; use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Parts\Part; +use App\Entity\UserSystem\User; use App\Exceptions\InvalidRegexException; use App\Form\AssemblySystem\AssemblyAddPartsType; use App\Form\AssemblySystem\AssemblyBuildType; diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index 5f08b8183..a2c86b7e8 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -50,8 +50,15 @@ */ class ToolsTreeBuilder { - public function __construct(protected TranslatorInterface $translator, protected UrlGeneratorInterface $urlGenerator, protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator, protected Security $security) - { + public function __construct( + protected TranslatorInterface $translator, + protected UrlGeneratorInterface $urlGenerator, + protected TagAwareCacheInterface $cache, + protected UserCacheKeyGenerator $keyGenerator, + protected Security $security, + protected ?array $dataSourceSynonyms = [], + ) { + $this->dataSourceSynonyms = $dataSourceSynonyms ?? []; } /** @@ -161,43 +168,43 @@ protected function getEditNodes(): array } if ($this->security->isGranted('read', new Category())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.categories'), + $this->getTranslatedDataSourceOrSynonym('category', 'tree.tools.edit.categories', $this->translator->getLocale()), $this->urlGenerator->generate('category_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-tags'); } if ($this->security->isGranted('read', new Project())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.projects'), + $this->getTranslatedDataSourceOrSynonym('project', 'tree.tools.edit.projects', $this->translator->getLocale()), $this->urlGenerator->generate('project_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-archive'); } if ($this->security->isGranted('read', new Assembly())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.assemblies'), + $this->getTranslatedDataSourceOrSynonym('assembly', 'tree.tools.edit.assemblies', $this->translator->getLocale()), $this->urlGenerator->generate('assembly_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-list'); } if ($this->security->isGranted('read', new Supplier())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.suppliers'), + $this->getTranslatedDataSourceOrSynonym('supplier', 'tree.tools.edit.suppliers', $this->translator->getLocale()), $this->urlGenerator->generate('supplier_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-truck'); } if ($this->security->isGranted('read', new Manufacturer())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.manufacturer'), + $this->getTranslatedDataSourceOrSynonym('manufacturer', 'tree.tools.edit.manufacturer', $this->translator->getLocale()), $this->urlGenerator->generate('manufacturer_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-industry'); } if ($this->security->isGranted('read', new StorageLocation())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.storelocation'), + $this->getTranslatedDataSourceOrSynonym('storagelocation', 'tree.tools.edit.storelocation', $this->translator->getLocale()), $this->urlGenerator->generate('store_location_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-cube'); } if ($this->security->isGranted('read', new Footprint())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.footprint'), + $this->getTranslatedDataSourceOrSynonym('footprint', 'tree.tools.edit.footprint', $this->translator->getLocale()), $this->urlGenerator->generate('footprint_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-microchip'); } @@ -305,4 +312,22 @@ protected function getSystemNodes(): array return $nodes; } + + protected function getTranslatedDataSourceOrSynonym(string $dataSource, string $translationKey, string $locale): string + { + $currentTranslation = $this->translator->trans($translationKey); + + // Call alternatives from DataSourcesynonyms (if available) + if (!empty($this->dataSourceSynonyms[$dataSource][$locale])) { + $alternativeTranslation = $this->dataSourceSynonyms[$dataSource][$locale]; + + // Use alternative translation when it deviates from the standard translation + if ($alternativeTranslation !== $currentTranslation) { + return $alternativeTranslation; + } + } + + // Otherwise return the standard translation + return $currentTranslation; + } } diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index 3a0979028..d5358bfa2 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -68,9 +68,11 @@ public function __construct( protected TranslatorInterface $translator, private readonly UrlGeneratorInterface $router, private readonly SidebarSettings $sidebarSettings, + protected ?array $dataSourceSynonyms = [], ) { $this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled; $this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded; + $this->dataSourceSynonyms = $dataSourceSynonyms ?? []; } /** @@ -226,14 +228,16 @@ protected function entityClassToRootNodeHref(string $class): ?string protected function entityClassToRootNodeString(string $class): string { + $locale = $this->translator->getLocale(); + return match ($class) { - Category::class => $this->translator->trans('category.labelp'), - StorageLocation::class => $this->translator->trans('storelocation.labelp'), - Footprint::class => $this->translator->trans('footprint.labelp'), - Manufacturer::class => $this->translator->trans('manufacturer.labelp'), - Supplier::class => $this->translator->trans('supplier.labelp'), - Project::class => $this->translator->trans('project.labelp'), - Assembly::class => $this->translator->trans('assembly.labelp'), + Category::class => $this->getTranslatedOrSynonym('category', $locale), + StorageLocation::class => $this->getTranslatedOrSynonym('storelocation', $locale), + Footprint::class => $this->getTranslatedOrSynonym('footprint', $locale), + Manufacturer::class => $this->getTranslatedOrSynonym('manufacturer', $locale), + Supplier::class => $this->getTranslatedOrSynonym('supplier', $locale), + Project::class => $this->getTranslatedOrSynonym('project', $locale), + Assembly::class => $this->getTranslatedOrSynonym('assembly', $locale), default => $this->translator->trans('tree.root_node.text'), }; } @@ -290,4 +294,22 @@ public function getGenericTree(string $class, ?AbstractStructuralDBElement $pare return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line }); } + + protected function getTranslatedOrSynonym(string $key, string $locale): string + { + $currentTranslation = $this->translator->trans($key . '.labelp'); + + // Call alternatives from DataSourcesynonyms (if available) + if (!empty($this->dataSourceSynonyms[$key][$locale])) { + $alternativeTranslation = $this->dataSourceSynonyms[$key][$locale]; + + // Use alternative translation when it deviates from the standard translation + if ($alternativeTranslation !== $currentTranslation) { + return $alternativeTranslation; + } + } + + // Otherwise return the standard translation + return $currentTranslation; + } } diff --git a/src/Twig/DataSourceNameExtension.php b/src/Twig/DataSourceNameExtension.php new file mode 100644 index 000000000..1c02243f7 --- /dev/null +++ b/src/Twig/DataSourceNameExtension.php @@ -0,0 +1,42 @@ +translator = $translator; + $this->dataSourceSynonyms = $dataSourceSynonyms ?? []; + } + + public function getFunctions(): array + { + return [ + new TwigFunction('get_data_source_name', [$this, 'getDataSourceName']), + ]; + } + + /** + * Based on the locale and data source names, gives the right synonym value back or the default translator value. + */ + public function getDataSourceName(string $dataSourceName, string $defaultKey): string + { + $locale = $this->translator->getLocale(); + + // Use alternative dataSource synonym (if available) + if (isset($this->dataSourceSynonyms[$dataSourceName][$locale])) { + return $this->dataSourceSynonyms[$dataSourceName][$locale]; + } + + // Otherwise return the standard translation + return $this->translator->trans($defaultKey); + } +} \ No newline at end of file diff --git a/templates/admin/assembly_admin.html.twig b/templates/admin/assembly_admin.html.twig index e6a90dc09..def4eeb2e 100644 --- a/templates/admin/assembly_admin.html.twig +++ b/templates/admin/assembly_admin.html.twig @@ -3,7 +3,9 @@ {# @var entity App\Entity\AssemblySystem\Assembly #} {% block card_title %} - {% trans %}assembly.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('assembly', 'assembly.caption') %} + {% set translatedSource = 'assembly.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block edit_title %} diff --git a/templates/admin/category_admin.html.twig b/templates/admin/category_admin.html.twig index 5811640b9..f1fe7663a 100644 --- a/templates/admin/category_admin.html.twig +++ b/templates/admin/category_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}category.labelp{% endtrans %} + {% set dataSourceName = get_data_source_name('category', 'category.labelp') %} + {% set translatedSource = 'category.labelp'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block additional_pills %} diff --git a/templates/admin/footprint_admin.html.twig b/templates/admin/footprint_admin.html.twig index a2c3e4afd..a6acbe84e 100644 --- a/templates/admin/footprint_admin.html.twig +++ b/templates/admin/footprint_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}footprint.labelp{% endtrans %} + {% set dataSourceName = get_data_source_name('footprint', 'footprint.labelp') %} + {% set translatedSource = 'footprint.labelp'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block master_picture_block %} diff --git a/templates/admin/manufacturer_admin.html.twig b/templates/admin/manufacturer_admin.html.twig index 5db892c04..3ce9a124c 100644 --- a/templates/admin/manufacturer_admin.html.twig +++ b/templates/admin/manufacturer_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - {% trans %}manufacturer.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('manufacturer', 'manufacturer.caption') %} + {% set translatedSource = 'manufacturer.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block edit_title %} diff --git a/templates/admin/project_admin.html.twig b/templates/admin/project_admin.html.twig index dcf8c64cf..401be7cf7 100644 --- a/templates/admin/project_admin.html.twig +++ b/templates/admin/project_admin.html.twig @@ -3,7 +3,9 @@ {# @var entity App\Entity\ProjectSystem\Project #} {% block card_title %} - {% trans %}project.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('project', 'project.caption') %} + {% set translatedSource = 'project.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block edit_title %} diff --git a/templates/admin/storelocation_admin.html.twig b/templates/admin/storelocation_admin.html.twig index c93339dc1..1e60eeea2 100644 --- a/templates/admin/storelocation_admin.html.twig +++ b/templates/admin/storelocation_admin.html.twig @@ -2,7 +2,9 @@ {% import "label_system/dropdown_macro.html.twig" as dropdown %} {% block card_title %} - {% trans %}storelocation.labelp{% endtrans %} + {% set dataSourceName = get_data_source_name('storagelocation', 'storelocation.labelp') %} + {% set translatedSource = 'storelocation.labelp'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block additional_controls %} diff --git a/templates/admin/supplier_admin.html.twig b/templates/admin/supplier_admin.html.twig index ce38a5ca4..b5cf7b236 100644 --- a/templates/admin/supplier_admin.html.twig +++ b/templates/admin/supplier_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - {% trans %}supplier.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('supplier', 'supplier.caption') %} + {% set translatedSource = 'supplier.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block additional_panes %} diff --git a/templates/components/tree_macros.html.twig b/templates/components/tree_macros.html.twig index 2e55147a1..210a00633 100644 --- a/templates/components/tree_macros.html.twig +++ b/templates/components/tree_macros.html.twig @@ -1,14 +1,16 @@ {% macro sidebar_dropdown() %} + {% set currentLocale = app.request.locale %} + {# Format is [mode, route, label, show_condition] #} {% set data_sources = [ - ['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read')], - ['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read')], - ['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read')], - ['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')], - ['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')], - ['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')], - ['assembly', path('tree_assembly_root'), 'assembly.labelp', is_granted('@assemblies.read')], - ['tools', path('tree_tools'), 'tools.label', true], + ['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read'), 'category'], + ['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read'), 'storagelocation'], + ['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read'), 'footprint'], + ['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read'), 'manufacturer'], + ['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read'), 'supplier'], + ['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read'), 'project'], + ['assembly', path('tree_assembly_root'), 'assembly.labelp', is_granted('@assemblies.read'), 'assembly'], + ['tools', path('tree_tools'), 'tools.label', true, 'tool'], ] %} @@ -19,9 +21,9 @@ {% for source in data_sources %} {% if source[3] %} {# show_condition #} -
  • + >{{ get_data_source_name(source[4], source[2]) }} {% endif %} {% endfor %} {% endmacro %} @@ -62,4 +64,4 @@
    -{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/templates/form/permission_layout.html.twig b/templates/form/permission_layout.html.twig index 166147b4c..896a2defa 100644 --- a/templates/form/permission_layout.html.twig +++ b/templates/form/permission_layout.html.twig @@ -6,12 +6,31 @@
    {% else %} - {{ form.vars.label | trans }} + def{{ form.vars.label | trans }} {% endif %} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index a1ede652a..c838f07b8 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -14781,5 +14781,11 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Neplatný regulární výraz (regex) + + + datasource.synonym + %name% (Váš synonymum: %synonym%) + + diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 5901e9f5a..07f6c1b37 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -13462,5 +13462,11 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Ugyldigt regulært udtryk (regex) + + + datasource.synonym + %name% (Dit synonym: %synonym%) + + diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index a7ece41a9..7e2c84385 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -14747,5 +14747,11 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Ungültiger regulärer Ausdruck (regex) + + + datasource.synonym + %name% (Ihr Synonym: %synonym%) + + diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index fd5114539..07143e5b7 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -2470,5 +2470,11 @@ Μη έγκυρη κανονική έκφραση (regex) + + + datasource.synonym + %name% (Το συνώνυμό σας: %synonym%) + + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index c4f7c60f9..df4c2aed7 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -14748,5 +14748,11 @@ Please note, that you can not impersonate a disabled user. If you try you will g Invalid regular expression (regex) + + + datasource.synonym + %name% (Your synonym: %synonym%) + + diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 461e7a7b1..1dfdf1dd6 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -13634,5 +13634,11 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Expresión regular no válida (regex) + + + datasource.synonym + %name% (Tu sinónimo: %synonym%) + + diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index fe470e449..5c055aaef 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -10044,5 +10044,11 @@ exemple de ville Expression régulière invalide (regex) + + + datasource.synonym + %name% (Votre synonyme : %synonym%) + + diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index c727eef82..b018468bb 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -13636,5 +13636,11 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Espressione regolare non valida (regex) + + + datasource.synonym + %name% (Il tuo sinonimo: %synonym%) + + diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 157b1cf26..4d1c82edf 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -9757,5 +9757,11 @@ Exampletown 無効な正規表現(regex) + + + datasource.synonym + %name% (あなたの同義語: %synonym%) + + diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 44a48dcb3..822191818 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1695,5 +1695,11 @@ Ongeldige reguliere expressie (regex) + + + datasource.synonym + %name% (Uw synoniem: %synonym%) + + diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 171c585d6..6a7e5d591 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -13489,5 +13489,11 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Nieprawidłowe wyrażenie regularne (regex) + + + datasource.synonym + %name% (Twój synonim: %synonym%) + + diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 7bd0f3bc4..2add6347a 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -13589,5 +13589,11 @@ Неверное регулярное выражение (regex) + + + datasource.synonym + %name% (Ваш синоним: %synonym%) + + diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 9e8d8aa1f..cd6b34b2c 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -13474,5 +13474,11 @@ Element 3 无效的正则表达式(regex) + + + datasource.synonym + %name% (您的同义词: %synonym%) + + From d26585b14213d8589ab6903a5cffcb05d3db3f03 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 8 Jul 2025 09:05:53 +0200 Subject: [PATCH 22/83] =?UTF-8?q?F=C3=BCge=20Validierung=20f=C3=BCr=20zykl?= =?UTF-8?q?ische=20Baugruppenreferenzen=20hinzu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eine neue Validierung wurde implementiert, um zyklische Referenzen in Baugruppen zu erkennen. Entsprechende Fehlertexte wurden in allen unterstützten Sprachen hinzugefügt. Zudem wurde der Validator in die Entität AssemblyBOMEntry integriert. --- config/services.yaml | 3 + src/Entity/AssemblySystem/Assembly.php | 2 + .../AssemblySystem/AssemblyBOMEntry.php | 2 + .../AssemblySystem/AssemblyCycle.php | 39 +++++ .../AssemblySystem/AssemblyCycleValidator.php | 139 ++++++++++++++++++ translations/validators.cs.xlf | 6 + translations/validators.da.xlf | 6 + translations/validators.de.xlf | 6 + translations/validators.el.xlf | 6 + translations/validators.en.xlf | 6 + translations/validators.fr.xlf | 6 + translations/validators.hr.xlf | 6 + translations/validators.it.xlf | 6 + translations/validators.ja.xlf | 6 + translations/validators.pl.xlf | 6 + translations/validators.ru.xlf | 6 + translations/validators.zh.xlf | 6 + 17 files changed, 257 insertions(+) create mode 100644 src/Validator/Constraints/AssemblySystem/AssemblyCycle.php create mode 100644 src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php diff --git a/config/services.yaml b/config/services.yaml index 3a13a7136..7f0b5897f 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -168,6 +168,9 @@ services: arguments: $useAssemblyIpnPlaceholder: '%partdb.create_assembly_use_ipn_placeholder_in_name%' + App\Validator\Constraints\AssemblySystem\AssemblyCycleValidator: + tags: [ 'validator.constraint_validator' ] + #################################################################################################################### # Table settings #################################################################################################################### diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 9593dbb53..5ce060d8e 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -23,6 +23,7 @@ namespace App\Entity\AssemblySystem; use App\Repository\AssemblyRepository; +use App\Validator\Constraints\AssemblySystem\AssemblyCycle; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; @@ -113,6 +114,7 @@ class Assembly extends AbstractStructuralDBElement #[Groups(['extended', 'full', 'import'])] #[ORM\OneToMany(targetEntity: AssemblyBOMEntry::class, mappedBy: 'assembly', cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])] + #[AssemblyCycle] #[UniqueReferencedAssembly] #[UniqueObjectCollection(message: 'assembly.bom_entry.project_already_in_bom', fields: ['project'])] #[UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name'])] diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 820fc2f5c..6a3e82d3c 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -38,6 +38,7 @@ use App\Entity\Contracts\TimeStampableInterface; use App\Entity\ProjectSystem\Project; use App\Repository\DBElementRepository; +use App\Validator\Constraints\AssemblySystem\AssemblyCycle; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; @@ -140,6 +141,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt '(this.getPart() === null or this.getReferencedAssembly() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))', message: 'validator.assembly.bom_entry.only_part_or_assembly_allowed' )] + #[AssemblyCycle] #[ORM\ManyToOne(targetEntity: Assembly::class)] #[ORM\JoinColumn(name: 'id_referenced_assembly', nullable: true, onDelete: 'SET NULL')] #[Groups(['bom_entry:read', 'bom_entry:write', ])] diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyCycle.php b/src/Validator/Constraints/AssemblySystem/AssemblyCycle.php new file mode 100644 index 000000000..9d79b879c --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/AssemblyCycle.php @@ -0,0 +1,39 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use Symfony\Component\Validator\Constraint; + +/** + * This constraint checks that there is no cycle in bom configuration of the assembly + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class AssemblyCycle extends Constraint +{ + public string $message = 'assembly.bom_entry.assembly_cycle'; + + public function validatedBy(): string + { + return AssemblyCycleValidator::class; + } +} \ No newline at end of file diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php new file mode 100644 index 000000000..3483f94a1 --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php @@ -0,0 +1,139 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; +use ReflectionClass; + +/** + * Validator class to check for cycles in assemblies based on BOM entries. + * + * This validator ensures that the structure of assemblies does not contain circular dependencies + * by validating each entry in the Bill of Materials (BOM) of the given assembly. Additionally, + * it can handle form-submitted BOM entries to include these in the validation process. + */ +class AssemblyCycleValidator extends ConstraintValidator +{ + public function validate($value, Constraint $constraint): void + { + if (!$constraint instanceof AssemblyCycle) { + throw new UnexpectedTypeException($constraint, AssemblyCycle::class); + } + + if (!$value instanceof Assembly) { + return; + } + + $bomEntries = $value->getBomEntries()->toArray(); + + // Consider additional entries from the form + if ($this->context->getRoot()->has('bom_entries')) { + $formBomEntries = $this->context->getRoot()->get('bom_entries')->getData(); + if ($formBomEntries) { + $given = is_array($formBomEntries) ? $formBomEntries : iterator_to_array($formBomEntries); + foreach ($given as $givenIdx => $entry) { + if (in_array($entry, $bomEntries, true)) { + continue; + } else { + $bomEntries[$givenIdx] = $entry; + } + } + } + } + + $visitedAssemblies = []; + foreach ($bomEntries as $bomEntry) { + if ($this->hasCycle($bomEntry->getReferencedAssembly(), $value, $visitedAssemblies)) { + $this->addViolation($value, $constraint); + } + } + } + + private function hasCycle(?Assembly $currentAssembly, Assembly $originalAssembly, array &$visitedAssemblies): bool + { + if ($currentAssembly === null) { + return false; + } + + if (in_array($currentAssembly, $visitedAssemblies, true)) { + return true; + } + + $visitedAssemblies[] = $currentAssembly; + + foreach ($currentAssembly->getBomEntries() as $bomEntry) { + if ($this->hasCycle($bomEntry->getReferencedAssembly(), $originalAssembly, $visitedAssemblies)) { + return true; + } + } + + return false; + } + + /** + * Adds a violation to the current context if it hasn’t already been added. + * + * This method checks whether a violation with the same property path as the current violation + * already exists in the context. If such a violation is found, the current violation is not added again. + * The process involves reflection to access private or protected properties of violation objects. + * + * @param mixed $value The value that triggered the violation. + * @param Constraint $constraint The constraint containing the validation details. + * + */ + private function addViolation($value, Constraint $constraint): void + { + /** @var ConstraintViolationBuilder $buildViolation */ + $buildViolation = $this->context->buildViolation($constraint->message) + ->setParameter('%name%', $value->getName()); + + $alreadyAdded = false; + + try { + $reflectionClass = new ReflectionClass($buildViolation); + $property = $reflectionClass->getProperty('propertyPath'); + $propertyPath = $property->getValue($buildViolation); + + $availableViolations = $this->context->getViolations(); + + foreach ($availableViolations as $tmpViolation) { + $tmpReflectionClass = new ReflectionClass($tmpViolation); + $tmpProperty = $tmpReflectionClass->getProperty('propertyPath'); + $tmpPropertyPath = $tmpProperty->getValue($tmpViolation); + + if ($tmpPropertyPath === $propertyPath) { + $alreadyAdded = true; + } + } + } catch (\ReflectionException) { + } + + if (!$alreadyAdded) { + $buildViolation->addViolation(); + } + } +} \ No newline at end of file diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index f053743f3..b99fe349d 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -401,6 +401,12 @@ Tato sestava již existuje jako položka v seznamu materiálů! + + + assembly.bom_entry.assembly_cycle + Byl zjištěn cyklus: Sestava "%name%" nepřímo odkazuje sama na sebe. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 239e3572a..24fa330af 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -371,6 +371,12 @@ Denne samling findes allerede som en post! + + + assembly.bom_entry.assembly_cycle + En cyklus blev opdaget: Samlingen "%name%" refererer indirekte til sig selv. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 86b31e583..fa84354b9 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -395,6 +395,12 @@ Diese Baugruppe existiert bereits als Eintrag! + + + assembly.bom_entry.assembly_cycle + Ein Zyklus wurde entdeckt: Die Baugruppe "%name%" referenziert sich indirekt selbst. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index 4e4278daf..bc9b0947e 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -37,6 +37,12 @@ Αυτή η συναρμολόγηση υπάρχει ήδη ως εγγραφή! + + + assembly.bom_entry.assembly_cycle + Εντοπίστηκε κύκλος: Η συναρμολόγηση "%name%" αναφέρεται έμμεσα στον εαυτό της. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 59cabf552..7d9beb4ec 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -395,6 +395,12 @@ This assembly already exists as an entry! + + + assembly.bom_entry.assembly_cycle + A cycle was detected: the assembly "%name%" indirectly references itself. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index aff68a185..1c9c53029 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -227,6 +227,12 @@ Cet assemblage existe déjà en tant qu'entrée ! + + + assembly.bom_entry.assembly_cycle + Un cycle a été détecté : L'assemblage "%name%" se réfère indirectement à lui-même. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 1ee5c06fe..89d470e7e 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -389,6 +389,12 @@ Ova se montaža već nalazi kao zapis! + + + assembly.bom_entry.assembly_cycle + Otkriven je ciklus: Sklop "%name%" neizravno referencira samog sebe. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index ac57a2cc4..e9b528bb7 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -389,6 +389,12 @@ Questo assemblaggio è già presente come voce! + + + assembly.bom_entry.assembly_cycle + È stato rilevato un ciclo: L'assemblaggio "%name%" fa riferimento indirettamente a sé stesso. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index a316707ad..80ec65ff2 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -227,6 +227,12 @@ このアセンブリはすでにエントリとして存在します! + + + assembly.bom_entry.assembly_cycle + 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 95c44ab4d..5df01cd6a 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -389,6 +389,12 @@ To zestawienie jest już dodane jako wpis! + + + assembly.bom_entry.assembly_cycle + 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 425ede5f1..8bf08ab3c 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -389,6 +389,12 @@ Этот сборочный узел уже добавлен как запись! + + + assembly.bom_entry.assembly_cycle + Обнаружен цикл: Сборка «%name%» косвенно ссылается на саму себя. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 4a02523bb..87c507c19 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -377,6 +377,12 @@ 此装配已经作为条目存在! + + + assembly.bom_entry.assembly_cycle + 检测到循环:装配体“%name%”间接引用了其自身。 + + assembly.bom_entry.project_already_in_bom From 360f4f07a770e631433a910d121ac0ea4332e35e Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 17 Jul 2025 10:54:44 +0200 Subject: [PATCH 23/83] Anpassungen aus Analyse vornehmen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Name Validierung bei Assembly Angabe in Stücklisten anpassen. permission_layout.html hinsichtlich Synonym-Ausgabe Datenquelle anpassen. Anpassung aus Analyse. --- assets/js/lib/datatables.js | 8 ++++++-- migrations/Version20250304154507.php | 2 +- src/DataTables/AssemblyBomEntriesDataTable.php | 2 ++ src/Entity/AssemblySystem/AssemblyBOMEntry.php | 6 +++--- templates/form/permission_layout.html.twig | 15 ++++++++++----- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/assets/js/lib/datatables.js b/assets/js/lib/datatables.js index 67bab02db..7c82439cc 100644 --- a/assets/js/lib/datatables.js +++ b/assets/js/lib/datatables.js @@ -13,11 +13,16 @@ * Initializes the datatable dynamically. */ $.fn.initDataTables = function(config, options) { - //Update default used url, so it reflects the current location (useful on single side apps) //CHANGED jbtronics: Preserve the get parameters (needed so we can pass additional params to query) $.fn.initDataTables.defaults.url = window.location.origin + window.location.pathname + window.location.search; + $.fn.dataTable.ext.errMode = function(settings, helpPage, message) { + if (message.includes('ColReorder')) { + console.warn('ColReorder does not fit the number of columns', message); + } + }; + var root = this, config = $.extend({}, $.fn.initDataTables.defaults, config), state = '' @@ -105,7 +110,6 @@ } } - root.html(data.template); dt = $('table', root).DataTable(dtOpts); if (config.state !== 'none') { diff --git a/migrations/Version20250304154507.php b/migrations/Version20250304154507.php index 4f7fed2f4..8a406b27d 100644 --- a/migrations/Version20250304154507.php +++ b/migrations/Version20250304154507.php @@ -18,7 +18,7 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { $this->addSql(<<<'SQL' - ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL + ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL AFTER build_project_id SQL); $this->addSql(<<<'SQL' ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php index fed6850f4..b2c3e118d 100644 --- a/src/DataTables/AssemblyBomEntriesDataTable.php +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -127,6 +127,8 @@ public function configure(DataTable $dataTable, array $options): void 'render' => function ($value, AssemblyBOMEntry $context) { if($context->getPart() instanceof Part) { return $context->getPart()->getIpn(); + } elseif($context->getReferencedAssembly() instanceof Assembly) { + return $context->getReferencedAssembly()->getIpn(); } return ''; diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 6a3e82d3c..8a4cdbc24 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -106,7 +106,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt /** * @var string|null An optional name describing this BOM entry (useful for non-part entries) */ - #[Assert\Expression('this.getPart() !== null or this.getName() !== null', message: 'validator.assembly.bom_entry.name_or_part_needed')] + #[Assert\Expression('this.getPart() !== null or this.getReferencedAssembly() !== null or this.getName() !== null', message: 'validator.assembly.bom_entry.name_or_part_needed')] #[ORM\Column(type: Types::STRING, nullable: true)] #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] protected ?string $name = null; @@ -206,7 +206,7 @@ public function setMountnames(string $mountnames): AssemblyBOMEntry */ public function getName(): ?string { - return $this->name; + return trim($this->name ?? '') === '' ? null : $this->name; } /** @@ -214,7 +214,7 @@ public function getName(): ?string */ public function setName(?string $name): AssemblyBOMEntry { - $this->name = $name; + $this->name = trim($name ?? '') === '' ? null : $name; return $this; } diff --git a/templates/form/permission_layout.html.twig b/templates/form/permission_layout.html.twig index 896a2defa..dcceae335 100644 --- a/templates/form/permission_layout.html.twig +++ b/templates/form/permission_layout.html.twig @@ -7,15 +7,15 @@
    From 40778c4e0519e99c328d7e45204f185b74185471 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Fri, 18 Jul 2025 10:36:12 +0200 Subject: [PATCH 24/83] =?UTF-8?q?St=C3=BCcklisten=20beim=20L=C3=B6schen:?= =?UTF-8?q?=20Markieren=20von=20referenzierten=20Baugruppen=20als=20Hinwei?= =?UTF-8?q?s=20vornehmen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminPages/BaseAdminController.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index 039751ea6..bae817de6 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -24,6 +24,7 @@ use App\DataTables\LogDataTable; use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentUpload; @@ -469,6 +470,10 @@ protected function _delete(Request $request, AbstractNamedDBElement $entity, Str return $this->redirectToRoute($this->route_base.'_edit', ['id' => $entity->getID()]); } } else { + if ($entity instanceof Assembly) { + $this->markReferencedBomEntry($entity); + } + if ($entity instanceof AbstractStructuralDBElement) { $parent = $entity->getParent(); @@ -516,4 +521,16 @@ protected function _exportEntity(AbstractNamedDBElement $entity, EntityExporter return $exporter->exportEntityFromRequest($entity, $request); } + + private function markReferencedBomEntry(Assembly $referencedAssembly): void + { + $bomEntries = $this->entityManager->getRepository(AssemblyBOMEntry::class)->findBy(['referencedAssembly' => $referencedAssembly]); + + foreach ($bomEntries as $entry) { + $entry->setReferencedAssembly(null); + $entry->setName($referencedAssembly->getName(). ' DELETED'); + + $this->entityManager->persist($entry); + } + } } From db9c7663104f9bcd9f0e5c00ef5672db867b93c1 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 21 Jul 2025 10:04:54 +0200 Subject: [PATCH 25/83] Migration: Spaltenname korrigieren --- migrations/Version20250304154507.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/Version20250304154507.php b/migrations/Version20250304154507.php index 8a406b27d..bb25b8022 100644 --- a/migrations/Version20250304154507.php +++ b/migrations/Version20250304154507.php @@ -18,7 +18,7 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { $this->addSql(<<<'SQL' - ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL AFTER build_project_id + ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL AFTER built_project_id SQL); $this->addSql(<<<'SQL' ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) From 64c9773b3366897cb6abf94efd10cb8ee8f98cbb Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 22 Jul 2025 10:30:22 +0200 Subject: [PATCH 26/83] BOMImporter: Verbesserung des CSV-Parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Das Parsing wurde angepasst, um sowohl Komma- als auch Semikolon-getrennte CSV-Dateien zu unterstützen. Zudem werden Spaltennamen in Kleinbuchstaben konvertiert und zusätzliche Fallback-Logik für bestimmte Felder hinzugefügt. --- .../ImportExportSystem/BOMImporter.php | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index e345e595c..a504ab879 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -433,14 +433,27 @@ private function parseJson(string $data, string $objectType = ProjectBOMEntry::c function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): ImporterResult { $result = new ImporterResult(); - $rows = explode("\n", trim($csvData)); - $headers = str_getcsv(array_shift($rows), ';'); + $rows = explode("\r\n", trim($csvData)); + $headers = str_getcsv(array_shift($rows), ','); + + if (count($headers) === 1 && isset($headers[0])) { + //If only one column was recognized, try fallback with semicolon as a separator + $headers = str_getcsv($headers[0], ';'); + } foreach ($rows as $key => $row) { $entry = []; - $values = str_getcsv($row, ';'); + $values = str_getcsv($row, ','); + + if (count($values) === 1 || count($values) !== count($headers)) { + //If only one column was recognized, try fallback with semicolon as a separator + $values = str_getcsv($row, ';'); + } foreach ($headers as $index => $column) { + //Change the column names in small letters + $column = strtolower($column); + //Convert column name into hierarchy $path = explode('_', $column); $temp = &$entry; @@ -493,6 +506,11 @@ function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): )); } + if (isset($entry['id']) && is_numeric($entry['id'])) { + //Use id column as a fallback for the expected part_id column + $entry['part']['id'] = (int) $entry['id']; + } + if (isset($entry['part'])) { $this->processPart($entry, $result, $key, $objectType, self::IMPORT_TYPE_CSV); } else { @@ -775,7 +793,12 @@ private function processPart(array $entry, ImporterResult $result, int $key, str $bomEntry->setQuantity((float) $entry['quantity']); if (isset($entry['name'])) { - $bomEntry->setName(trim($entry['name']) === '' ? null : trim ($entry['name'])); + $givenName = trim($entry['name']) === '' ? null : trim ($entry['name']); + + if ($givenName !== null && $bomEntry->getPart() !== null && $bomEntry->getPart()->getName() !== $givenName) { + //Apply different names for parts list entry + $bomEntry->setName(trim($entry['name']) === '' ? null : trim ($entry['name'])); + } } else { $bomEntry->setName(null); } From 15ad64c93b4632be3da6512827bac265b1c8a4a2 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 22 Jul 2025 12:20:26 +0200 Subject: [PATCH 27/83] Reihenfolge der Tabs in Baugruppenansicht korrigiert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Tabs "Details" und "Stückliste" wurden in der Baugruppenansicht vertauscht, um der Nutzererwartung besser zu entsprechen. Zudem wurde die Methode zur rekursiven Ermittlung referenzierter Baugruppen überarbeitet und in der Datentabelle integriert. --- src/DataTables/AssemblyDataTable.php | 5 ++-- src/Entity/AssemblySystem/Assembly.php | 29 +++++++++++++++++++----- templates/assemblies/info/info.html.twig | 24 ++++++++++---------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/DataTables/AssemblyDataTable.php b/src/DataTables/AssemblyDataTable.php index f3854ebca..745a62b62 100644 --- a/src/DataTables/AssemblyDataTable.php +++ b/src/DataTables/AssemblyDataTable.php @@ -109,14 +109,13 @@ public function configure(DataTable $dataTable, array $options): void $this->csh->add('referencedAssemblies', TextColumn::class, [ 'label' => $this->translator->trans('assembly.referencedAssembly.labelp'), 'render' => function ($value, Assembly $context): string { - $assemblies = $context->getReferencedAssemblies(); + $assemblies = $context->getAllReferencedAssembliesRecursive($context); $max = 5; $tmp = ""; for ($i = 0; $i < min($max, count($assemblies)); $i++) { - $url = $this->urlGenerator->infoURL($assemblies[$i]); - $tmp .= sprintf('%s', $url, htmlspecialchars($assemblies[$i]->getName())); + $tmp .= $this->assemblyDataTableHelper->renderName($assemblies[$i]); if ($i < count($assemblies) - 1) { $tmp .= ", "; } diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 5ce060d8e..cb6e94def 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -393,20 +393,37 @@ public function validate(ExecutionContextInterface $context, $payload): void } /** - * Get all referenced assemblies which uses this assembly. + * Get all assemblies and sub-assemblies recursive that are referenced in the assembly bom entries. * - * @return Assembly[] all referenced assemblies which uses this assembly as a one-dimensional array of assembly objects + * @param Assembly $assembly Assembly, which is to be processed recursively. + * @param array $processedAssemblies (optional) a list of the already edited assemblies to avoid circulatory references. + * @return Assembly[] A flat list of all recursively found assemblies. */ - public function getReferencedAssemblies(): array + public function getAllReferencedAssembliesRecursive(Assembly $assembly, array &$processedAssemblies = []): array { $assemblies = []; - foreach($this->bom_entries as $entry) { - if ($entry->getReferencedAssembly() !== null) { - $assemblies[] = $entry->getReferencedAssembly(); + // Avoid circular references + if (in_array($assembly, $processedAssemblies, true)) { + return $assemblies; + } + + // Add the current assembly to the processed + $processedAssemblies[] = $assembly; + + // Iterate by the bom entries of the current assembly + foreach ($assembly->getBomEntries() as $bomEntry) { + if ($bomEntry->getReferencedAssembly() !== null) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + $assemblies[] = $referencedAssembly; + + // Continue recursively to process sub-assemblies + $assemblies = array_merge($assemblies, $this->getAllReferencedAssembliesRecursive($referencedAssembly, $processedAssemblies)); } } return $assemblies; } + } diff --git a/templates/assemblies/info/info.html.twig b/templates/assemblies/info/info.html.twig index d787ea086..098173861 100644 --- a/templates/assemblies/info/info.html.twig +++ b/templates/assemblies/info/info.html.twig @@ -64,18 +64,18 @@ {% block card_content %}
    -
    - {% include "assemblies/info/_part.html.twig" %} -
    {% include "assemblies/info/_info.html.twig" %}
    +
    + {% include "assemblies/info/_part.html.twig" %} +
    {% include "assemblies/info/_builds.html.twig" %}
    From 08a33338bdcb3e67a97d63fef83079444442d890 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 22 Jul 2025 15:52:57 +0200 Subject: [PATCH 28/83] BOMImporter und AssemblyCycleValidator: Verbesserte Import-Logik MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Importmethoden wurden vereinheitlicht, um Projekte und Baugruppen gleichermaßen zu unterstützen. Zudem wurde die Validierung optimiert und die CSV-, JSON- und KiCAD-Parsing-Methoden angepasst, um kontextbasierte BOM-Einträge effizienter zu erstellen. --- .../ImportExportSystem/BOMImporter.php | 180 ++++++++++++------ .../AssemblySystem/AssemblyCycleValidator.php | 3 +- 2 files changed, 124 insertions(+), 59 deletions(-) diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index a504ab879..6a3528117 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -100,7 +100,7 @@ protected function configureOptions(OptionsResolver $resolver): OptionsResolver */ public function importFileIntoProject(UploadedFile $file, Project $project, array $options): ImporterResult { - $importerResult = $this->fileToImporterResult($file, $options); + $importerResult = $this->fileToImporterResult($project, $file, $options); if ($importerResult->getViolations()->count() === 0) { //Assign the bom_entries to the project @@ -113,12 +113,21 @@ public function importFileIntoProject(UploadedFile $file, Project $project, arra } /** - * Converts the given file into an ImporterResult with an array of BOM entries using the given options and save them into the given assembly. - * The changes are not saved into the database yet. + * Imports a file into an Assembly object and processes its contents. + * + * This method converts the provided file into an ImporterResult object that contains BOM entries and potential + * validation violations. If no violations are found, the BOM entries extracted from the file are added to the + * provided Assembly object. + * + * @param UploadedFile $file The file to be imported and processed. + * @param Assembly $assembly The target Assembly object to which the BOM entries are added. + * @param array $options Options or configurations related to the import process. + * + * @return ImporterResult An object containing the result of the import process, including BOM entries and any violations. */ public function importFileIntoAssembly(UploadedFile $file, Assembly $assembly, array $options): ImporterResult { - $importerResult = $this->fileToImporterResult($file, $options, AssemblyBOMEntry::class); + $importerResult = $this->fileToImporterResult($assembly, $file, $options); if ($importerResult->getViolations()->count() === 0) { //Assign the bom_entries to the assembly @@ -131,18 +140,37 @@ public function importFileIntoAssembly(UploadedFile $file, Assembly $assembly, a } /** - * Converts the given file into an array of BOM entries using the given options. - * @return ProjectBOMEntry[]|AssemblyBOMEntry[] + * Converts the content of a file into an array of BOM (Bill of Materials) entries. + * + * This method processes the content of the provided file and delegates the conversion + * to a helper method that generates BOM entries based on the provided import object and options. + * + * @param Project|Assembly $importObject The object determining the context of the BOM entries (either a Project or Assembly). + * @param File $file The file whose content will be converted into BOM entries. + * @param array $options Additional options or configurations to be applied during the conversion process. + * + * @return array An array of BOM entries created from the file content. */ - public function fileToBOMEntries(File $file, array $options, string $objectType = ProjectBOMEntry::class): array + public function fileToBOMEntries(Project|Assembly $importObject, File $file, array $options): array { - return $this->stringToBOMEntries($file->getContent(), $options, $objectType); + return $this->stringToBOMEntries($importObject, $file->getContent(), $options); } + /** - * Converts the given file into an ImporterResult with an array of BOM entries using the given options. + * Handles the conversion of an uploaded file into an ImporterResult for a given project or assembly. + * + * This method processes the uploaded file by validating its file extension based on the provided import type + * options and then proceeds to convert the file content into an ImporterResult. If the file extension is + * invalid or unsupported, the result will contain a corresponding violation. + * + * @param Project|Assembly $importObject The context of the import operation (either a Project or Assembly). + * @param UploadedFile $file The uploaded file to be processed. + * @param array $options An array of options, expected to include an 'type' key to determine valid file types. + * + * @return ImporterResult An object containing the results of the import process, including any detected violations. */ - public function fileToImporterResult(UploadedFile $file, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult + public function fileToImporterResult(Project|Assembly $importObject, UploadedFile $file, array $options): ImporterResult { $result = new ImporterResult(); @@ -172,7 +200,7 @@ public function fileToImporterResult(UploadedFile $file, array $options, string $fileExtension, [ '%extension%' => $fileExtension, - '%importType%' => $this->translator->trans($objectType === ProjectBOMEntry::class ? 'project.bom_import.type.'.$options['type'] : 'assembly.bom_import.type.'.$options['type']), + '%importType%' => $this->translator->trans($importObject instanceof Project ? 'project.bom_import.type.'.$options['type'] : 'assembly.bom_import.type.'.$options['type']), '%allowedExtensions%' => implode(', ', $validExtensions), ] )); @@ -180,7 +208,7 @@ public function fileToImporterResult(UploadedFile $file, array $options, string return $result; } - return $this->stringToImporterResult($file->getContent(), $options, $objectType); + return $this->stringToImporterResult($importObject, $file->getContent(), $options); } /** @@ -202,11 +230,14 @@ public function validateBOMData(string $data, array $options): array /** * Import string data into an array of BOM entries, which are not yet assigned to a project. - * @param string $data The data to import - * @param array $options An array of options + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $data The data to import + * @param array $options An array of options + * * @return ProjectBOMEntry[]|AssemblyBOMEntry[] An array of imported entries */ - public function stringToBOMEntries(string $data, array $options, string $objectType = ProjectBOMEntry::class): array + public function stringToBOMEntries(Project|Assembly $importObject, string $data, array $options): array { $resolver = new OptionsResolver(); $resolver = $this->configureOptions($resolver); @@ -221,11 +252,14 @@ public function stringToBOMEntries(string $data, array $options, string $objectT /** * Import string data into an array of BOM entries, which are not yet assigned to a project. - * @param string $data The data to import - * @param array $options An array of options + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $data The data to import + * @param array $options An array of options + * * @return ImporterResult An result of imported entries or a violation list */ - public function stringToImporterResult(string $data, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult + public function stringToImporterResult(Project|Assembly $importObject, string $data, array $options): ImporterResult { $resolver = new OptionsResolver(); $resolver = $this->configureOptions($resolver); @@ -238,14 +272,28 @@ public function stringToImporterResult(string $data, array $options, string $obj )); return match ($options['type']) { - self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $objectType), - self::IMPORT_TYPE_JSON => $this->parseJson($data, $objectType), - self::IMPORT_TYPE_CSV => $this->parseCsv($data, $objectType), + self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($importObject, $data, $options), + self::IMPORT_TYPE_JSON => $this->parseJson($importObject, $data), + self::IMPORT_TYPE_CSV => $this->parseCsv($importObject, $data), default => $defaultImporterResult, }; } - private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntry::class): ImporterResult + /** + * Parses a KiCAD PCB file and imports its BOM (Bill of Materials) entries into the given Project or Assembly context. + * + * This method processes a semicolon-delimited CSV data string, normalizes column names, + * validates the required fields, and creates BOM entries for each record in the data. + * The BOM entries are added to the provided Project or Assembly, depending on the context. + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $data The semicolon- or comma-delimited CSV data to be parsed. + * + * @return ImporterResult The result of the import process, containing the created BOM entries. + * + * @throws UnexpectedValueException If required fields are missing in the provided data. + */ + private function parseKiCADPCB(Project|Assembly $importObject, string $data): ImporterResult { $result = new ImporterResult(); @@ -271,8 +319,8 @@ private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntr throw new \UnexpectedValueException('Quantity missing at line ' . ($offset + 1) . '!'); } - $bom_entry = $objectType === ProjectBOMEntry::class ? new ProjectBOMEntry() : new AssemblyBOMEntry(); - if ($objectType === ProjectBOMEntry::class) { + $bom_entry = $importObject instanceof Project ? new ProjectBOMEntry() : new AssemblyBOMEntry(); + if ($bom_entry instanceof ProjectBOMEntry) { $bom_entry->setName($entry['Designation'] . ' (' . $entry['Package'] . ')'); } else { $bom_entry->setName($entry['Designation']); @@ -370,15 +418,15 @@ private function validateKiCADSchematicData(string $data, array $options): array * - Checking for empty or invalid descriptions. * - Ensuring manufacturers, if specified, have valid `name` or `id` values. * - * @param string $data JSON encoded string containing BOM entries data. - * @param string $objectType The type of entries expected during import (e.g., `ProjectBOMEntry` or `AssemblyBOMEntry`). + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $data JSON encoded string containing BOM entries data. * * @return ImporterResult The result containing parsed data and any violations encountered during the parsing process. */ - private function parseJson(string $data, string $objectType = ProjectBOMEntry::class): ImporterResult + private function parseJson(Project|Assembly $importObject, string $data): ImporterResult { $result = new ImporterResult(); - $this->jsonRoot = 'JSON Import for '.$objectType === ProjectBOMEntry::class ? 'Project' : 'Assembly'; + $this->jsonRoot = 'JSON Import for '.($importObject instanceof Project ? 'Project' : 'Assembly'); $data = json_decode($data, true); @@ -407,9 +455,9 @@ private function parseJson(string $data, string $objectType = ProjectBOMEntry::c } if (isset($entry['part'])) { - $this->processPart($entry, $result, $key, $objectType,self::IMPORT_TYPE_JSON); + $this->processPart($importObject, $entry, $result, $key, self::IMPORT_TYPE_JSON); } else { - $bomEntry = $this->getOrCreateBomEntry($objectType, $entry['name'] ?? null); + $bomEntry = $this->getOrCreateBomEntry($importObject, $entry['name'] ?? null); $bomEntry->setQuantity((float) $entry['quantity']); $result->addBomEntry($bomEntry); @@ -425,16 +473,16 @@ private function parseJson(string $data, string $objectType = ProjectBOMEntry::c * performing validations and converting data based on the provided headers. * Handles potential violations and manages the creation of BOM entries based on the given type. * - * @param string $csvData The raw CSV data to parse, with rows separated by newlines. - * @param string $objectType The class type to instantiate for BOM entries, defaults to ProjectBOMEntry. + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $csvData The raw CSV data to parse, with rows separated by newlines. * * @return ImporterResult Returns an ImporterResult instance containing BOM entries and any validation violations encountered. */ - function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): ImporterResult + function parseCsv(Project|Assembly $importObject, string $csvData): ImporterResult { $result = new ImporterResult(); $rows = explode("\r\n", trim($csvData)); - $headers = str_getcsv(array_shift($rows), ','); + $headers = str_getcsv(array_shift($rows)); if (count($headers) === 1 && isset($headers[0])) { //If only one column was recognized, try fallback with semicolon as a separator @@ -443,7 +491,7 @@ function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): foreach ($rows as $key => $row) { $entry = []; - $values = str_getcsv($row, ','); + $values = str_getcsv($row); if (count($values) === 1 || count($values) !== count($headers)) { //If only one column was recognized, try fallback with semicolon as a separator @@ -471,7 +519,7 @@ function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): //Check whether the value is numerical if (is_numeric($values[$index])) { //Convert to integer or float - $temp = (strpos($values[$index], '.') !== false) + $temp = (str_contains($values[$index], '.')) ? floatval($values[$index]) : intval($values[$index]); } else { @@ -512,9 +560,9 @@ function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): } if (isset($entry['part'])) { - $this->processPart($entry, $result, $key, $objectType, self::IMPORT_TYPE_CSV); + $this->processPart($importObject, $entry, $result, $key, self::IMPORT_TYPE_CSV); } else { - $bomEntry = $this->getOrCreateBomEntry($objectType, $entry['name'] ?? null); + $bomEntry = $this->getOrCreateBomEntry($importObject, $entry['name'] ?? null); $bomEntry->setQuantity((float) $entry['quantity'] ?? 0); $result->addBomEntry($bomEntry); @@ -531,15 +579,15 @@ function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): * to identify corresponding objects in the database. The result is recorded, and violations are * logged if issues or discrepancies exist in the validation or database matching process. * - * @param array $entry The array representation of the part entry. - * @param ImporterResult $result The result object used for recording validation violations. - * @param int $key The index of the entry in the data array. - * @param string $objectType The type of object being processed. - * @param string $importType The type of import being performed. + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param array $entry The array representation of the part entry. + * @param ImporterResult $result The result object used for recording validation violations. + * @param int $key The index of the entry in the data array. + * @param string $importType The type of import being performed. * * @return void */ - private function processPart(array $entry, ImporterResult $result, int $key, string $objectType, string $importType): void + private function processPart(Project|Assembly $importObject, array $entry, ImporterResult $result, int $key, string $importType): void { $prefix = $importType === self::IMPORT_TYPE_JSON ? 'entry' : 'row'; @@ -764,12 +812,12 @@ private function processPart(array $entry, ImporterResult $result, int $key, str $part->setCategory($category); } - if ($objectType === AssemblyBOMEntry::class) { - $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['part' => $part]); + if ($importObject instanceof Assembly) { + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['assembly' => $importObject, 'part' => $part]); if ($bomEntry === null) { if (isset($entry['name']) && $entry['name'] !== '') { - $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $entry['name']]); + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['assembly' => $importObject, 'name' => $entry['name']]); } if ($bomEntry === null) { @@ -777,11 +825,11 @@ private function processPart(array $entry, ImporterResult $result, int $key, str } } } else { - $bomEntry = $this->projectBOMEntryRepository->findOneBy(['part' => $part]); + $bomEntry = $this->projectBOMEntryRepository->findOneBy(['project' => $importObject, 'part' => $part]); if ($bomEntry === null) { if (isset($entry['name']) && $entry['name'] !== '') { - $bomEntry = $this->projectBOMEntryRepository->findOneBy(['name' => $entry['name']]); + $bomEntry = $this->projectBOMEntryRepository->findOneBy(['project' => $importObject, 'name' => $entry['name']]); } if ($bomEntry === null) { @@ -828,22 +876,38 @@ private function removeEmptyProperties(array $data): array return $data; } - private function getOrCreateBomEntry(string $objectType, ?string $name) + /** + * Retrieves an existing BOM (Bill of Materials) entry by name or creates a new one if not found. + * + * Depending on whether the provided import object is a Project or Assembly, this method attempts to locate + * a corresponding BOM entry in the appropriate repository. If no entry is located, a new BOM entry object + * is instantiated according to the type of the import object. + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string|null $name The name of the BOM entry to search for or assign to a new entry. + * + * @return ProjectBOMEntry|AssemblyBOMEntry An existing or newly created BOM entry. + */ + private function getOrCreateBomEntry(Project|Assembly $importObject, ?string $name): ProjectBOMEntry|AssemblyBOMEntry { $bomEntry = null; //Check whether there is a name if (!empty($name)) { - if ($objectType === ProjectBOMEntry::class) { + if ($importObject instanceof Project) { $bomEntry = $this->projectBOMEntryRepository->findOneBy(['name' => $name]); - } elseif ($objectType === AssemblyBOMEntry::class) { + } else { $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $name]); } } - //If no bom enttry was found, a new object create + //If no bom entry was found, a new object create if ($bomEntry === null) { - $bomEntry = new $objectType(); + if ($importObject instanceof Project) { + $bomEntry = new ProjectBOMEntry(); + } else { + $bomEntry = new AssemblyBOMEntry(); + } } $bomEntry->setName($name); @@ -1374,10 +1438,10 @@ public function detectFields(string $data, ?string $delimiter = null): array * The violation includes a message, property path, invalid value, and other contextual information. * Translations for the violation message can be applied through the translator service. * - * @param string $message The translation key for the validation message. - * @param string $propertyPath The property path where the violation occurred. - * @param mixed|null $invalidValue The value that caused the violation (optional). - * @param array $parameters Additional parameters for message placeholders (default is an empty array). + * @param string $message The translation key for the validation message. + * @param string $propertyPath The property path where the violation occurred. + * @param mixed|null $invalidValue The value that caused the violation (optional). + * @param array $parameters Additional parameters for message placeholders (default is an empty array). * * @return ConstraintViolation The created constraint violation object. */ diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php index 3483f94a1..73df284dd 100644 --- a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php +++ b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php @@ -23,6 +23,7 @@ namespace App\Validator\Constraints\AssemblySystem; use App\Entity\AssemblySystem\Assembly; +use Symfony\Component\Form\Form; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; @@ -51,7 +52,7 @@ public function validate($value, Constraint $constraint): void $bomEntries = $value->getBomEntries()->toArray(); // Consider additional entries from the form - if ($this->context->getRoot()->has('bom_entries')) { + if ($this->context->getRoot() instanceof Form && $this->context->getRoot()->has('bom_entries')) { $formBomEntries = $this->context->getRoot()->get('bom_entries')->getData(); if ($formBomEntries) { $given = is_array($formBomEntries) ? $formBomEntries : iterator_to_array($formBomEntries); From 771c6c573b70f42da41890a346c7c50ee7afc568 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 24 Jul 2025 09:11:28 +0200 Subject: [PATCH 29/83] =?UTF-8?q?BOMValidator:=20Validierung=20f=C3=BCr=20?= =?UTF-8?q?rekursive=20Baugruppen-Eintragspr=C3=BCfung=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Es wurde eine neue Validierung hinzugefügt, um sicherzustellen, dass keine Baugruppe in ihrer eigenen Hierarchie als Unterbaugruppe referenziert wird. Diese Logik wurde in die entsprechenden Dateien integriert und unterstützt Mehrsprachigkeit durch neue Übersetzungen. --- src/Entity/AssemblySystem/Assembly.php | 6 +- .../AssemblySystem/AssemblyBOMEntry.php | 2 + .../AssemblySystem/AssemblyCycleValidator.php | 67 +++++--- .../AssemblyInvalidBomEntry.php | 21 +++ .../AssemblyInvalidBomEntryValidator.php | 155 ++++++++++++++++++ translations/validators.cs.xlf | 6 + translations/validators.da.xlf | 6 + translations/validators.de.xlf | 6 + translations/validators.el.xlf | 6 + translations/validators.en.xlf | 6 + translations/validators.fr.xlf | 6 + translations/validators.hr.xlf | 6 + translations/validators.it.xlf | 6 + translations/validators.ja.xlf | 6 + translations/validators.pl.xlf | 6 + translations/validators.ru.xlf | 6 + translations/validators.zh.xlf | 6 + 17 files changed, 302 insertions(+), 21 deletions(-) create mode 100644 src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php create mode 100644 src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntryValidator.php diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index cb6e94def..c65ca71fe 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -24,6 +24,7 @@ use App\Repository\AssemblyRepository; use App\Validator\Constraints\AssemblySystem\AssemblyCycle; +use App\Validator\Constraints\AssemblySystem\AssemblyInvalidBomEntry; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; @@ -111,11 +112,12 @@ class Assembly extends AbstractStructuralDBElement * @var Collection */ #[Assert\Valid] + #[AssemblyCycle] + #[AssemblyInvalidBomEntry] + #[UniqueReferencedAssembly] #[Groups(['extended', 'full', 'import'])] #[ORM\OneToMany(targetEntity: AssemblyBOMEntry::class, mappedBy: 'assembly', cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])] - #[AssemblyCycle] - #[UniqueReferencedAssembly] #[UniqueObjectCollection(message: 'assembly.bom_entry.project_already_in_bom', fields: ['project'])] #[UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name'])] protected Collection $bom_entries; diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 8a4cdbc24..7d54fe684 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -39,6 +39,7 @@ use App\Entity\ProjectSystem\Project; use App\Repository\DBElementRepository; use App\Validator\Constraints\AssemblySystem\AssemblyCycle; +use App\Validator\Constraints\AssemblySystem\AssemblyInvalidBomEntry; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; @@ -142,6 +143,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt message: 'validator.assembly.bom_entry.only_part_or_assembly_allowed' )] #[AssemblyCycle] + #[AssemblyInvalidBomEntry] #[ORM\ManyToOne(targetEntity: Assembly::class)] #[ORM\JoinColumn(name: 'id_referenced_assembly', nullable: true, onDelete: 'SET NULL')] #[Groups(['bom_entry:read', 'bom_entry:write', ])] diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php index 73df284dd..f12f19a73 100644 --- a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php +++ b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php @@ -49,49 +49,78 @@ public function validate($value, Constraint $constraint): void return; } - $bomEntries = $value->getBomEntries()->toArray(); + $availableViolations = $this->context->getViolations(); + if (count($availableViolations) > 0) { + //already violations given, currently no more needed to check + + return; + } + + $bomEntries = []; - // Consider additional entries from the form if ($this->context->getRoot() instanceof Form && $this->context->getRoot()->has('bom_entries')) { - $formBomEntries = $this->context->getRoot()->get('bom_entries')->getData(); - if ($formBomEntries) { - $given = is_array($formBomEntries) ? $formBomEntries : iterator_to_array($formBomEntries); - foreach ($given as $givenIdx => $entry) { - if (in_array($entry, $bomEntries, true)) { - continue; - } else { - $bomEntries[$givenIdx] = $entry; - } - } + $bomEntries = $this->context->getRoot()->get('bom_entries')->getData(); + $bomEntries = is_array($bomEntries) ? $bomEntries : iterator_to_array($bomEntries); + } elseif ($this->context->getRoot() instanceof Assembly) { + $bomEntries = $value->getBomEntries()->toArray(); + } + + $relevantEntries = []; + + foreach ($bomEntries as $bomEntry) { + if ($bomEntry->getReferencedAssembly() !== null) { + $relevantEntries[$bomEntry->getId()] = $bomEntry; } } $visitedAssemblies = []; - foreach ($bomEntries as $bomEntry) { + foreach ($relevantEntries as $bomEntry) { if ($this->hasCycle($bomEntry->getReferencedAssembly(), $value, $visitedAssemblies)) { $this->addViolation($value, $constraint); } } } - private function hasCycle(?Assembly $currentAssembly, Assembly $originalAssembly, array &$visitedAssemblies): bool + /** + * Determines if there is a cyclic dependency in the assembly hierarchy. + * + * This method checks if a cycle exists in the hierarchy of referenced assemblies starting + * from a given assembly. It traverses through the Bill of Materials (BOM) entries of each + * assembly recursively and keeps track of visited assemblies to detect cycles. + * + * @param Assembly|null $currentAssembly The current assembly being checked for cycles. + * @param Assembly $originalAssembly The original assembly from where the cycle detection started. + * @param Assembly[] $visitedAssemblies A list of assemblies that have been visited during the current traversal. + * + * @return bool True if a cycle is detected, false otherwise. + */ + private function hasCycle(?Assembly $currentAssembly, Assembly $originalAssembly, array $visitedAssemblies = []): bool { + //No referenced assembly → no cycle if ($currentAssembly === null) { return false; } - if (in_array($currentAssembly, $visitedAssemblies, true)) { + //If the assembly has already been visited, there is a cycle + if (in_array($currentAssembly->getId(), array_map(fn($a) => $a->getId(), $visitedAssemblies), true)) { return true; } + //Add the current assembly to the visited $visitedAssemblies[] = $currentAssembly; + //Go through the bom entries of the current assembly foreach ($currentAssembly->getBomEntries() as $bomEntry) { - if ($this->hasCycle($bomEntry->getReferencedAssembly(), $originalAssembly, $visitedAssemblies)) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + if ($referencedAssembly !== null && $this->hasCycle($referencedAssembly, $originalAssembly, $visitedAssemblies)) { return true; } } + //Remove the current assembly from the list of visit (recursion completed) + array_pop($visitedAssemblies); + return false; } @@ -102,11 +131,11 @@ private function hasCycle(?Assembly $currentAssembly, Assembly $originalAssembly * already exists in the context. If such a violation is found, the current violation is not added again. * The process involves reflection to access private or protected properties of violation objects. * - * @param mixed $value The value that triggered the violation. - * @param Constraint $constraint The constraint containing the validation details. + * @param mixed $value The value that triggered the violation. + * @param Constraint $constraint The constraint containing the validation details. * */ - private function addViolation($value, Constraint $constraint): void + private function addViolation(mixed $value, Constraint $constraint): void { /** @var ConstraintViolationBuilder $buildViolation */ $buildViolation = $this->context->buildViolation($constraint->message) diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php b/src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php new file mode 100644 index 000000000..73234c86e --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php @@ -0,0 +1,21 @@ +context->getViolations(); + if (count($availableViolations) > 0) { + //already violations given, currently no more needed to check + + return; + } + + $bomEntries = []; + + if ($this->context->getRoot() instanceof Form && $this->context->getRoot()->has('bom_entries')) { + $bomEntries = $this->context->getRoot()->get('bom_entries')->getData(); + $bomEntries = is_array($bomEntries) ? $bomEntries : iterator_to_array($bomEntries); + } elseif ($this->context->getRoot() instanceof Assembly) { + $bomEntries = $value->getBomEntries()->toArray(); + } + + $relevantEntries = []; + + foreach ($bomEntries as $bomEntry) { + if ($bomEntry->getReferencedAssembly() !== null) { + $relevantEntries[$bomEntry->getId()] = $bomEntry; + } + } + + foreach ($relevantEntries as $bomEntry) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + if ($bomEntry->getAssembly()->getParent()?->getId() === $referencedAssembly->getParent()?->getId()) { + //Save on the same assembly level + continue; + } elseif ($this->isInvalidBomEntry($referencedAssembly, $bomEntry->getAssembly())) { + $this->addViolation($value, $constraint); + } + } + } + + /** + * Determines whether a Bill of Materials (BOM) entry is invalid based on the relationship + * between the current assembly and the parent assembly. + * + * @param Assembly|null $currentAssembly The current assembly being analyzed. Null indicates no assembly is referenced. + * @param Assembly $parentAssembly The parent assembly to check against the current assembly. + * + * @return bool Returns + */ + private function isInvalidBomEntry(?Assembly $currentAssembly, Assembly $parentAssembly): bool + { + //No assembly referenced -> no problems + if ($currentAssembly === null) { + return false; + } + + //Check: is the current assembly a descendant of the parent assembly? + if ($currentAssembly->isChildOf($parentAssembly)) { + return true; + } + + //Recursive check: Analyze the current assembly list + foreach ($currentAssembly->getBomEntries() as $bomEntry) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + if ($this->isInvalidBomEntry($referencedAssembly, $parentAssembly)) { + return true; + } + } + + return false; + + } + + private function isOnSameLevel(Assembly $assembly1, Assembly $assembly2): bool + { + $parent1 = $assembly1->getParent(); + $parent2 = $assembly2->getParent(); + + if ($parent1 === null || $parent2 === null) { + return false; + } + + // Beide Assemblies teilen denselben Parent + return $parent1 !== null && $parent2 !== null && $parent1->getId() === $parent2->getId(); + } + + /** + * Adds a violation to the current context if it hasn’t already been added. + * + * This method checks whether a violation with the same property path as the current violation + * already exists in the context. If such a violation is found, the current violation is not added again. + * The process involves reflection to access private or protected properties of violation objects. + * + * @param mixed $value The value that triggered the violation. + * @param Constraint $constraint The constraint containing the validation details. + * + */ + private function addViolation($value, Constraint $constraint): void + { + /** @var ConstraintViolationBuilder $buildViolation */ + $buildViolation = $this->context->buildViolation($constraint->message) + ->setParameter('%name%', $value->getName()); + + $alreadyAdded = false; + + try { + $reflectionClass = new ReflectionClass($buildViolation); + $property = $reflectionClass->getProperty('propertyPath'); + $propertyPath = $property->getValue($buildViolation); + + $availableViolations = $this->context->getViolations(); + + foreach ($availableViolations as $tmpViolation) { + $tmpReflectionClass = new ReflectionClass($tmpViolation); + $tmpProperty = $tmpReflectionClass->getProperty('propertyPath'); + $tmpPropertyPath = $tmpProperty->getValue($tmpViolation); + + if ($tmpPropertyPath === $propertyPath) { + $alreadyAdded = true; + } + } + } catch (\ReflectionException) { + } + + if (!$alreadyAdded) { + $buildViolation->addViolation(); + } + } +} \ No newline at end of file diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index b99fe349d..838f47450 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -407,6 +407,12 @@ Byl zjištěn cyklus: Sestava "%name%" nepřímo odkazuje sama na sebe. + + + assembly.bom_entry.invalid_child_entry + Sestava nesmí ve svém seznamu materiálů (BOM) odkazovat na podskupinu, která je součástí její vlastní hierarchie. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 24fa330af..f30dd2116 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -377,6 +377,12 @@ En cyklus blev opdaget: Samlingen "%name%" refererer indirekte til sig selv. + + + assembly.bom_entry.invalid_child_entry + En samling må ikke referere til en undergruppe fra sin egen hierarki i BOM-listerne. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index fa84354b9..645d72ed1 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -401,6 +401,12 @@ Ein Zyklus wurde entdeckt: Die Baugruppe "%name%" referenziert sich indirekt selbst. + + + assembly.bom_entry.invalid_child_entry + Eine Baugruppe darf keine Unterbaugruppe aus seiner eigenen Hierarchie in den BOM-Einträgen referenzieren. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index bc9b0947e..9464f2886 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -43,6 +43,12 @@ Εντοπίστηκε κύκλος: Η συναρμολόγηση "%name%" αναφέρεται έμμεσα στον εαυτό της. + + + assembly.bom_entry.invalid_child_entry + Μία συναρμολόγηση δεν πρέπει να αναφέρεται σε μία υποσυναρμολόγηση από την ίδια την ιεραρχία της στη λίστα BOM. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 7d9beb4ec..53ad4cde6 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -401,6 +401,12 @@ A cycle was detected: the assembly "%name%" indirectly references itself. + + + assembly.bom_entry.invalid_child_entry + An assembly must not reference a subassembly from its own hierarchy in the BOM entries. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index 1c9c53029..440468295 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -233,6 +233,12 @@ Un cycle a été détecté : L'assemblage "%name%" se réfère indirectement à lui-même. + + + assembly.bom_entry.invalid_child_entry + Un assemblage ne doit pas référencer un sous-assemblage de sa propre hiérarchie dans les entrées de la nomenclature (BOM). + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 89d470e7e..485cb0e2f 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -395,6 +395,12 @@ Otkriven je ciklus: Sklop "%name%" neizravno referencira samog sebe. + + + assembly.bom_entry.invalid_child_entry + Sklop ne smije referencirati podsklop iz vlastite hijerarhije u unosima BOM-a. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index e9b528bb7..74d3969f7 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -395,6 +395,12 @@ È stato rilevato un ciclo: L'assemblaggio "%name%" fa riferimento indirettamente a sé stesso. + + + assembly.bom_entry.invalid_child_entry + Un assemblaggio non deve fare riferimento a un sottoassemblaggio nella propria gerarchia nelle voci della distinta base (BOM). + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index 80ec65ff2..f9f8a54f8 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -233,6 +233,12 @@ 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + + assembly.bom_entry.invalid_child_entry + アセンブリは、BOMエントリで自身の階層内のサブアセンブリを参照してはいけません。 + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 5df01cd6a..9916178ca 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -395,6 +395,12 @@ 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + + assembly.bom_entry.invalid_child_entry + Zespół nie może odwoływać się do podzespołu w swojej własnej hierarchii w wpisach BOM. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 8bf08ab3c..b8029e47f 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -395,6 +395,12 @@ Обнаружен цикл: Сборка «%name%» косвенно ссылается на саму себя. + + + assembly.bom_entry.invalid_child_entry + Сборка не должна ссылаться на подсборку внутри своей собственной иерархии в записях спецификации (BOM). + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 87c507c19..6e4fc0568 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -383,6 +383,12 @@ 检测到循环:装配体“%name%”间接引用了其自身。 + + + assembly.bom_entry.invalid_child_entry + Сборка не должна ссылаться на подсборку внутри своей собственной иерархии в записях спецификации (BOM). + + assembly.bom_entry.project_already_in_bom From eb6e5905ee7d4fbf960df0064b3a68a57a4e3326 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 24 Jul 2025 10:31:19 +0200 Subject: [PATCH 30/83] Tabs und BOMImporter: Verbesserte Anzeige und Validierung Die Standardanzeige des Tabs "Details" wurde korrigiert. Im BOMImporter wurden nichtnumerische Spalten kategorisch ausgeschlossen und eine Validation-message angepasst. --- src/Services/ImportExportSystem/BOMImporter.php | 4 ++-- templates/assemblies/info/info.html.twig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 6a3528117..034bebaaa 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -517,7 +517,7 @@ function parseCsv(Project|Assembly $importObject, string $csvData): ImporterResu //If there is no value, skip if (isset($values[$index]) && $values[$index] !== '') { //Check whether the value is numerical - if (is_numeric($values[$index])) { + if (is_numeric($values[$index]) && !in_array($column, ['name','description','manufacturer','designator'])) { //Convert to integer or float $temp = (str_contains($values[$index], '.')) ? floatval($values[$index]) @@ -548,7 +548,7 @@ function parseCsv(Project|Assembly $importObject, string $csvData): ImporterResu if (isset($entry['name']) && !is_string($entry['name'])) { $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.csv.parameter.string.notEmpty', + 'validator.bom_importer.json_csv.parameter.string.notEmpty', "row[$key].name", $entry['name'] )); diff --git a/templates/assemblies/info/info.html.twig b/templates/assemblies/info/info.html.twig index 098173861..5a419e364 100644 --- a/templates/assemblies/info/info.html.twig +++ b/templates/assemblies/info/info.html.twig @@ -98,10 +98,10 @@
    -
    +
    {% include "assemblies/info/_info.html.twig" %}
    -
    +
    {% include "assemblies/info/_part.html.twig" %}
    From c08c2f0b43d4056741654bad85604a7f446c0f65 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 28 Jul 2025 10:33:30 +0200 Subject: [PATCH 31/83] =?UTF-8?q?BOMImporter=20und=20AssemblyBomEntriesDat?= =?UTF-8?q?aTable:=20Mountnames=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Verarbeitung der Designators im BOMImporter wurde ergänzt, um Mountnames zu setzen. Zudem wurden neue Spalten wie Kategorie, Footprint, Hersteller und Mountnames in der AssemblyBomEntriesDataTable hinzugefügt. Dies verbessert die Darstellung und Handhabung von Bauteildaten in der Stücklisten-Ansicht. --- .../AssemblyBomEntriesDataTable.php | 49 +++++++++++++++++++ .../ImportExportSystem/BOMImporter.php | 9 ++++ 2 files changed, 58 insertions(+) diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php index b2c3e118d..e506dccd2 100644 --- a/src/DataTables/AssemblyBomEntriesDataTable.php +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -22,6 +22,7 @@ */ namespace App\DataTables; +use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; use App\DataTables\Helpers\AssemblyDataTableHelper; @@ -146,6 +147,54 @@ public function configure(DataTable $dataTable, array $options): void return $context->getComment(); }, ]) + ->add('category', EntityColumn::class, [ + 'label' => $this->translator->trans('part.table.category'), + 'property' => 'part.category', + 'orderField' => 'NATSORT(category.name)', + ]) + ->add('footprint', EntityColumn::class, [ + 'property' => 'part.footprint', + 'label' => $this->translator->trans('part.table.footprint'), + 'orderField' => 'NATSORT(footprint.name)', + ]) + ->add('manufacturer', EntityColumn::class, [ + 'property' => 'part.manufacturer', + 'label' => $this->translator->trans('part.table.manufacturer'), + 'orderField' => 'NATSORT(manufacturer.name)', + ]) + ->add('mountnames', TextColumn::class, [ + 'label' => 'assembly.bom.mountnames', + 'render' => function ($value, AssemblyBOMEntry $context) { + $html = ''; + + foreach (explode(',', $context->getMountnames()) as $mountname) { + $html .= sprintf('%s ', htmlspecialchars($mountname)); + } + return $html; + }, + ]) + ->add('instockAmount', TextColumn::class, [ + 'label' => 'assembly.bom.instockAmount', + 'visible' => false, + 'render' => function ($value, AssemblyBOMEntry $context) { + if ($context->getPart() !== null) { + return $this->partDataTableHelper->renderAmount($context->getPart()); + } + + return ''; + } + ]) + ->add('storageLocations', TextColumn::class, [ + 'label' => 'part.table.storeLocations', + 'visible' => false, + 'render' => function ($value, AssemblyBOMEntry $context) { + if ($context->getPart() !== null) { + return $this->partDataTableHelper->renderStorageLocations($context->getPart()); + } + + return ''; + } + ]) ->add('addedDate', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.addedDate'), ]) diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 034bebaaa..1a57e1f90 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -563,6 +563,11 @@ function parseCsv(Project|Assembly $importObject, string $csvData): ImporterResu $this->processPart($importObject, $entry, $result, $key, self::IMPORT_TYPE_CSV); } else { $bomEntry = $this->getOrCreateBomEntry($importObject, $entry['name'] ?? null); + + if (isset($entry['designator'])) { + $bomEntry->setMountnames(trim($entry['designator']) === '' ? '' : trim($entry['designator'])); + } + $bomEntry->setQuantity((float) $entry['quantity'] ?? 0); $result->addBomEntry($bomEntry); @@ -851,6 +856,10 @@ private function processPart(Project|Assembly $importObject, array $entry, Impor $bomEntry->setName(null); } + if (isset($entry['designator'])) { + $bomEntry->setMountnames(trim($entry['designator']) === '' ? '' : trim($entry['designator'])); + } + $bomEntry->setPart($part); $result->addBomEntry($bomEntry); From 1c45054967d30f950afb0e39f619e8ca50ae6c1c Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 8 Sep 2025 13:32:34 +0200 Subject: [PATCH 32/83] =?UTF-8?q?F=C3=BCge=20Option=20f=C3=BCr=20lesbares?= =?UTF-8?q?=20CSV=20beim=20Export=20hinzu=20(APS-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elements/toggle_visibility_controller.js | 52 ++++ .../AssemblySystem/AssemblyBOMEntry.php | 6 +- .../ImportExportSystem/EntityExporter.php | 291 +++++++++++++++++- templates/admin/_export_form.html.twig | 19 +- translations/messages.cs.xlf | 6 + translations/messages.da.xlf | 6 + translations/messages.de.xlf | 6 + translations/messages.el.xlf | 6 + translations/messages.en.xlf | 6 + translations/messages.es.xlf | 6 + translations/messages.fr.xlf | 6 + translations/messages.it.xlf | 6 + translations/messages.ja.xlf | 6 + translations/messages.nl.xlf | 6 + translations/messages.pl.xlf | 6 + translations/messages.ru.xlf | 6 + translations/messages.zh.xlf | 6 + 17 files changed, 432 insertions(+), 14 deletions(-) create mode 100644 assets/controllers/elements/toggle_visibility_controller.js diff --git a/assets/controllers/elements/toggle_visibility_controller.js b/assets/controllers/elements/toggle_visibility_controller.js new file mode 100644 index 000000000..4600dfb29 --- /dev/null +++ b/assets/controllers/elements/toggle_visibility_controller.js @@ -0,0 +1,52 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + + static values = { + classes: Array + }; + + connect() { + this.readableCheckbox = this.element.querySelector("#readable"); + + if (!this.readableCheckbox) { + return; + } + + // Apply the initial visibility state based on the checkbox being checked or not + this.toggleContainers(this.readableCheckbox.checked); + + // Add a change event listener to the 'readable' checkbox + this.readableCheckbox.addEventListener("change", (event) => { + // Toggle container visibility when the checkbox value changes + this.toggleContainers(event.target.checked); + }); + } + + /** + * Toggles the visibility of containers based on the checkbox state. + * Hides specified containers if the checkbox is checked and shows them otherwise. + * + * @param {boolean} isChecked - The current state of the checkbox: + * true if checked (hide elements), false if unchecked (show them). + */ + toggleContainers(isChecked) { + if (!Array.isArray(this.classesValue) || this.classesValue.length === 0) { + return; + } + + this.classesValue.forEach((cssClass) => { + const elements = document.querySelectorAll(`.${cssClass}`); + + if (!elements.length) { + return; + } + + // Update the visibility for each selected element + elements.forEach((element) => { + // If the checkbox is checked, hide the container; otherwise, show it + element.style.display = isChecked ? "none" : ""; + }); + }); + } +} \ No newline at end of file diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 7d54fe684..9bca209d0 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -124,7 +124,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt */ #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'bom_entries')] #[ORM\JoinColumn(name: 'id_assembly', nullable: true)] - #[Groups(['bom_entry:read', 'bom_entry:write', ])] + #[Groups(['bom_entry:read', 'bom_entry:write'])] protected ?Assembly $assembly = null; /** @@ -146,7 +146,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt #[AssemblyInvalidBomEntry] #[ORM\ManyToOne(targetEntity: Assembly::class)] #[ORM\JoinColumn(name: 'id_referenced_assembly', nullable: true, onDelete: 'SET NULL')] - #[Groups(['bom_entry:read', 'bom_entry:write', ])] + #[Groups(['bom_entry:read', 'bom_entry:write'])] protected ?Assembly $referencedAssembly = null; /** @@ -158,7 +158,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt )] #[ORM\ManyToOne(targetEntity: Project::class)] #[ORM\JoinColumn(name: 'id_project', nullable: true)] - #[Groups(['bom_entry:read', 'bom_entry:write', ])] + #[Groups(['bom_entry:read', 'bom_entry:write'])] protected ?Project $project = null; /** diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 271642dad..1adbcdcbb 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -22,8 +22,19 @@ namespace App\Services\ImportExportSystem; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\LabelSystem\LabelProfile; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\StorageLocation; +use App\Entity\Parts\Supplier; +use App\Entity\PriceInformations\Currency; +use App\Entity\ProjectSystem\Project; use App\Helpers\FilenameSanatizer; use App\Serializer\APIPlatform\SkippableItemNormalizer; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -62,6 +73,9 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setDefault('include_children', false); $resolver->setAllowedTypes('include_children', 'bool'); + + $resolver->setDefault('readable', false); + $resolver->setAllowedTypes('readable', 'bool'); } /** @@ -144,15 +158,50 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities, $entities = [$entities]; } - //Do the serialization with the given options - $serialized_data = $this->exportEntities($entities, $options); + if ($request->get('readable', false)) { + // Map entity classes to export functions + $entityExportMap = [ + AttachmentType::class => fn($entities) => $this->exportReadable($entities, AttachmentType::class), + Category::class => fn($entities) => $this->exportReadable($entities, Category::class), + Project::class => fn($entities) => $this->exportReadable($entities, Project::class), + Assembly::class => fn($entities) => $this->exportReadable($entities, Assembly::class), + Supplier::class => fn($entities) => $this->exportReadable($entities, Supplier::class), + Manufacturer::class => fn($entities) => $this->exportReadable($entities, Manufacturer::class), + StorageLocation::class => fn($entities) => $this->exportReadable($entities, StorageLocation::class), + Footprint::class => fn($entities) => $this->exportReadable($entities, Footprint::class), + Currency::class => fn($entities) => $this->exportReadable($entities, Currency::class), + MeasurementUnit::class => fn($entities) => $this->exportReadable($entities, MeasurementUnit::class), + LabelProfile::class => fn($entities) => $this->exportReadable($entities, LabelProfile::class, false), + ]; + + // Determine the type of the entity + $type = null; + foreach ($entities as $entity) { + $entityClass = get_class($entity); + if (isset($entityExportMap[$entityClass])) { + $type = $entityClass; + break; + } + } + + // Generate the response + $response = isset($entityExportMap[$type]) + ? new Response($entityExportMap[$type]($entities)) + : new Response(''); + + $options['format'] = 'csv'; + $options['level'] = 'readable'; + } else { + //Do the serialization with the given options + $serialized_data = $this->exportEntities($entities, $options); - $response = new Response($serialized_data); + $response = new Response($serialized_data); - //Resolve the format - $optionsResolver = new OptionsResolver(); - $this->configureOptions($optionsResolver); - $options = $optionsResolver->resolve($options); + //Resolve the format + $optionsResolver = new OptionsResolver(); + $this->configureOptions($optionsResolver); + $options = $optionsResolver->resolve($options); + } //Determine the content type for the response @@ -203,4 +252,232 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities, return $response; } + + /** + * Exports data for multiple entity types in a readable CSV format. + * + * @param array $entities The entities to export. + * @param string $type The type of entities ('category', 'project', 'assembly', 'attachmentType', 'supplier'). + * @return string The generated CSV content as a string. + */ + public function exportReadable(array $entities, string $type, bool $isHierarchical = true): string + { + //Define headers and entity-specific processing logic + $defaultProcessEntity = fn($entity, $depth) => [ + 'Id' => $entity->getId(), + 'ParentId' => $entity->getParent()?->getId() ?? '', + 'NameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(), + 'Name' => $entity->getName(), + 'FullName' => $this->getFullName($entity), + ]; + + $config = [ + AttachmentType::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Category::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Project::class => [ + 'header' => [ + 'Id', 'ParentId', 'Type', 'ProjectNameHierarchical', 'ProjectName', 'ProjectFullName', + 'BomQuantity', 'BomPartId', 'BomPartIpn', 'BomPartName', 'BomName', 'BomPartDescription', 'BomMountNames' + ], + 'processEntity' => fn($entity, $depth) => [ + 'ProjectId' => $entity->getId(), + 'ParentProjectId' => $entity->getParent()?->getId() ?? '', + 'Type' => 'project', + 'ProjectNameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(), + 'ProjectName' => $entity->getName(), + 'ProjectFullName' => $this->getFullName($entity), + 'BomQuantity' => '-', + 'BomPartId' => '-', + 'BomPartIpn' => '-', + 'BomPartName' => '-', + 'BomName' => '-', + 'BomPartDescription' => '-', + 'BomMountNames' => '-', + ], + 'processBomEntries' => fn($entity, $depth) => array_map(fn($bomEntry) => [ + 'Id' => $entity->getId(), + 'ParentId' => '', + 'Type' => 'project_bom_entry', + 'ProjectNameHierarchical' => str_repeat('--', $depth) . '> ' . $entity->getName(), + 'ProjectName' => $entity->getName(), + 'ProjectFullName' => $this->getFullName($entity), + 'BomQuantity' => $bomEntry->getQuantity() ?? '', + 'BomPartId' => $bomEntry->getPart()?->getId() ?? '', + 'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '', + 'BomPartName' => $bomEntry->getPart()?->getName() ?? '', + 'BomName' => $bomEntry->getName() ?? '', + 'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '', + 'BomMountNames' => $bomEntry->getMountNames(), + ], $entity->getBomEntries()->toArray()), + ], + Assembly::class => [ + 'header' => [ + 'Id', 'ParentId', 'Type', 'AssemblyIpn', 'AssemblyNameHierarchical', 'AssemblyName', + 'AssemblyFullName', 'BomQuantity', 'BomPartId', 'BomPartIpn', 'BomPartName', 'BomName', 'BomPartDescription', + 'BomMountNames', 'BomReferencedAssemblyId', 'BomReferencedAssemblyIpn', 'BomReferencedAssemblyFullName' + ], + 'processEntity' => fn($entity, $depth) => [ + 'Id' => $entity->getId(), + 'ParentId' => $entity->getParent()?->getId() ?? '', + 'Type' => 'assembly', + 'AssemblyIpn' => $entity->getIpn(), + 'AssemblyNameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(), + 'AssemblyName' => $entity->getName(), + 'AssemblyFullName' => $this->getFullName($entity), + 'BomQuantity' => '-', + 'BomPartId' => '-', + 'BomPartIpn' => '-', + 'BomPartName' => '-', + 'BomName' => '-', + 'BomPartDescription' => '-', + 'BomMountNames' => '-', + 'BomReferencedAssemblyId' => '-', + 'BomReferencedAssemblyIpn' => '-', + 'BomReferencedAssemblyFullName' => '-', + ], + 'processBomEntries' => fn($entity, $depth) => array_map(fn($bomEntry) => [ + 'Id' => $entity->getId(), + 'ParentId' => '', + 'Type' => 'assembly_bom_entry', + 'AssemblyIpn' => $entity->getIpn(), + 'AssemblyNameHierarchical' => str_repeat('--', $depth) . '> ' . $entity->getName(), + 'AssemblyName' => $entity->getName(), + 'AssemblyFullName' => $this->getFullName($entity), + 'BomQuantity' => $bomEntry->getQuantity() ?? '', + 'BomPartId' => $bomEntry->getPart()?->getId() ?? '', + 'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '', + 'BomPartName' => $bomEntry->getPart()?->getName() ?? '', + 'BomName' => $bomEntry->getName() ?? '', + 'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '', + 'BomMountNames' => $bomEntry->getMountNames(), + 'BomReferencedAssemblyId' => $bomEntry->getReferencedAssembly()?->getId() ?? '', + 'BomReferencedAssemblyIpn' => $bomEntry->getReferencedAssembly()?->getIpn() ?? '', + 'BomReferencedAssemblyFullName' => $this->getFullName($bomEntry->getReferencedAssembly() ?? null), + ], $entity->getBomEntries()->toArray()), + ], + Supplier::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Manufacturer::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + StorageLocation::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Footprint::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Currency::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + MeasurementUnit::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + LabelProfile::class => [ + 'header' => ['Id', 'SupportedElement', 'Name'], + 'processEntity' => fn(LabelProfile $entity, $depth) => [ + 'Id' => $entity->getId(), + 'SupportedElement' => $entity->getOptions()->getSupportedElement()->name, + 'Name' => $entity->getName(), + ], + ], + ]; + + //Get configuration for the entity type + $entityConfig = $config[$type] ?? null; + + if (!$entityConfig) { + return ''; + } + + //Initialize CSV data with the header + $csvData = []; + $csvData[] = $entityConfig['header']; + + $relevantEntities = $entities; + + if ($isHierarchical) { + //Filter root entities (those without parents) + $relevantEntities = array_filter($entities, fn($entity) => $entity->getParent() === null); + + if (count($relevantEntities) === 0 && count($entities) > 0) { + //If no root entities are found, then we need to add all entities + + $relevantEntities = $entities; + } + } + + //Sort root entities alphabetically by `name` + usort($relevantEntities, fn($a, $b) => strnatcasecmp($a->getName(), $b->getName())); + + //Recursive function to process an entity and its children + $processEntity = function ($entity, &$csvData, $depth = 0) use (&$processEntity, $entityConfig, $isHierarchical) { + //Add main entity data to CSV + $csvData[] = $entityConfig['processEntity']($entity, $depth); + + //Process BOM entries if applicable + if (isset($entityConfig['processBomEntries'])) { + $bomRows = $entityConfig['processBomEntries']($entity, $depth); + foreach ($bomRows as $bomRow) { + $csvData[] = $bomRow; + } + } + + if ($isHierarchical) { + //Retrieve children, sort alphabetically, then process them + $children = $entity->getChildren()->toArray(); + usort($children, fn($a, $b) => strnatcasecmp($a->getName(), $b->getName())); + foreach ($children as $childEntity) { + $processEntity($childEntity, $csvData, $depth + 1); + } + } + }; + + //Start processing with root entities + foreach ($relevantEntities as $rootEntity) { + $processEntity($rootEntity, $csvData); + } + + //Generate CSV string + $output = ''; + foreach ($csvData as $line) { + $output .= implode(';', $line) . "\n"; // Use a semicolon as the delimiter + } + + return $output; + } + + /** + * Constructs the full hierarchical name of an object by traversing + * through its parent objects and concatenating their names using + * a specified separator. + * + * @param AttachmentType|Category|Project|Assembly|Supplier|Manufacturer|StorageLocation|Footprint|Currency|MeasurementUnit|LabelProfile|null $object The object whose full name is to be constructed. If null, the result will be an empty string. + * @param string $separator The string used to separate the names of the objects in the full hierarchy. + * + * @return string The full hierarchical name constructed by concatenating the names of the object and its parents. + */ + private function getFullName(AttachmentType|Category|Project|Assembly|Supplier|Manufacturer|StorageLocation|Footprint|Currency|MeasurementUnit|LabelProfile|null $object, string $separator = '->'): string + { + $fullNameParts = []; + + while ($object !== null) { + array_unshift($fullNameParts, $object->getName()); + $object = $object->getParent(); + } + + return implode($separator, $fullNameParts); + } } diff --git a/templates/admin/_export_form.html.twig b/templates/admin/_export_form.html.twig index 07b00d43c..527518640 100644 --- a/templates/admin/_export_form.html.twig +++ b/templates/admin/_export_form.html.twig @@ -1,6 +1,6 @@ -
    + -
    +
    @@ -23,7 +23,7 @@
    -
    +
    @@ -34,6 +34,17 @@
    +
    +
    +
    + + +
    +
    +
    +
    diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index c838f07b8..778b34614 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -351,6 +351,12 @@ Exportovat všechny prvky + + + export.readable + Čitelné CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 07f6c1b37..8e66fdf4d 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -351,6 +351,12 @@ Eksportér alle elementer + + + export.readable + Læsbar CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 7e2c84385..07c597478 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1004,6 +1004,12 @@ Subelemente werden beim Löschen nach oben verschoben. Unterelemente auch exportieren + + + export.readable + Lesbares CSV + + Part-DB1\templates\AdminPages\_export_form.html.twig:39 diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 07143e5b7..6f4e3f686 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -228,6 +228,12 @@ Εξαγωγή όλων των στοιχείων + + + export.readable + Αναγνώσιμο CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index df4c2aed7..f5d4dc7ae 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -351,6 +351,12 @@ Export all elements + + + export.readable + Readable CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 1dfdf1dd6..5927b8ec9 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -351,6 +351,12 @@ Exportar todos los elementos + + + export.readable + CSV legible + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 5c055aaef..bb7128e56 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -320,6 +320,12 @@ Exporter tous les éléments + + + export.readable + CSV lisible + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index b018468bb..29a9d64c7 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -351,6 +351,12 @@ Esportare tutti gli elementi + + + export.readable + CSV leggibile + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 4d1c82edf..95b9bc815 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -320,6 +320,12 @@ すべてエクスポートする + + + export.readable + 読みやすいCSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 822191818..5b8fe89f3 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -351,6 +351,12 @@ Exporteer alle elementen + + + export.readable + Leesbare CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 6a7e5d591..3be5447fe 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -351,6 +351,12 @@ Eksportuj wszystkie elementy + + + export.readable + Czytelny CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 2add6347a..e7b4ef938 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -351,6 +351,12 @@ Экспортировать всё + + + export.readable + Читаемый CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index cd6b34b2c..8d445de4b 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -351,6 +351,12 @@ 导出所有元素 + + + export.readable + 可读的 CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 From d4fe88e4c348a92f5a611eb6afe5940739f28b29 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 10 Sep 2025 13:56:36 +0200 Subject: [PATCH 33/83] Rebase auf Part-DB v2.1.2 --- .env | 13 -- config/parameters.yaml | 6 - config/services.yaml | 12 -- migrations/Version20250304081039.php | 128 ++++++++---------- migrations/Version20250627130848.php | 2 +- migrations/Version20250910113423.php | 51 +++++++ src/Controller/AssemblyController.php | 1 + src/Controller/ProjectController.php | 6 +- .../AssemblyBomEntriesDataTable.php | 25 ++-- src/DataTables/AssemblyDataTable.php | 6 +- .../AssemblySystem/AssemblyBOMEntry.php | 12 -- src/Form/AdminPages/AssemblyAdminForm.php | 5 +- src/Form/AdminPages/BaseEntityAdminForm.php | 5 +- .../ImportExportSystem/BOMImporter.php | 37 +++-- .../AssemblyBomTableColumns.php | 46 +++++++ .../BehaviorSettings/AssemblyTableColumns.php | 49 +++++++ .../BehaviorSettings/TableSettings.php | 64 ++++++++- .../MiscSettings/AssemblySettings.php | 45 ++++++ src/Settings/MiscSettings/MiscSettings.php | 5 +- translations/messages.cs.xlf | 78 +++++++++++ translations/messages.da.xlf | 42 ++++++ translations/messages.de.xlf | 78 +++++++++++ translations/messages.el.xlf | 42 ++++++ translations/messages.en.xlf | 78 +++++++++++ translations/messages.es.xlf | 42 ++++++ translations/messages.fr.xlf | 42 ++++++ translations/messages.it.xlf | 42 ++++++ translations/messages.ja.xlf | 42 ++++++ translations/messages.nl.xlf | 42 ++++++ translations/messages.pl.xlf | 42 ++++++ translations/messages.ru.xlf | 46 ++++++- translations/messages.zh.xlf | 42 ++++++ translations/validators.cs.xlf | 6 - 33 files changed, 1020 insertions(+), 162 deletions(-) create mode 100644 migrations/Version20250910113423.php create mode 100644 src/Settings/BehaviorSettings/AssemblyBomTableColumns.php create mode 100644 src/Settings/BehaviorSettings/AssemblyTableColumns.php create mode 100644 src/Settings/MiscSettings/AssemblySettings.php diff --git a/.env b/.env index c530f312c..73b7b350d 100644 --- a/.env +++ b/.env @@ -35,9 +35,6 @@ DATABASE_EMULATE_NATURAL_SORT=0 # This must end with a slash! DEFAULT_URI="https://partdb.changeme.invalid/" -# Use an %%ipn%% placeholder in the name of a assembly. Placeholder is replaced with the ipn input while saving. -CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME=0 - ################################################################################### # Email settings ################################################################################### @@ -63,16 +60,6 @@ ERROR_PAGE_ADMIN_EMAIL='' # If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them... ERROR_PAGE_SHOW_HELP=1 -################################################################################## -# Part table settings -################################################################################## - -# Configure which columns will be visible by default in the specific table (and in which order). -# This is a comma separated list of column names. See documentation for available values. -TABLE_PARTS_DEFAULT_COLUMNS=name,description,category,footprint,manufacturer,storage_location,amount -TABLE_ASSEMBLIES_DEFAULT_COLUMNS=id,ipn,name,description,referencedAssemblies,edit -TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS=quantity,id,ipn,name,description - ################################################################################### # SAML Single sign on-settings ################################################################################### diff --git a/config/parameters.yaml b/config/parameters.yaml index 9a590ec87..8bab49589 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -59,12 +59,6 @@ parameters: ###################################################################################################################### partdb.saml.enabled: '%env(bool:SAML_ENABLED)%' # If this is set to true, SAML authentication is enabled - ###################################################################################################################### - # Table settings - ###################################################################################################################### - partdb.table.assemblies.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_DEFAULT_COLUMNS)%' # The default columns in assembly tables and their order - partdb.table.assemblies_bom.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS)%' # The default columns in assembly bom tables and their order - ###################################################################################################################### # Miscellaneous ###################################################################################################################### diff --git a/config/services.yaml b/config/services.yaml index 7f0b5897f..e354ca849 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -164,10 +164,6 @@ services: arguments: $saml_enabled: '%partdb.saml.enabled%' - App\Form\AdminPages\AssemblyAdminForm: - arguments: - $useAssemblyIpnPlaceholder: '%partdb.create_assembly_use_ipn_placeholder_in_name%' - App\Validator\Constraints\AssemblySystem\AssemblyCycleValidator: tags: [ 'validator.constraint_validator' ] @@ -177,12 +173,6 @@ services: App\DataTables\Helpers\ColumnSortHelper: shared: false # Service has a state so not share it between different tables - App\DataTables\AssemblyDataTable: - arguments: - $visible_columns: '%partdb.table.assemblies.default_columns%' - App\DataTables\AssemblyBomEntriesDataTable: - arguments: - $visible_columns: '%partdb.table.assemblies_bom.default_columns%' #################################################################################################################### # Label system @@ -206,8 +196,6 @@ services: #################################################################################################################### App\Services\Trees\TreeViewGenerator: arguments: - $rootNodeExpandedByDefault: '%partdb.sidebar.root_expanded%' - $rootNodeEnabled: '%partdb.sidebar.root_node_enable%' $dataSourceSynonyms: '%partdb.data_sources.synonyms%' App\Services\Trees\ToolsTreeBuilder: arguments: diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php index ccdb24ac8..0c64e2507 100644 --- a/migrations/Version20250304081039.php +++ b/migrations/Version20250304081039.php @@ -18,42 +18,42 @@ public function mySQLUp(Schema $schema): void { $this->addSql(<<<'SQL' CREATE TABLE assemblies ( - id INT AUTO_INCREMENT NOT NULL, - parent_id INT DEFAULT NULL, - id_preview_attachment INT DEFAULT NULL, - name VARCHAR(255) NOT NULL, - comment LONGTEXT NOT NULL, - not_selectable TINYINT(1) NOT NULL, - alternative_names LONGTEXT DEFAULT NULL, - order_quantity INT NOT NULL, - status VARCHAR(64) DEFAULT NULL, - order_only_missing_parts TINYINT(1) NOT NULL, - description LONGTEXT NOT NULL, - last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - INDEX IDX_5F3832C0727ACA70 (parent_id), - INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), + id INT AUTO_INCREMENT NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, + name VARCHAR(255) NOT NULL, + comment LONGTEXT NOT NULL, + not_selectable TINYINT(1) NOT NULL, + alternative_names LONGTEXT DEFAULT NULL, + order_quantity INT NOT NULL, + status VARCHAR(64) DEFAULT NULL, + order_only_missing_parts TINYINT(1) NOT NULL, + description LONGTEXT NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + INDEX IDX_5F3832C0727ACA70 (parent_id), + INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), PRIMARY KEY(id) ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` SQL); $this->addSql(<<<'SQL' CREATE TABLE assembly_bom_entries ( - id INT AUTO_INCREMENT NOT NULL, - id_assembly INT DEFAULT NULL, - id_part INT DEFAULT NULL, - id_project INT DEFAULT NULL, - quantity DOUBLE PRECISION NOT NULL, - mountnames LONGTEXT NOT NULL, - name VARCHAR(255) DEFAULT NULL, - comment LONGTEXT NOT NULL, - price NUMERIC(11, 5) DEFAULT NULL, - price_currency_id INT DEFAULT NULL, - last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - INDEX IDX_8C74887E2F180363 (id_assembly), - INDEX IDX_8C74887EC22F6CC4 (id_part), - INDEX IDX_8C74887EF12E799E (id_project), - INDEX IDX_8C74887E3FFDCD60 (price_currency_id), + id INT AUTO_INCREMENT NOT NULL, + id_assembly INT DEFAULT NULL, + id_part INT DEFAULT NULL, + id_project INT DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames LONGTEXT NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment LONGTEXT NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + price_currency_id INT DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + INDEX IDX_8C74887E2F180363 (id_assembly), + INDEX IDX_8C74887EC22F6CC4 (id_part), + INDEX IDX_8C74887EF12E799E (id_project), + INDEX IDX_8C74887E3FFDCD60 (price_currency_id), PRIMARY KEY(id) ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` SQL); @@ -109,7 +109,7 @@ public function sqLiteUp(Schema $schema): void { $this->addSql(<<<'SQL' CREATE TABLE assemblies ( - id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, order_quantity INTEGER NOT NULL, @@ -120,7 +120,7 @@ public function sqLiteUp(Schema $schema): void last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, status VARCHAR(64) DEFAULT NULL, - ipn VARCHAR(100) DEFAULT NULL, + ipn VARCHAR(100) DEFAULT NULL, description CLOB NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, @@ -145,7 +145,6 @@ public function sqLiteUp(Schema $schema): void id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_assembly INTEGER DEFAULT NULL, id_part INTEGER DEFAULT NULL, - id_project INTEGER DEFAULT NULL, id_referenced_assembly INTEGER DEFAULT NULL, price_currency_id INTEGER DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, @@ -157,9 +156,8 @@ public function sqLiteUp(Schema $schema): void datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_8C74887E4AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_8C74887E22522999 FOREIGN KEY (id_referenced_assembly) REFERENCES assemblies (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + CONSTRAINT FK_8C74887E22522999 FOREIGN KEY (id_referenced_assembly) REFERENCES assemblies (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE ) SQL); $this->addSql(<<<'SQL' @@ -168,9 +166,6 @@ public function sqLiteUp(Schema $schema): void $this->addSql(<<<'SQL' CREATE INDEX IDX_8C74887EC22F6CC4 ON assembly_bom_entries (id_part) SQL); - $this->addSql(<<<'SQL' - CREATE INDEX IDX_8C74887EF12E799E ON assembly_bom_entries (id_project) - SQL); $this->addSql(<<<'SQL' CREATE INDEX IDX_8C74887E22522999 ON assembly_bom_entries (id_referenced_assembly) SQL); @@ -193,19 +188,19 @@ public function postgreSQLUp(Schema $schema): void { $this->addSql(<<<'SQL' CREATE TABLE assemblies ( - id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, - name VARCHAR(255) NOT NULL, - last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, - datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, - comment TEXT NOT NULL, - not_selectable BOOLEAN NOT NULL, - alternative_names TEXT DEFAULT NULL, - order_quantity INT NOT NULL, - status VARCHAR(64) DEFAULT NULL, - order_only_missing_parts BOOLEAN NOT NULL, - description TEXT NOT NULL, - parent_id INT DEFAULT NULL, - id_preview_attachment INT DEFAULT NULL, + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + comment TEXT NOT NULL, + not_selectable BOOLEAN NOT NULL, + alternative_names TEXT DEFAULT NULL, + order_quantity INT NOT NULL, + status VARCHAR(64) DEFAULT NULL, + order_only_missing_parts BOOLEAN NOT NULL, + description TEXT NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id) ) SQL); @@ -217,18 +212,17 @@ public function postgreSQLUp(Schema $schema): void SQL); $this->addSql(<<<'SQL' CREATE TABLE assembly_bom_entries ( - id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, - id_assembly INT DEFAULT NULL, - id_part INT DEFAULT NULL, - id_project INT DEFAULT NULL, - quantity DOUBLE PRECISION NOT NULL, - mountnames TEXT NOT NULL, - name VARCHAR(255) DEFAULT NULL, - comment TEXT NOT NULL, - price NUMERIC(11, 5) DEFAULT NULL, + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + id_assembly INT DEFAULT NULL, + id_part INT DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames TEXT NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment TEXT NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, price_currency_id INT DEFAULT NULL, - last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, - datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY(id) ) SQL); @@ -238,9 +232,6 @@ public function postgreSQLUp(Schema $schema): void $this->addSql(<<<'SQL' CREATE INDEX IDX_8C74887EC22F6CC4 ON assembly_bom_entries (id_part) SQL); - $this->addSql(<<<'SQL' - CREATE INDEX IDX_8C74887EF12E799E ON assembly_bom_entries (id_project) - SQL); $this->addSql(<<<'SQL' CREATE INDEX IDX_8C74887E3FFDCD60 ON assembly_bom_entries (price_currency_id) SQL); @@ -256,9 +247,6 @@ public function postgreSQLUp(Schema $schema): void $this->addSql(<<<'SQL' ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE SQL); - $this->addSql(<<<'SQL' - ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES "projects" (id) NOT DEFERRABLE INITIALLY IMMEDIATE - SQL); $this->addSql(<<<'SQL' ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE SQL); diff --git a/migrations/Version20250627130848.php b/migrations/Version20250627130848.php index 16a6f3185..6223de13b 100644 --- a/migrations/Version20250627130848.php +++ b/migrations/Version20250627130848.php @@ -17,7 +17,7 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { $this->addSql(<<<'SQL' - ALTER TABLE assembly_bom_entries ADD id_referenced_assembly INT DEFAULT NULL AFTER id_project + ALTER TABLE assembly_bom_entries ADD id_referenced_assembly INT DEFAULT NULL AFTER id_part SQL); $this->addSql(<<<'SQL' ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E22522999 FOREIGN KEY (id_referenced_assembly) REFERENCES assemblies (id) ON DELETE SET NULL diff --git a/migrations/Version20250910113423.php b/migrations/Version20250910113423.php new file mode 100644 index 000000000..0e65e4aba --- /dev/null +++ b/migrations/Version20250910113423.php @@ -0,0 +1,51 @@ +addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY `FK_8C74887EF12E799E`'); + $this->addSql('DROP INDEX IDX_8C74887EF12E799E ON assembly_bom_entries'); + $this->addSql('ALTER TABLE assembly_bom_entries DROP id_project'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE assembly_bom_entries ADD id_project INT DEFAULT NULL'); + $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT `FK_8C74887EF12E799E` FOREIGN KEY (id_project) REFERENCES projects (id)'); + $this->addSql('CREATE INDEX IDX_8C74887EF12E799E ON assembly_bom_entries (id_project)'); + } + + public function sqLiteUp(Schema $schema): void + { + //nothing to do. Already removed from AssemblyBOMEntry and Version20250304081039 + } + + public function sqLiteDown(Schema $schema): void + { + //nothing to do. + } + + public function postgreSQLUp(Schema $schema): void + { + //nothing to do. Already removed from AssemblyBOMEntry and Version20250304081039 + } + + public function postgreSQLDown(Schema $schema): void + { + //nothing to do. + } +} diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php index 94e129643..520bc52de 100644 --- a/src/Controller/AssemblyController.php +++ b/src/Controller/AssemblyController.php @@ -216,6 +216,7 @@ public function importBOM(Request $request, EntityManagerInterface $entityManage 'assembly.bom_import.type.json' => 'json', 'assembly.bom_import.type.csv' => 'csv', 'assembly.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew', + 'assembly.bom_import.type.kicad_schematic' => 'kicad_schematic', ] ]); $builder->add('clear_existing_bom', CheckboxType::class, [ diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 2106098c6..e510506f3 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -193,10 +193,6 @@ public function importBOM( } // For PCB imports, proceed directly - $entries = $BOMImporter->importFileIntoProject($form->get('file')->getData(), $project, [ - 'type' => $import_type, - ]); - $importerResult = $BOMImporter->importFileIntoProject($form->get('file')->getData(), $project, [ 'type' => $import_type, ]); @@ -425,7 +421,7 @@ public function importBOMMapFields( } // Import with field mapping and priorities (validation already passed) - $entries = $BOMImporter->stringToBOMEntries($file_content, [ + $entries = $BOMImporter->stringToBOMEntries($project, $file_content, [ 'type' => 'kicad_schematic', 'field_mapping' => $field_mapping, 'field_priorities' => $field_priorities, diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php index e506dccd2..f9baeb869 100644 --- a/src/DataTables/AssemblyBomEntriesDataTable.php +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -34,8 +34,8 @@ use App\Entity\Parts\Part; use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\ProjectSystem\Project; -use App\Services\EntityURLGenerator; use App\Services\Formatters\AmountFormatter; +use App\Settings\BehaviorSettings\TableSettings; use Doctrine\ORM\QueryBuilder; use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; @@ -47,15 +47,14 @@ class AssemblyBomEntriesDataTable implements DataTableTypeInterface { public function __construct( - protected TranslatorInterface $translator, - protected PartDataTableHelper $partDataTableHelper, - protected ProjectDataTableHelper $projectDataTableHelper, - protected AssemblyDataTableHelper $assemblyDataTableHelper, - protected EntityURLGenerator $entityURLGenerator, - protected AmountFormatter $amountFormatter, - private string $visible_columns, - private ColumnSortHelper $csh - ){ + private readonly TranslatorInterface $translator, + private readonly PartDataTableHelper $partDataTableHelper, + private readonly ProjectDataTableHelper $projectDataTableHelper, + private readonly AssemblyDataTableHelper $assemblyDataTableHelper, + private readonly AmountFormatter $amountFormatter, + private readonly ColumnSortHelper $csh, + private readonly TableSettings $tableSettings, + ) { } public function configure(DataTable $dataTable, array $options): void @@ -200,11 +199,11 @@ public function configure(DataTable $dataTable, array $options): void ]) ->add('lastModified', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.lastModified'), - ]) - ; + ]); //Apply the user configured order and visibility and add the columns to the table - $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->visible_columns,"TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS"); + $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->tableSettings->assembliesBomDefaultColumns, + "TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS"); $dataTable->addOrderBy('name'); diff --git a/src/DataTables/AssemblyDataTable.php b/src/DataTables/AssemblyDataTable.php index 745a62b62..aaad2e45e 100644 --- a/src/DataTables/AssemblyDataTable.php +++ b/src/DataTables/AssemblyDataTable.php @@ -34,6 +34,7 @@ use App\Doctrine\Helpers\FieldHelper; use App\Entity\AssemblySystem\Assembly; use App\Services\EntityURLGenerator; +use App\Settings\BehaviorSettings\TableSettings; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\QueryBuilder; use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; @@ -53,8 +54,8 @@ public function __construct( private readonly TranslatorInterface $translator, private readonly AssemblyDataTableHelper $assemblyDataTableHelper, private readonly Security $security, - private readonly string $visible_columns, private readonly ColumnSortHelper $csh, + private readonly TableSettings $tableSettings, ) { } @@ -139,7 +140,8 @@ public function configure(DataTable $dataTable, array $options): void ]); //Apply the user configured order and visibility and add the columns to the table - $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->visible_columns, "TABLE_ASSEMBLIES_DEFAULT_COLUMNS"); + $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->tableSettings->assembliesDefaultColumns, + "TABLE_ASSEMBLIES_DEFAULT_COLUMNS"); $dataTable->addOrderBy('name') ->createAdapter(TwoStepORMAdapter::class, [ diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 9bca209d0..6de6cc3a6 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -149,18 +149,6 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt #[Groups(['bom_entry:read', 'bom_entry:write'])] protected ?Assembly $referencedAssembly = null; - /** - * @var Project|null The associated project - */ - #[Assert\Expression( - '(this.getPart() === null or this.getProject() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))', - message: 'validator.project.bom_entry.only_part_or_assembly_allowed' - )] - #[ORM\ManyToOne(targetEntity: Project::class)] - #[ORM\JoinColumn(name: 'id_project', nullable: true)] - #[Groups(['bom_entry:read', 'bom_entry:write'])] - protected ?Project $project = null; - /** * @var BigDecimal|null The price of this non-part BOM entry */ diff --git a/src/Form/AdminPages/AssemblyAdminForm.php b/src/Form/AdminPages/AssemblyAdminForm.php index 0512f64a4..2c2f3cc48 100644 --- a/src/Form/AdminPages/AssemblyAdminForm.php +++ b/src/Form/AdminPages/AssemblyAdminForm.php @@ -26,6 +26,7 @@ use App\Form\AssemblySystem\AssemblyBOMEntryCollectionType; use App\Form\Type\RichTextEditorType; use App\Services\LogSystem\EventCommentNeededHelper; +use App\Settings\MiscSettings\AssemblySettings; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -36,9 +37,9 @@ class AssemblyAdminForm extends BaseEntityAdminForm public function __construct( protected Security $security, protected EventCommentNeededHelper $eventCommentNeededHelper, - protected bool $useAssemblyIpnPlaceholder = false + protected AssemblySettings $assemblySettings, ) { - parent::__construct($security, $eventCommentNeededHelper, $useAssemblyIpnPlaceholder); + parent::__construct($security, $eventCommentNeededHelper, $assemblySettings); } protected function additionalFormElements(FormBuilderInterface $builder, array $options, AbstractNamedDBElement $entity): void diff --git a/src/Form/AdminPages/BaseEntityAdminForm.php b/src/Form/AdminPages/BaseEntityAdminForm.php index bbc437e3a..5ffd7f4df 100644 --- a/src/Form/AdminPages/BaseEntityAdminForm.php +++ b/src/Form/AdminPages/BaseEntityAdminForm.php @@ -27,6 +27,7 @@ use App\Entity\ProjectSystem\Project; use App\Entity\UserSystem\Group; use App\Services\LogSystem\EventCommentType; +use App\Settings\MiscSettings\AssemblySettings; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; @@ -51,7 +52,7 @@ class BaseEntityAdminForm extends AbstractType public function __construct( protected Security $security, protected EventCommentNeededHelper $eventCommentNeededHelper, - protected bool $useAssemblyIpnPlaceholder = false + protected AssemblySettings $assemblySettings, ) { } @@ -73,7 +74,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('name', TextType::class, [ 'empty_data' => '', 'label' => 'name.label', - 'data' => $is_new && $entity instanceof Assembly && $this->useAssemblyIpnPlaceholder ? '%%ipn%%' : $entity->getName(), + 'data' => $is_new && $entity instanceof Assembly && $this->assemblySettings->useIpnPlaceholderInName ? '%%ipn%%' : $entity->getName(), 'attr' => [ 'placeholder' => 'part.name.placeholder', ], diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 1a57e1f90..5f3310efe 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -63,19 +63,29 @@ class BOMImporter 5 => 'Supplier and ref', ]; + private readonly PartRepository $partRepository; + + private readonly ManufacturerRepository $manufacturerRepository; + + private readonly CategoryRepository $categoryRepository; + + private readonly DBElementRepository $projectBomEntryRepository; + + private readonly DBElementRepository $assemblyBomEntryRepository; + private string $jsonRoot = ''; public function __construct( private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, private readonly BOMValidationService $validationService, - private readonly PartRepository $partRepository, - private readonly ManufacturerRepository $manufacturerRepository, - private readonly CategoryRepository $categoryRepository, - private readonly DBElementRepository $projectBOMEntryRepository, - private readonly DBElementRepository $assemblyBOMEntryRepositor, private readonly TranslatorInterface $translator ) { + $this->partRepository = $this->entityManager->getRepository(Part::class); + $this->manufacturerRepository = $this->entityManager->getRepository(Manufacturer::class); + $this->categoryRepository = $this->entityManager->getRepository(Category::class); + $this->projectBomEntryRepository = $this->entityManager->getRepository(ProjectBOMEntry::class); + $this->assemblyBomEntryRepository = $this->entityManager->getRepository(AssemblyBOMEntry::class); } protected function configureOptions(OptionsResolver $resolver): OptionsResolver @@ -244,7 +254,7 @@ public function stringToBOMEntries(Project|Assembly $importObject, string $data, $options = $resolver->resolve($options); return match ($options['type']) { - self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $objectType)->getBomEntries(), + self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $importObject)->getBomEntries(), self::IMPORT_TYPE_KICAD_SCHEMATIC => $this->parseKiCADSchematic($data, $options), default => throw new InvalidArgumentException($this->translator->trans('validator.bom_importer.invalid_import_type', [], 'validators')), }; @@ -272,7 +282,7 @@ public function stringToImporterResult(Project|Assembly $importObject, string $d )); return match ($options['type']) { - self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($importObject, $data, $options), + self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $importObject), self::IMPORT_TYPE_JSON => $this->parseJson($importObject, $data), self::IMPORT_TYPE_CSV => $this->parseCsv($importObject, $data), default => $defaultImporterResult, @@ -286,14 +296,13 @@ public function stringToImporterResult(Project|Assembly $importObject, string $d * validates the required fields, and creates BOM entries for each record in the data. * The BOM entries are added to the provided Project or Assembly, depending on the context. * + * @param string $data The semicolon- or comma-delimited CSV data to be parsed. * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). - * @param string $data The semicolon- or comma-delimited CSV data to be parsed. - * * @return ImporterResult The result of the import process, containing the created BOM entries. * * @throws UnexpectedValueException If required fields are missing in the provided data. */ - private function parseKiCADPCB(Project|Assembly $importObject, string $data): ImporterResult + private function parseKiCADPCB(string $data, Project|Assembly $importObject): ImporterResult { $result = new ImporterResult(); @@ -818,11 +827,11 @@ private function processPart(Project|Assembly $importObject, array $entry, Impor } if ($importObject instanceof Assembly) { - $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['assembly' => $importObject, 'part' => $part]); + $bomEntry = $this->assemblyBomEntryRepository->findOneBy(['assembly' => $importObject, 'part' => $part]); if ($bomEntry === null) { if (isset($entry['name']) && $entry['name'] !== '') { - $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['assembly' => $importObject, 'name' => $entry['name']]); + $bomEntry = $this->assemblyBomEntryRepository->findOneBy(['assembly' => $importObject, 'name' => $entry['name']]); } if ($bomEntry === null) { @@ -830,11 +839,11 @@ private function processPart(Project|Assembly $importObject, array $entry, Impor } } } else { - $bomEntry = $this->projectBOMEntryRepository->findOneBy(['project' => $importObject, 'part' => $part]); + $bomEntry = $this->projectBomEntryRepository->findOneBy(['project' => $importObject, 'part' => $part]); if ($bomEntry === null) { if (isset($entry['name']) && $entry['name'] !== '') { - $bomEntry = $this->projectBOMEntryRepository->findOneBy(['project' => $importObject, 'name' => $entry['name']]); + $bomEntry = $this->projectBomEntryRepository->findOneBy(['project' => $importObject, 'name' => $entry['name']]); } if ($bomEntry === null) { diff --git a/src/Settings/BehaviorSettings/AssemblyBomTableColumns.php b/src/Settings/BehaviorSettings/AssemblyBomTableColumns.php new file mode 100644 index 000000000..da8557c2d --- /dev/null +++ b/src/Settings/BehaviorSettings/AssemblyBomTableColumns.php @@ -0,0 +1,46 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\BehaviorSettings; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +enum AssemblyBomTableColumns : string implements TranslatableInterface +{ + + case NAME = "name"; + case ID = "id"; + case QUANTITY = "quantity"; + case IPN = "ipn"; + case DESCRIPTION = "description"; + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + $key = match($this) { + default => 'assembly.bom.table.' . $this->value, + }; + + return $translator->trans($key, locale: $locale); + } +} diff --git a/src/Settings/BehaviorSettings/AssemblyTableColumns.php b/src/Settings/BehaviorSettings/AssemblyTableColumns.php new file mode 100644 index 000000000..02c315b49 --- /dev/null +++ b/src/Settings/BehaviorSettings/AssemblyTableColumns.php @@ -0,0 +1,49 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\BehaviorSettings; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +enum AssemblyTableColumns : string implements TranslatableInterface +{ + + case NAME = "name"; + case ID = "id"; + case IPN = "ipn"; + case DESCRIPTION = "description"; + case REFERENCED_ASSEMBLIES = "referencedAssemblies"; + case ADDED_DATE = "addedDate"; + case LAST_MODIFIED = "lastModified"; + case EDIT = "edit"; + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + $key = match($this) { + default => 'assembly.table.' . $this->value, + }; + + return $translator->trans($key, locale: $locale); + } +} diff --git a/src/Settings/BehaviorSettings/TableSettings.php b/src/Settings/BehaviorSettings/TableSettings.php index b69648769..5c7455e25 100644 --- a/src/Settings/BehaviorSettings/TableSettings.php +++ b/src/Settings/BehaviorSettings/TableSettings.php @@ -53,7 +53,6 @@ class TableSettings )] public int $fullDefaultPageSize = 50; - /** @var PartTableColumns[] */ #[SettingsParameter(ArrayType::class, label: new TM("settings.behavior.table.parts_default_columns"), @@ -70,6 +69,37 @@ class TableSettings PartTableColumns::CATEGORY, PartTableColumns::FOOTPRINT, PartTableColumns::MANUFACTURER, PartTableColumns::LOCATION, PartTableColumns::AMOUNT]; + /** @var AssemblyTableColumns[] */ + #[SettingsParameter(ArrayType::class, + label: new TM("settings.behavior.table.assemblies_default_columns"), + description: new TM("settings.behavior.table.assemblies_default_columns.help"), + options: ['type' => EnumType::class, 'options' => ['class' => AssemblyTableColumns::class]], + formType: \Symfony\Component\Form\Extension\Core\Type\EnumType::class, + formOptions: ['class' => AssemblyTableColumns::class, 'multiple' => true, 'ordered' => true], + envVar: "TABLE_ASSEMBLIES_DEFAULT_COLUMNS", envVarMode: EnvVarMode::OVERWRITE, envVarMapper: [self::class, 'mapAssembliesDefaultColumnsEnv'] + )] + #[Assert\NotBlank()] + #[Assert\Unique()] + #[Assert\All([new Assert\Type(AssemblyTableColumns::class)])] + public array $assembliesDefaultColumns = [AssemblyTableColumns::ID, AssemblyTableColumns::IPN, AssemblyTableColumns::NAME, + AssemblyTableColumns::DESCRIPTION, AssemblyTableColumns::REFERENCED_ASSEMBLIES, AssemblyTableColumns::EDIT]; + + /** @var AssemblyBomTableColumns[] */ + #[SettingsParameter(ArrayType::class, + label: new TM("settings.behavior.table.assemblies_bom_default_columns"), + description: new TM("settings.behavior.table.assemblies_bom_default_columns.help"), + options: ['type' => EnumType::class, 'options' => ['class' => AssemblyBomTableColumns::class]], + formType: \Symfony\Component\Form\Extension\Core\Type\EnumType::class, + formOptions: ['class' => AssemblyBomTableColumns::class, 'multiple' => true, 'ordered' => true], + envVar: "TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS", envVarMode: EnvVarMode::OVERWRITE, envVarMapper: [self::class, 'mapAssemblyBomsDefaultColumnsEnv'] + )] + #[Assert\NotBlank()] + #[Assert\Unique()] + #[Assert\All([new Assert\Type(AssemblyBomTableColumns::class)])] + + public array $assembliesBomDefaultColumns = [AssemblyBomTableColumns::QUANTITY, AssemblyTableColumns::ID, AssemblyTableColumns::IPN, + AssemblyTableColumns::NAME, AssemblyTableColumns::DESCRIPTION]; + #[SettingsParameter(label: new TM("settings.behavior.table.preview_image_min_width"), formOptions: ['attr' => ['min' => 1, 'max' => 100]], envVar: "int:TABLE_IMAGE_PREVIEW_MIN_SIZE", envVarMode: EnvVarMode::OVERWRITE @@ -101,4 +131,36 @@ public static function mapPartsDefaultColumnsEnv(string $columns): array return $ret; } + public static function mapAssembliesDefaultColumnsEnv(string $columns): array + { + $exploded = explode(',', $columns); + $ret = []; + foreach ($exploded as $column) { + $enum = AssemblyTableColumns::tryFrom($column); + if (!$enum) { + throw new \InvalidArgumentException("Invalid column '$column' in TABLE_ASSEMBLIES_DEFAULT_COLUMNS"); + } + + $ret[] = $enum; + } + + return $ret; + } + + public static function mapAssemblyBomsDefaultColumnsEnv(string $columns): array + { + $exploded = explode(',', $columns); + $ret = []; + foreach ($exploded as $column) { + $enum = AssemblyBomTableColumns::tryFrom($column); + if (!$enum) { + throw new \InvalidArgumentException("Invalid column '$column' in TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS"); + } + + $ret[] = $enum; + } + + return $ret; + } + } diff --git a/src/Settings/MiscSettings/AssemblySettings.php b/src/Settings/MiscSettings/AssemblySettings.php new file mode 100644 index 000000000..82fb26b66 --- /dev/null +++ b/src/Settings/MiscSettings/AssemblySettings.php @@ -0,0 +1,45 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\MiscSettings; + +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Translation\TranslatableMessage as TM; + +#[Settings(label: new TM("settings.misc.assembly"))] +#[SettingsIcon("fa-list")] +class AssemblySettings +{ + use SettingsTrait; + + #[SettingsParameter( + label: new TM("settings.misc.assembly.useIpnPlaceholderInName"), + envVar: "bool:CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME", envVarMode: EnvVarMode::OVERWRITE, + )] + public bool $useIpnPlaceholderInName = true; +} diff --git a/src/Settings/MiscSettings/MiscSettings.php b/src/Settings/MiscSettings/MiscSettings.php index b8a3a73f6..68d0cf6d1 100644 --- a/src/Settings/MiscSettings/MiscSettings.php +++ b/src/Settings/MiscSettings/MiscSettings.php @@ -34,4 +34,7 @@ class MiscSettings #[EmbeddedSettings] public ?ExchangeRateSettings $exchangeRate = null; -} \ No newline at end of file + + #[EmbeddedSettings] + public ?AssemblySettings $assembly = null; +} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 778b34614..c5f9000a3 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -13138,6 +13138,30 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Sloupce, které se mají ve výchozím nastavení zobrazovat v částečných tabulkách. Pořadí položek lze změnit pomocí funkce drag & drop. + + + settings.behavior.table.assemblies_default_columns + Výchozí sloupce pro tabulky sestav + + + + + settings.behavior.table.assemblies_default_columns.help + Sloupce, které by měly být zobrazeny ve výchozím nastavení v tabulkách sestav. Pořadí prvků lze změnit přetažením. + + + + + settings.behavior.table.assemblies_bom_default_columns + Výchozí sloupce pro kusovníky sestav + + + + + settings.behavior.table.assemblies_bom_default_columns.help + Sloupce, které by měly být zobrazeny ve výchozím nastavení v kusovnících sestav. Pořadí prvků lze změnit přetažením. + + settings.ips.oemsecrets @@ -13348,6 +13372,18 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Pokud potřebujete směnné kurzy mezi měnami mimo eurozónu, můžete zde zadat API klíč z fixer.io. + + + settings.misc.assembly + Sestavy + + + + + settings.misc.assembly.useIpnPlaceholderInName + Použít zástupný symbol %%ipn%% v názvu sestavy. Zástupný symbol bude při ukládání nahrazen vstupem IPN. + + settings.behavior.part_info @@ -13792,6 +13828,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz CSV (KiCAD Pcbnew BOM) + + + assembly.bom_import.type.kicad_schematic + KiCAD Schématický editor BOM (CSV soubor) + + assembly.bom_import.clear_existing_bom @@ -14757,6 +14799,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Popis + + + assembly.table.referencedAssemblies + Referenceované sestavy + + assembly.table.addedDate @@ -14787,6 +14835,36 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Neplatný regulární výraz (regex) + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Název + + + + + assembly.bom.table.quantity + Množství + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Popis + + datasource.synonym diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 8e66fdf4d..4518c801d 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -12953,6 +12953,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver CSV (KiCAD Pcbnew BOM) + + + assembly.bom_import.type.kicad_schematic + KiCAD Skematisk editor BOM (CSV-fil) + + assembly.bom_import.clear_existing_bom @@ -13438,6 +13444,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Beskrivelse + + + assembly.table.referencedAssemblies + Referencerede forsamlinger + + assembly.table.addedDate @@ -13468,6 +13480,36 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Ugyldigt regulært udtryk (regex) + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Navn + + + + + assembly.bom.table.quantity + Mængde + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Beskrivelse + + datasource.synonym diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 07c597478..097348731 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -13158,6 +13158,30 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Die Spalten, die standardmäßig in Bauteiltabellen angezeigt werden sollen. Die Reihenfolge der Elemente kann per Drag & Drop geändert werden. + + + settings.behavior.table.assemblies_default_columns + Standardmäßige Spalten für Baugruppentabellen + + + + + settings.behavior.table.assemblies_default_columns.help + Die Spalten, die standardmäßig in Baugruppentabellen angezeigt werden sollen. Die Reihenfolge der Elemente kann per Drag & Drop geändert werden. + + + + + settings.behavior.table.assemblies_bom_default_columns + Standardmäßige Spalten für Baugruppen-Stücklisten + + + + + settings.behavior.table.assemblies_bom_default_columns.help + Die Spalten, die standardmäßig in Baugruppen-Stücklisten angezeigt werden sollen. Die Reihenfolge der Elemente kann per Drag & Drop geändert werden. + + settings.ips.oemsecrets @@ -13608,6 +13632,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön CSV (KiCAD Pcbnew BOM) + + + assembly.bom_import.type.kicad_schematic + KiCAD Schaltplaneditor BOM (CSV Datei) + + assembly.bom_import.clear_existing_bom @@ -14123,6 +14153,18 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Wenn Sie Wechselkurse zwischen Nicht-Euro-Währungen benötigen, können Sie hier einen API-Schlüssel von fixer.io eingeben. + + + settings.misc.assembly + Baugruppen + + + + + settings.misc.assembly.useIpnPlaceholderInName + Verwenden Sie einen %%ipn%%-Platzhalter im Namen einer Baugruppe. Der Platzhalter wird beim Speichern durch die eingegebene IPN ersetzt. + + settings.behavior.part_info @@ -14723,6 +14765,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Beschreibung + + + assembly.table.referencedAssemblies + Referenzierte Baugruppen + + assembly.table.addedDate @@ -14753,6 +14801,36 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Ungültiger regulärer Ausdruck (regex) + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Name + + + + + assembly.bom.table.quantity + Stückzahl + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Beschreibung + + datasource.synonym diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 6f4e3f686..fecbac216 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1961,6 +1961,12 @@ CSV (KiCAD Pcbnew BOM) + + + assembly.bom_import.type.kicad_schematic + KiCAD Σχηματικό BOM (CSV αρχείο) + + assembly.bom_import.clear_existing_bom @@ -2446,6 +2452,12 @@ Περιγραφή + + + assembly.table.referencedAssemblies + Αναφερόμενες συναρμολογήσεις + + assembly.table.addedDate @@ -2476,6 +2488,36 @@ Μη έγκυρη κανονική έκφραση (regex) + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Όνομα + + + + + assembly.bom.table.quantity + Ποσότητα + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Περιγραφή + + datasource.synonym diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index f5d4dc7ae..74a85bcf8 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13159,6 +13159,30 @@ Please note, that you can not impersonate a disabled user. If you try you will g The columns to show by default in part tables. Order of items can be changed via drag & drop. + + + settings.behavior.table.assemblies_default_columns + Default columns for assembly tables + + + + + settings.behavior.table.assemblies_default_columns.help + The columns to show by default in assembly tables. Order of items can be changed via drag & drop. + + + + + settings.behavior.table.assemblies_bom_default_columns + Default columns for assembly BOM tables + + + + + settings.behavior.table.assemblies_bom_default_columns.help + The columns to show by default in assembly BOM tables. Order of items can be changed via drag & drop. + + settings.ips.oemsecrets @@ -13609,6 +13633,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g CSV (KiCAD Pcbnew BOM) + + + assembly.bom_import.type.kicad_schematic + KiCAD Schematic BOM (CSV file) + + assembly.bom_import.clear_existing_bom @@ -14124,6 +14154,18 @@ Please note, that you can not impersonate a disabled user. If you try you will g If you need exchange rates between non-euro currencies, you can input an API key from fixer.io here. + + + settings.misc.assembly + Assemblies + + + + + settings.misc.assembly.useIpnPlaceholderInName + Use an %%ipn%% placeholder in the name of an assembly. Placeholder is replaced with the ipn input while saving. + + settings.behavior.part_info @@ -14724,6 +14766,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Description + + + assembly.table.referencedAssemblies + Referenced assemblies + + assembly.table.addedDate @@ -14754,6 +14802,36 @@ Please note, that you can not impersonate a disabled user. If you try you will g Invalid regular expression (regex) + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Name + + + + + assembly.bom.table.quantity + Quantity + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Description + + datasource.synonym diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 5927b8ec9..0268e83ca 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -13101,6 +13101,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S CSV (KiCAD Pcbnew BOM) + + + assembly.bom_import.type.kicad_schematic + KiCAD Editor Esquemático BOM (archivo CSV) + + assembly.bom_import.clear_existing_bom @@ -13610,6 +13616,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Descripción + + + assembly.table.referencedAssemblies + Ensambles referenciados + + assembly.table.addedDate @@ -13640,6 +13652,36 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Expresión regular no válida (regex) + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Nombre + + + + + assembly.bom.table.quantity + Cantidad + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Descripción + + datasource.synonym diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index bb7128e56..44af739d7 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -9535,6 +9535,12 @@ exemple de ville CSV (KiCAD Pcbnew BOM) + + + assembly.bom_import.type.kicad_schematic + KiCAD Éditeur Schématique BOM (fichier CSV) + + assembly.bom_import.clear_existing_bom @@ -10020,6 +10026,12 @@ exemple de ville Description + + + assembly.table.referencedAssemblies + Ensembles référencés + + assembly.table.addedDate @@ -10050,6 +10062,36 @@ exemple de ville Expression régulière invalide (regex) + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Nom + + + + + assembly.bom.table.quantity + Quantité + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Description + + datasource.synonym diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 29a9d64c7..70ae96f2b 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -13103,6 +13103,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a CSV (KiCAD Pcbnew) + + + assembly.bom_import.type.kicad_schematic + KiCAD Editor Schematico BOM (file CSV) + + assembly.bom_import.clear_existing_bom @@ -13612,6 +13618,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Descrizione + + + assembly.table.referencedAssemblies + Assiemi referenziati + + assembly.table.addedDate @@ -13642,6 +13654,36 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Espressione regolare non valida (regex) + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Nome + + + + + assembly.bom.table.quantity + Quantità + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Descrizione + + datasource.synonym diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 95b9bc815..13ced250c 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -9248,6 +9248,12 @@ Exampletown CSV (KiCAD Pcbnew) + + + assembly.bom_import.type.kicad_schematic + KiCAD 回路図エディタ BOM (CSV ファイル) + + assembly.bom_import.clear_existing_bom @@ -9733,6 +9739,12 @@ Exampletown 説明 + + + assembly.table.referencedAssemblies + 参照されているアセンブリ + + assembly.table.addedDate @@ -9763,6 +9775,36 @@ Exampletown 無効な正規表現(regex) + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + 名前 + + + + + assembly.bom.table.quantity + 数量 + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + 説明 + + datasource.synonym diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 5b8fe89f3..5d473dc2e 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1186,6 +1186,12 @@ CSV (KiCAD Pcbnew) + + + assembly.bom_import.type.kicad_schematic + KiCAD Schematische editor BOM (CSV-bestand) + + assembly.bom_import.clear_existing_bom @@ -1671,6 +1677,12 @@ Beschrijving + + + assembly.table.referencedAssemblies + Gerefereerde assemblages + + assembly.table.addedDate @@ -1701,6 +1713,36 @@ Ongeldige reguliere expressie (regex) + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Naam + + + + + assembly.bom.table.quantity + Hoeveelheid + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Beschrijving + + datasource.synonym diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 3be5447fe..88ba9a0ae 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -12980,6 +12980,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli CSV (KiCAD Pcbnew) + + + assembly.bom_import.type.kicad_schematic + KiCAD Schematyczny edytor BOM (plik CSV) + + assembly.bom_import.clear_existing_bom @@ -13465,6 +13471,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Opis + + + assembly.table.referencedAssemblies + Zestawy referencyjne + + assembly.table.addedDate @@ -13495,6 +13507,36 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Nieprawidłowe wyrażenie regularne (regex) + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Nazwa + + + + + assembly.bom.table.quantity + Ilość + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Opis + + datasource.synonym diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index e7b4ef938..4d0b102ba 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -737,7 +737,7 @@ user.edit.tfa.disable_tfa_message - Это выключит <b>все активные двухфакторной способы аутентификации пользователя</b>и удалит <b>резервные коды</b>! + Это выключит <b>все активные двухфакторной способы аутентификации пользователя</b>и удалит <b>резервные коды</b>! <br> Пользователь должен будет снова настроить все методы двухфакторной аутентификации и распечатать новые резервные коды! <br><br> <b>Делайте это только в том случае, если вы абсолютно уверены в личности пользователя (обращающегося за помощью), в противном случае учетная запись может быть взломана злоумышленником!</b> @@ -3746,7 +3746,7 @@ tfa_backup.reset_codes.confirm_message - Это удалит все предыдущие коды и создаст набор новых. Это не может быть отменено. + Это удалит все предыдущие коды и создаст набор новых. Это не может быть отменено. Не забудьте распечатать новы кода и хранить их в безопасном месте! @@ -13080,6 +13080,12 @@ CSV (KiCAD Pcbnew) + + + assembly.bom_import.type.kicad_schematic + KiCAD Схематический редактор BOM (CSV файл) + + assembly.bom_import.clear_existing_bom @@ -13565,6 +13571,12 @@ Описание + + + assembly.table.referencedAssemblies + Ссылочные сборки + + assembly.table.addedDate @@ -13595,6 +13607,36 @@ Неверное регулярное выражение (regex) + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Название + + + + + assembly.bom.table.quantity + Количество + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Описание + + datasource.synonym diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 8d445de4b..6fc40930f 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -12965,6 +12965,12 @@ Element 3 CSV 文件(KiCAD Pcbnew) + + + assembly.bom_import.type.kicad_schematic + KiCAD 原理图编辑器 BOM(CSV 文件) + + assembly.bom_import.clear_existing_bom @@ -13450,6 +13456,12 @@ Element 3 描述 + + + assembly.table.referencedAssemblies + 引用的组件 + + assembly.table.addedDate @@ -13480,6 +13492,36 @@ Element 3 无效的正则表达式(regex) + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + 名称 + + + + + assembly.bom.table.quantity + 数量 + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + 描述 + + datasource.synonym diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 838f47450..66f440d12 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -251,12 +251,6 @@ Musíte vybrat díl pro položku BOM dílu nebo nastavit název pro položku BOM bez dílu. - - - validator.project.bom_entry.only_part_or_assembly_allowed - Je povoleno vybrat pouze jednu součástku nebo sestavu. Upravit prosím svůj výběr! - - project.bom_entry.name_already_in_bom From b782d977795e7eea65989b63e3c92dd2c0a9e88c Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 19 Mar 2025 08:13:45 +0100 Subject: [PATCH 34/83] =?UTF-8?q?Assemblies=20einf=C3=BChren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elements/assembly_select_controller.js | 70 ++ .../elements/part_select_controller.js | 2 +- ...dont_check_quantity_checkbox_controller.js | 2 +- config/permissions.yaml | 4 + migrations/Version20250304081039.php | 33 + migrations/Version20250304154507.php | 25 + migrations/Version20250310160354.php | 27 + .../Migrations/ConvertBBCodeCommand.php | 2 + .../AdminPages/AssemblyAdminController.php | 80 ++ src/Controller/AssemblyController.php | 302 ++++++++ src/Controller/PartController.php | 24 +- src/Controller/TreeController.php | 14 + src/Controller/TypeaheadController.php | 64 +- .../AssemblyBomEntriesDataTable.php | 209 +++++ .../Helpers/AssemblyDataTableHelper.php | 48 ++ src/DataTables/ProjectBomEntriesDataTable.php | 34 +- src/Entity/AssemblySystem/Assembly.php | 358 +++++++++ .../AssemblySystem/AssemblyBOMEntry.php | 302 ++++++++ src/Entity/Attachments/AssemblyAttachment.php | 48 ++ src/Entity/Attachments/Attachment.php | 5 +- src/Entity/Base/AbstractDBElement.php | 5 +- .../LogSystem/CollectionElementDeleted.php | 5 + src/Entity/LogSystem/LogTargetType.php | 6 + src/Entity/Parameters/AbstractParameter.php | 4 +- src/Entity/Parameters/AssemblyParameter.php | 65 ++ src/Entity/Parts/Part.php | 3 + src/Entity/Parts/PartTraits/AssemblyTrait.php | 83 ++ src/Entity/ProjectSystem/Project.php | 1 + src/Entity/ProjectSystem/ProjectBOMEntry.php | 40 +- src/Form/AdminPages/AssemblyAdminForm.php | 64 ++ src/Form/AdminPages/BaseEntityAdminForm.php | 3 +- .../AssemblySystem/AssemblyAddPartsType.php | 88 +++ .../AssemblyBOMEntryCollectionType.php | 32 + .../AssemblySystem/AssemblyBOMEntryType.php | 90 +++ src/Form/AssemblySystem/AssemblyBuildType.php | 183 +++++ src/Form/Filters/AttachmentFilterType.php | 2 + .../ProjectSystem/ProjectAddPartsType.php | 1 + .../ProjectSystem/ProjectBOMEntryType.php | 15 +- src/Form/ProjectSystem/ProjectBuildType.php | 38 +- src/Form/Type/AssemblySelectType.php | 125 +++ src/Form/Type/PartSelectType.php | 5 +- .../Assemblies/AssemblyBuildRequest.php | 306 ++++++++ src/Helpers/Projects/ProjectBuildRequest.php | 102 ++- src/Repository/AssemblyRepository.php | 69 ++ src/Repository/DBElementRepository.php | 10 + src/Security/Voter/AttachmentVoter.php | 3 + src/Security/Voter/StructureVoter.php | 2 + .../AssemblySystem/AssemblyBuildHelper.php | 154 ++++ .../AssemblyBuildPartHelper.php | 40 + .../Attachments/AssemblyPreviewGenerator.php | 93 +++ .../Attachments/AttachmentSubmitHandler.php | 2 + src/Services/ElementTypeNameGenerator.php | 6 + src/Services/EntityURLGenerator.php | 8 + .../ImportExportSystem/BOMImporter.php | 236 +++++- .../ProjectSystem/ProjectBuildHelper.php | 71 +- src/Services/Trees/ToolsTreeBuilder.php | 7 + src/Services/Trees/TreeViewGenerator.php | 7 + src/Twig/EntityExtension.php | 2 + .../ValidAssemblyBuildRequest.php | 37 + .../ValidAssemblyBuildRequestValidator.php | 84 ++ templates/admin/assembly_admin.html.twig | 62 ++ templates/admin/project_admin.html.twig | 2 +- templates/assemblies/add_parts.html.twig | 22 + templates/assemblies/build/_form.html.twig | 88 +++ templates/assemblies/build/build.html.twig | 40 + templates/assemblies/import_bom.html.twig | 60 ++ templates/assemblies/info/_bom.html.twig | 22 + templates/assemblies/info/_builds.html.twig | 40 + templates/assemblies/info/_info.html.twig | 77 ++ .../assemblies/info/_info_card.html.twig | 133 ++++ .../assemblies/info/_subassemblies.html.twig | 28 + templates/assemblies/info/info.html.twig | 105 +++ .../components/assemblies.macro.html.twig | 8 + templates/components/tree_macros.html.twig | 1 + .../form/collection_types_layout.html.twig | 8 +- ...collection_types_layout_assembly.html.twig | 80 ++ templates/helper.twig | 16 + templates/projects/build/_form.html.twig | 55 +- tests/Entity/Attachments/AttachmentTest.php | 3 + .../Assemblies/AssemblyBuildRequestTest.php | 177 +++++ .../AssemblyBuildHelperTest.php | 117 +++ .../AssemblyBuildPartHelperTest.php | 52 ++ translations/messages.cs.xlf | 700 +++++++++++++++++ translations/messages.da.xlf | 700 +++++++++++++++++ translations/messages.de.xlf | 688 +++++++++++++++++ translations/messages.el.xlf | 688 +++++++++++++++++ translations/messages.en.xlf | 688 +++++++++++++++++ translations/messages.es.xlf | 688 +++++++++++++++++ translations/messages.fr.xlf | 688 +++++++++++++++++ translations/messages.it.xlf | 688 +++++++++++++++++ translations/messages.ja.xlf | 652 ++++++++++++++++ translations/messages.nl.xlf | 724 ++++++++++++++++++ translations/messages.pl.xlf | 688 +++++++++++++++++ translations/messages.ru.xlf | 688 +++++++++++++++++ translations/messages.zh.xlf | 688 +++++++++++++++++ translations/validators.cs.xlf | 24 + translations/validators.da.xlf | 24 + translations/validators.de.xlf | 26 +- translations/validators.el.xlf | 24 + translations/validators.en.xlf | 26 +- translations/validators.fr.xlf | 24 + translations/validators.hr.xlf | 24 + translations/validators.it.xlf | 24 + translations/validators.ja.xlf | 24 + translations/validators.pl.xlf | 24 + translations/validators.ru.xlf | 24 + translations/validators.zh.xlf | 24 + 107 files changed, 14106 insertions(+), 100 deletions(-) create mode 100644 assets/controllers/elements/assembly_select_controller.js create mode 100644 migrations/Version20250304081039.php create mode 100644 migrations/Version20250304154507.php create mode 100644 migrations/Version20250310160354.php create mode 100644 src/Controller/AdminPages/AssemblyAdminController.php create mode 100644 src/Controller/AssemblyController.php create mode 100644 src/DataTables/AssemblyBomEntriesDataTable.php create mode 100644 src/DataTables/Helpers/AssemblyDataTableHelper.php create mode 100644 src/Entity/AssemblySystem/Assembly.php create mode 100644 src/Entity/AssemblySystem/AssemblyBOMEntry.php create mode 100644 src/Entity/Attachments/AssemblyAttachment.php create mode 100644 src/Entity/Parameters/AssemblyParameter.php create mode 100644 src/Entity/Parts/PartTraits/AssemblyTrait.php create mode 100644 src/Form/AdminPages/AssemblyAdminForm.php create mode 100644 src/Form/AssemblySystem/AssemblyAddPartsType.php create mode 100644 src/Form/AssemblySystem/AssemblyBOMEntryCollectionType.php create mode 100644 src/Form/AssemblySystem/AssemblyBOMEntryType.php create mode 100644 src/Form/AssemblySystem/AssemblyBuildType.php create mode 100644 src/Form/Type/AssemblySelectType.php create mode 100644 src/Helpers/Assemblies/AssemblyBuildRequest.php create mode 100644 src/Repository/AssemblyRepository.php create mode 100644 src/Services/AssemblySystem/AssemblyBuildHelper.php create mode 100644 src/Services/AssemblySystem/AssemblyBuildPartHelper.php create mode 100644 src/Services/Attachments/AssemblyPreviewGenerator.php create mode 100644 src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequest.php create mode 100644 src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequestValidator.php create mode 100644 templates/admin/assembly_admin.html.twig create mode 100644 templates/assemblies/add_parts.html.twig create mode 100644 templates/assemblies/build/_form.html.twig create mode 100644 templates/assemblies/build/build.html.twig create mode 100644 templates/assemblies/import_bom.html.twig create mode 100644 templates/assemblies/info/_bom.html.twig create mode 100644 templates/assemblies/info/_builds.html.twig create mode 100644 templates/assemblies/info/_info.html.twig create mode 100644 templates/assemblies/info/_info_card.html.twig create mode 100644 templates/assemblies/info/_subassemblies.html.twig create mode 100644 templates/assemblies/info/info.html.twig create mode 100644 templates/components/assemblies.macro.html.twig create mode 100644 templates/form/collection_types_layout_assembly.html.twig create mode 100644 tests/Helpers/Assemblies/AssemblyBuildRequestTest.php create mode 100644 tests/Services/AssemblySystem/AssemblyBuildHelperTest.php create mode 100644 tests/Services/AssemblySystem/AssemblyBuildPartHelperTest.php diff --git a/assets/controllers/elements/assembly_select_controller.js b/assets/controllers/elements/assembly_select_controller.js new file mode 100644 index 000000000..98702d419 --- /dev/null +++ b/assets/controllers/elements/assembly_select_controller.js @@ -0,0 +1,70 @@ +import {Controller} from "@hotwired/stimulus"; + +import "tom-select/dist/css/tom-select.bootstrap5.css"; +import '../../css/components/tom-select_extensions.css'; +import TomSelect from "tom-select"; +import {marked} from "marked"; + +export default class extends Controller { + _tomSelect; + + connect() { + + let settings = { + allowEmptyOption: true, + plugins: ['dropdown_input', 'clear_button'], + searchField: ["name", "description", "category", "footprint"], + valueField: "id", + labelField: "name", + preload: "focus", + render: { + item: (data, escape) => { + return '' + (data.image ? "" : "") + escape(data.name) + ''; + }, + option: (data, escape) => { + if(data.text) { + return '' + escape(data.text) + ''; + } + + let tmp = '
    ' + + "
    " + + (data.image ? "" : "") + + "
    " + + "
    " + + '
    ' + escape(data.name) + '
    ' + + (data.description ? '

    ' + marked.parseInline(data.description) + '

    ' : "") + + (data.category ? '

    ' + escape(data.category) : ""); + + return tmp + '

    ' + + '
    '; + } + } + }; + + + if (this.element.dataset.autocomplete) { + const base_url = this.element.dataset.autocomplete; + settings.valueField = "id"; + settings.load = (query, callback) => { + const url = base_url.replace('__QUERY__', encodeURIComponent(query)); + + fetch(url) + .then(response => response.json()) + .then(json => {callback(json);}) + .catch(() => { + callback() + }); + }; + + + this._tomSelect = new TomSelect(this.element, settings); + //this._tomSelect.clearOptions(); + } + } + + disconnect() { + super.disconnect(); + //Destroy the TomSelect instance + this._tomSelect.destroy(); + } +} \ No newline at end of file diff --git a/assets/controllers/elements/part_select_controller.js b/assets/controllers/elements/part_select_controller.js index 0658f4b46..2b658d526 100644 --- a/assets/controllers/elements/part_select_controller.js +++ b/assets/controllers/elements/part_select_controller.js @@ -12,7 +12,7 @@ export default class extends Controller { let settings = { allowEmptyOption: true, - plugins: ['dropdown_input'], + plugins: ['dropdown_input', 'clear_button'], searchField: ["name", "description", "category", "footprint"], valueField: "id", labelField: "name", diff --git a/assets/controllers/pages/dont_check_quantity_checkbox_controller.js b/assets/controllers/pages/dont_check_quantity_checkbox_controller.js index 2abd3d77b..f3e8cb900 100644 --- a/assets/controllers/pages/dont_check_quantity_checkbox_controller.js +++ b/assets/controllers/pages/dont_check_quantity_checkbox_controller.js @@ -38,7 +38,7 @@ export default class extends Controller { connect() { //Add event listener to the checkbox - this.getCheckbox().addEventListener('change', this.toggleInputLimits.bind(this)); + this.getCheckbox()?.addEventListener('change', this.toggleInputLimits.bind(this)); } toggleInputLimits() { diff --git a/config/permissions.yaml b/config/permissions.yaml index 8cbd60c3f..8709fdb77 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -121,6 +121,10 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co <<: *PART_CONTAINING label: "perm.projects" + assemblies: + <<: *PART_CONTAINING + label: "perm.assemblies" + attachment_types: <<: *PART_CONTAINING label: "perm.part.attachment_types" diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php new file mode 100644 index 000000000..4e521ade9 --- /dev/null +++ b/migrations/Version20250304081039.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE assemblies (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, order_quantity INT NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts TINYINT(1) NOT NULL, description LONGTEXT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, INDEX IDX_5F3832C0727ACA70 (parent_id), INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('CREATE TABLE assembly_bom_entries (id INT AUTO_INCREMENT NOT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames LONGTEXT NOT NULL, name VARCHAR(255) DEFAULT NULL, comment LONGTEXT NOT NULL, price NUMERIC(11, 5) DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_assembly INT DEFAULT NULL, id_part INT DEFAULT NULL, price_currency_id INT DEFAULT NULL, INDEX IDX_8C74887E2F180363 (id_assembly), INDEX IDX_8C74887EC22F6CC4 (id_part), INDEX IDX_8C74887E3FFDCD60 (price_currency_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id)'); + $this->addSql('ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES `attachments` (id) ON DELETE SET NULL'); + $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E2F180363 FOREIGN KEY (id_assembly) REFERENCES assemblies (id)'); + $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES `parts` (id)'); + $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0727ACA70'); + $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0EA7100A1'); + $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E2F180363'); + $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EC22F6CC4'); + $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E3FFDCD60'); + $this->addSql('DROP TABLE assemblies'); + $this->addSql('DROP TABLE assembly_bom_entries'); + } +} diff --git a/migrations/Version20250304154507.php b/migrations/Version20250304154507.php new file mode 100644 index 000000000..62dcc43c0 --- /dev/null +++ b/migrations/Version20250304154507.php @@ -0,0 +1,25 @@ +addSql('ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON parts (built_assembly_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE `parts` DROP FOREIGN KEY FK_6940A7FECC660B3C'); + $this->addSql('DROP INDEX UNIQ_6940A7FECC660B3C ON `parts`'); + $this->addSql('ALTER TABLE `parts` DROP built_assembly_id'); + } +} diff --git a/migrations/Version20250310160354.php b/migrations/Version20250310160354.php new file mode 100644 index 000000000..542fcac28 --- /dev/null +++ b/migrations/Version20250310160354.php @@ -0,0 +1,27 @@ +addSql('ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e2f180363 TO IDX_8C74887E4AD2039E'); + $this->addSql('ALTER TABLE project_bom_entries ADD id_assembly INT DEFAULT NULL AFTER id_part'); + $this->addSql('ALTER TABLE project_bom_entries ADD CONSTRAINT FK_1AA2DD314AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id)'); + $this->addSql('CREATE INDEX IDX_1AA2DD314AD2039E ON project_bom_entries (id_assembly)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e4ad2039e TO IDX_8C74887E2F180363'); + $this->addSql('ALTER TABLE project_bom_entries DROP FOREIGN KEY FK_1AA2DD314AD2039E'); + $this->addSql('DROP INDEX IDX_1AA2DD314AD2039E ON project_bom_entries'); + $this->addSql('ALTER TABLE project_bom_entries DROP id_assembly'); + } +} diff --git a/src/Command/Migrations/ConvertBBCodeCommand.php b/src/Command/Migrations/ConvertBBCodeCommand.php index 201263ffd..b0c083921 100644 --- a/src/Command/Migrations/ConvertBBCodeCommand.php +++ b/src/Command/Migrations/ConvertBBCodeCommand.php @@ -22,6 +22,7 @@ namespace App\Command\Migrations; +use App\Entity\AssemblySystem\Assembly; use Symfony\Component\Console\Attribute\AsCommand; use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractNamedDBElement; @@ -88,6 +89,7 @@ protected function getTargetsLists(): array AttachmentType::class => ['comment'], StorageLocation::class => ['comment'], Project::class => ['comment'], + Assembly::class => ['comment'], Category::class => ['comment'], Manufacturer::class => ['comment'], MeasurementUnit::class => ['comment'], diff --git a/src/Controller/AdminPages/AssemblyAdminController.php b/src/Controller/AdminPages/AssemblyAdminController.php new file mode 100644 index 000000000..20f640923 --- /dev/null +++ b/src/Controller/AdminPages/AssemblyAdminController.php @@ -0,0 +1,80 @@ +. + */ + +declare(strict_types=1); + +namespace App\Controller\AdminPages; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\AssemblyAttachment; +use App\Entity\Parameters\AssemblyParameter; +use App\Form\AdminPages\AssemblyAdminForm; +use App\Services\ImportExportSystem\EntityExporter; +use App\Services\ImportExportSystem\EntityImporter; +use App\Services\Trees\StructuralElementRecursionHelper; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +#[Route(path: '/assembly')] +class AssemblyAdminController extends BaseAdminController +{ + protected string $entity_class = Assembly::class; + protected string $twig_template = 'admin/assembly_admin.html.twig'; + protected string $form_class = AssemblyAdminForm::class; + protected string $route_base = 'assembly'; + protected string $attachment_class = AssemblyAttachment::class; + protected ?string $parameter_class = AssemblyParameter::class; + + #[Route(path: '/{id}', name: 'assembly_delete', methods: ['DELETE'])] + public function delete(Request $request, Assembly $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse + { + return $this->_delete($request, $entity, $recursionHelper); + } + + #[Route(path: '/{id}/edit/{timestamp}', name: 'assembly_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}/edit', requirements: ['id' => '\d+'])] + public function edit(Assembly $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response + { + return $this->_edit($entity, $request, $em, $timestamp); + } + + #[Route(path: '/new', name: 'assembly_new')] + #[Route(path: '/{id}/clone', name: 'assembly_clone')] + #[Route(path: '/')] + public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Assembly $entity = null): Response + { + return $this->_new($request, $em, $importer, $entity); + } + + #[Route(path: '/export', name: 'assembly_export_all')] + public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response + { + return $this->_exportAll($em, $exporter, $request); + } + + #[Route(path: '/{id}/export', name: 'assembly_export')] + public function exportEntity(Assembly $entity, EntityExporter $exporter, Request $request): Response + { + return $this->_exportEntity($entity, $exporter, $request); + } +} diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php new file mode 100644 index 000000000..9710e9bec --- /dev/null +++ b/src/Controller/AssemblyController.php @@ -0,0 +1,302 @@ +. + */ +namespace App\Controller; + +use App\DataTables\AssemblyBomEntriesDataTable; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Parts\Part; +use App\Form\AssemblySystem\AssemblyAddPartsType; +use App\Form\AssemblySystem\AssemblyBuildType; +use App\Helpers\Assemblies\AssemblyBuildRequest; +use App\Repository\PartRepository; +use App\Services\ImportExportSystem\BOMImporter; +use App\Services\AssemblySystem\AssemblyBuildHelper; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\EntityManagerInterface; +use League\Csv\SyntaxError; +use Omines\DataTablesBundle\DataTableFactory; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +use Symfony\Contracts\Translation\TranslatorInterface; +use function Symfony\Component\Translation\t; + +#[Route(path: '/assembly')] +class AssemblyController extends AbstractController +{ + private PartRepository $partRepository; + + public function __construct( + private readonly DataTableFactory $dataTableFactory, + private readonly EntityManagerInterface $entityManager, + private readonly TranslatorInterface $translator, + ) { + $this->partRepository = $this->entityManager->getRepository(Part::class); + } + + #[Route(path: '/{id}/info', name: 'assembly_info', requirements: ['id' => '\d+'])] + public function info(Assembly $assembly, Request $request, AssemblyBuildHelper $buildHelper): Response + { + $this->denyAccessUnlessGranted('read', $assembly); + + $table = $this->dataTableFactory->createFromType(AssemblyBomEntriesDataTable::class, ['assembly' => $assembly]) + ->handleRequest($request); + + if ($table->isCallback()) { + return $table->getResponse(); + } + + return $this->render('assemblies/info/info.html.twig', [ + 'buildHelper' => $buildHelper, + 'datatable' => $table, + 'assembly' => $assembly, + ]); + } + + #[Route(path: '/{id}/build', name: 'assembly_build', requirements: ['id' => '\d+'])] + public function build(Assembly $assembly, Request $request, AssemblyBuildHelper $buildHelper, EntityManagerInterface $entityManager): Response + { + $this->denyAccessUnlessGranted('read', $assembly); + + //If no number of builds is given (or it is invalid), just assume 1 + $number_of_builds = $request->query->getInt('n', 1); + if ($number_of_builds < 1) { + $number_of_builds = 1; + } + + $assemblyBuildRequest = new AssemblyBuildRequest($assembly, $number_of_builds); + $form = $this->createForm(AssemblyBuildType::class, $assemblyBuildRequest); + + $form->handleRequest($request); + if ($form->isSubmitted()) { + if ($form->isValid()) { + //Ensure that the user can withdraw stock from all parts + $this->denyAccessUnlessGranted('@parts_stock.withdraw'); + + //We have to do a flush already here, so that the newly created partLot gets an ID and can be logged to DB later. + $entityManager->flush(); + $buildHelper->doBuild($assemblyBuildRequest); + $entityManager->flush(); + $this->addFlash('success', 'assembly.build.flash.success'); + + return $this->redirect( + $request->get('_redirect', + $this->generateUrl('assembly_info', ['id' => $assembly->getID()] + ))); + } + + $this->addFlash('error', 'assembly.build.flash.invalid_input'); + } + + return $this->render('assemblies/build/build.html.twig', [ + 'buildHelper' => $buildHelper, + 'assembly' => $assembly, + 'build_request' => $assemblyBuildRequest, + 'number_of_builds' => $number_of_builds, + 'form' => $form, + ]); + } + + #[Route(path: '/{id}/import_bom', name: 'assembly_import_bom', requirements: ['id' => '\d+'])] + public function importBOM(Request $request, EntityManagerInterface $entityManager, Assembly $assembly, + BOMImporter $BOMImporter, ValidatorInterface $validator): Response + { + $this->denyAccessUnlessGranted('edit', $assembly); + + $builder = $this->createFormBuilder(); + $builder->add('file', FileType::class, [ + 'label' => 'import.file', + 'required' => true, + 'attr' => [ + 'accept' => '.csv, .json' + ] + ]); + $builder->add('type', ChoiceType::class, [ + 'label' => 'assembly.bom_import.type', + 'required' => true, + 'choices' => [ + 'assembly.bom_import.type.json' => 'json', + 'assembly.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew', + ] + ]); + $builder->add('clear_existing_bom', CheckboxType::class, [ + 'label' => 'assembly.bom_import.clear_existing_bom', + 'required' => false, + 'data' => false, + 'help' => 'assembly.bom_import.clear_existing_bom.help', + ]); + $builder->add('submit', SubmitType::class, [ + 'label' => 'import.btn', + ]); + + $form = $builder->getForm(); + + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + + //Clear existing BOM entries if requested + if ($form->get('clear_existing_bom')->getData()) { + $assembly->getBomEntries()->clear(); + $entityManager->flush(); + } + + try { + $entries = $BOMImporter->importFileIntoAssembly($form->get('file')->getData(), $assembly, [ + 'type' => $form->get('type')->getData(), + ]); + + //Validate the assembly entries + $errors = $validator->validateProperty($assembly, 'bom_entries'); + + //If no validation errors occured, save the changes and redirect to edit page + if (count ($errors) === 0) { + foreach ($entries as $entry) { + if ($entry instanceof AssemblyBOMEntry && $entry->getPart() !== null) { + $part = $entry->getPart(); + if ($part->getID() === null) { + $this->partRepository->save($part); + } + } + } + + $this->addFlash('success', t('assembly.bom_import.flash.success', ['%count%' => count($entries)])); + $entityManager->flush(); + return $this->redirectToRoute('assembly_edit', ['id' => $assembly->getID()]); + } + + //When we get here, there were validation errors + $this->addFlash('error', t('assembly.bom_import.flash.invalid_entries')); + + } catch (\UnexpectedValueException|\RuntimeException|SyntaxError $e) { + $this->addFlash('error', t('assembly.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()])); + } + } + + $jsonTemplate = [ + [ + "quantity" => 1.0, + "name" => $this->translator->trans('assembly.bom_import.template.entry.name'), + "part" => [ + "id" => null, + "ipn" => $this->translator->trans('assembly.bom_import.template.entry.part.ipn'), + "mpnr" => $this->translator->trans('assembly.bom_import.template.entry.part.mpnr'), + "name" => $this->translator->trans('assembly.bom_import.template.entry.part.name'), + "description" => null, + "manufacturer" => [ + "id" => null, + "name" => $this->translator->trans('assembly.bom_import.template.entry.part.manufacturer.name') + ], + "category" => [ + "id" => null, + "name" => $this->translator->trans('assembly.bom_import.template.entry.part.category.name') + ] + ] + ] + ]; + + return $this->render('assemblies/import_bom.html.twig', [ + 'assembly' => $assembly, + 'jsonTemplate' => $jsonTemplate, + 'form' => $form, + 'errors' => $errors ?? null, + ]); + } + + #[Route(path: '/add_parts', name: 'assembly_add_parts_no_id')] + #[Route(path: '/{id}/add_parts', name: 'assembly_add_parts', requirements: ['id' => '\d+'])] + public function addPart(Request $request, EntityManagerInterface $entityManager, ?Assembly $assembly): Response + { + if($assembly instanceof Assembly) { + $this->denyAccessUnlessGranted('edit', $assembly); + } else { + $this->denyAccessUnlessGranted('@assemblies.edit'); + } + + $form = $this->createForm(AssemblyAddPartsType::class, null, [ + 'assembly' => $assembly, + ]); + + //Preset the BOM entries with the selected parts, when the form was not submitted yet + $preset_data = new ArrayCollection(); + foreach (explode(',', (string) $request->get('parts', '')) as $part_id) { + //Skip empty part IDs. Postgres seems to be especially sensitive to empty strings, as it does not allow them in integer columns + if ($part_id === '') { + continue; + } + + $part = $entityManager->getRepository(Part::class)->find($part_id); + if (null !== $part) { + //If there is already a BOM entry for this part, we use this one (we edit it then) + $bom_entry = $entityManager->getRepository(AssemblyBOMEntry::class)->findOneBy([ + 'assembly' => $assembly, + 'part' => $part + ]); + if ($bom_entry !== null) { + $preset_data->add($bom_entry); + } else { //Otherwise create an empty one + $entry = new AssemblyBOMEntry(); + $entry->setAssembly($assembly); + $entry->setPart($part); + $preset_data->add($entry); + } + } + } + $form['bom_entries']->setData($preset_data); + + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $target_assembly = $assembly ?? $form->get('assembly')->getData(); + + //Ensure that we really have acces to the selected assembly + $this->denyAccessUnlessGranted('edit', $target_assembly); + + $data = $form->getData(); + $bom_entries = $data['bom_entries']; + foreach ($bom_entries as $bom_entry){ + $target_assembly->addBOMEntry($bom_entry); + } + + $entityManager->flush(); + + //If a redirect query parameter is set, redirect to this page + if ($request->query->get('_redirect')) { + return $this->redirect($request->query->get('_redirect')); + } + //Otherwise just show the assembly info page + return $this->redirectToRoute('assembly_info', ['id' => $target_assembly->getID()]); + } + + return $this->render('assemblies/add_parts.html.twig', [ + 'assembly' => $assembly, + 'form' => $form, + ]); + } +} diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 6708ed4cd..9e7e61b03 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -23,6 +23,7 @@ namespace App\Controller; use App\DataTables\LogDataTable; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\AttachmentUpload; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -45,6 +46,7 @@ use App\Services\Parameters\ParameterExtractor; use App\Services\Parts\PartLotWithdrawAddHelper; use App\Services\Parts\PricedetailHelper; +use App\Services\AssemblySystem\AssemblyBuildPartHelper; use App\Services\ProjectSystem\ProjectBuildPartHelper; use App\Settings\BehaviorSettings\PartInfoSettings; use DateTime; @@ -158,11 +160,15 @@ public function delete(Request $request, Part $part): RedirectResponse #[Route(path: '/new', name: 'part_new')] #[Route(path: '/{id}/clone', name: 'part_clone')] - #[Route(path: '/new_build_part/{project_id}', name: 'part_new_build_part')] - public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator, - AttachmentSubmitHandler $attachmentSubmitHandler, ProjectBuildPartHelper $projectBuildPartHelper, - #[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null, - #[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null): Response + #[Route(path: '/new_build_part_project/{project_id}', name: 'part_new_build_part_project')] + #[Route(path: '/new_build_part_assembly/{assembly_id}', name: 'part_new_build_part_assembly')] + public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator, + AttachmentSubmitHandler $attachmentSubmitHandler, + ProjectBuildPartHelper $projectBuildPartHelper, + AssemblyBuildPartHelper $assemblyBuildPartHelper, + #[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null, + #[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null, + #[MapEntity(mapping: ['assembly_id' => 'id'])] ?Assembly $assembly = null): Response { if ($part instanceof Part) { @@ -176,6 +182,14 @@ public function new(Request $request, EntityManagerInterface $em, TranslatorInte return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]); } $new_part = $projectBuildPartHelper->getPartInitialization($project); + } elseif ($assembly instanceof Assembly) { + //Initialize a new part for a build part from the given assembly + //Ensure that the assembly has not already a build part + if ($project->getBuildPart() instanceof Part) { + $this->addFlash('error', 'part.new_build_part.error.build_part_already_exists'); + return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]); + } + $new_part = $assemblyBuildPartHelper->getPartInitialization($assembly); } else { //Create an empty part from scratch $new_part = new Part(); } diff --git a/src/Controller/TreeController.php b/src/Controller/TreeController.php index 71f8ba5c6..0ba3a1584 100644 --- a/src/Controller/TreeController.php +++ b/src/Controller/TreeController.php @@ -22,6 +22,7 @@ namespace App\Controller; +use App\Entity\AssemblySystem\Assembly; use Symfony\Component\HttpFoundation\Response; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; @@ -129,4 +130,17 @@ public function deviceTree(?Project $device = null): JsonResponse return new JsonResponse($tree); } + + #[Route(path: '/assembly/{id}', name: 'tree_assembly')] + #[Route(path: '/assemblies', name: 'tree_assembly_root')] + public function assemblyTree(?Assembly $assembly = null): JsonResponse + { + if ($this->isGranted('@assemblies.read')) { + $tree = $this->treeGenerator->getTreeView(Assembly::class, $assembly, 'assemblies'); + } else { + return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN); + } + + return new JsonResponse($tree); + } } diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 89eac7ff7..09792951a 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -22,7 +22,9 @@ namespace App\Controller; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Parameters\AbstractParameter; +use App\Services\Attachments\AssemblyPreviewGenerator; use Symfony\Component\HttpFoundation\Response; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Category; @@ -53,6 +55,8 @@ use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; +use Symfony\Contracts\Translation\TranslatorInterface; +use InvalidArgumentException; /** * In this controller the endpoints for the typeaheads are collected. @@ -60,8 +64,11 @@ #[Route(path: '/typeahead')] class TypeaheadController extends AbstractController { - public function __construct(protected AttachmentURLGenerator $urlGenerator, protected Packages $assets) - { + public function __construct( + protected AttachmentURLGenerator $urlGenerator, + protected Packages $assets, + protected TranslatorInterface $translator + ) { } #[Route(path: '/builtInResources/search', name: 'typeahead_builtInRessources')] @@ -109,19 +116,22 @@ private function typeToParameterClass(string $type): string 'group' => GroupParameter::class, 'measurement_unit' => MeasurementUnitParameter::class, 'currency' => Currency::class, - default => throw new \InvalidArgumentException('Invalid parameter type: '.$type), + default => throw new InvalidArgumentException('Invalid parameter type: '.$type), }; } #[Route(path: '/parts/search/{query}', name: 'typeahead_parts')] - public function parts(EntityManagerInterface $entityManager, PartPreviewGenerator $previewGenerator, - AttachmentURLGenerator $attachmentURLGenerator, string $query = ""): JsonResponse - { + public function parts( + EntityManagerInterface $entityManager, + PartPreviewGenerator $previewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" + ): JsonResponse { $this->denyAccessUnlessGranted('@parts.read'); - $repo = $entityManager->getRepository(Part::class); + $partRepository = $entityManager->getRepository(Part::class); - $parts = $repo->autocompleteSearch($query, 100); + $parts = $partRepository->autocompleteSearch($query, 100); $data = []; foreach ($parts as $part) { @@ -147,6 +157,44 @@ public function parts(EntityManagerInterface $entityManager, PartPreviewGenerato return new JsonResponse($data); } + #[Route(path: '/assemblies/search/{query}', name: 'typeahead_assemblies')] + public function assemblies( + EntityManagerInterface $entityManager, + AssemblyPreviewGenerator $assemblyPreviewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" + ): JsonResponse { + $this->denyAccessUnlessGranted('@assemblies.read'); + + $result = []; + + $assemblyRepository = $entityManager->getRepository(Assembly::class); + + $assemblies = $assemblyRepository->autocompleteSearch($query, 100); + + foreach ($assemblies as $assembly) { + $preview_attachment = $assemblyPreviewGenerator->getTablePreviewAttachment($assembly); + + if($preview_attachment instanceof Attachment) { + $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); + } else { + $preview_url = ''; + } + + /** @var Assembly $assembly */ + $result[] = [ + 'id' => $assembly->getID(), + 'name' => $this->translator->trans('typeahead.parts.assembly.name', ['%name%' => $assembly->getName()]), + 'category' => '', + 'footprint' => '', + 'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'), + 'image' => $preview_url, + ]; + } + + return new JsonResponse($result); + } + #[Route(path: '/parameters/{type}/search/{query}', name: 'typeahead_parameters', requirements: ['type' => '.+'])] public function parameters(string $type, EntityManagerInterface $entityManager, string $query = ""): JsonResponse { diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php new file mode 100644 index 000000000..7149ed5f5 --- /dev/null +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -0,0 +1,209 @@ +. + */ +namespace App\DataTables; + +use App\DataTables\Column\EntityColumn; +use App\DataTables\Column\LocaleDateTimeColumn; +use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Helpers\PartDataTableHelper; +use App\Entity\Attachments\Attachment; +use App\Entity\Parts\Part; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Services\EntityURLGenerator; +use App\Services\Formatters\AmountFormatter; +use Doctrine\ORM\QueryBuilder; +use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; +use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; +use Omines\DataTablesBundle\Column\TextColumn; +use Omines\DataTablesBundle\DataTable; +use Omines\DataTablesBundle\DataTableTypeInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +class AssemblyBomEntriesDataTable implements DataTableTypeInterface +{ + public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter) + { + } + + + public function configure(DataTable $dataTable, array $options): void + { + $dataTable + //->add('select', SelectColumn::class) + ->add('picture', TextColumn::class, [ + 'label' => '', + 'className' => 'no-colvis', + 'render' => function ($value, AssemblyBOMEntry $context) { + if(!$context->getPart() instanceof Part) { + return ''; + } + return $this->partDataTableHelper->renderPicture($context->getPart()); + }, + ]) + ->add('id', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.id'), + 'visible' => false, + ]) + ->add('quantity', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.bom.quantity'), + 'className' => 'text-center', + 'orderField' => 'bom_entry.quantity', + 'render' => function ($value, AssemblyBOMEntry $context): float|string { + //If we have a non-part entry, only show the rounded quantity + if (!$context->getPart() instanceof Part) { + return round($context->getQuantity()); + } + //Otherwise use the unit of the part to format the quantity + return htmlspecialchars($this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit())); + }, + ]) + ->add('name', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.name'), + 'orderField' => 'NATSORT(part.name)', + 'render' => function ($value, AssemblyBOMEntry $context) { + if(!$context->getPart() instanceof Part) { + return htmlspecialchars((string) $context->getName()); + } + + //Part exists if we reach this point + + $tmp = $this->partDataTableHelper->renderName($context->getPart()); + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
    '.htmlspecialchars($context->getName()).''; + } + return $tmp; + }, + ]) + ->add('ipn', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.ipn'), + 'orderField' => 'NATSORT(part.ipn)', + 'visible' => false, + 'render' => function ($value, AssemblyBOMEntry $context) { + if($context->getPart() instanceof Part) { + return $context->getPart()->getIpn(); + } + } + ]) + ->add('description', MarkdownColumn::class, [ + 'label' => $this->translator->trans('part.table.description'), + 'data' => function (AssemblyBOMEntry $context) { + if($context->getPart() instanceof Part) { + return $context->getPart()->getDescription(); + } + //For non-part BOM entries show the comment field + return $context->getComment(); + }, + ]) + ->add('category', EntityColumn::class, [ + 'label' => $this->translator->trans('part.table.category'), + 'property' => 'part.category', + 'orderField' => 'NATSORT(category.name)', + ]) + ->add('footprint', EntityColumn::class, [ + 'property' => 'part.footprint', + 'label' => $this->translator->trans('part.table.footprint'), + 'orderField' => 'NATSORT(footprint.name)', + ]) + ->add('manufacturer', EntityColumn::class, [ + 'property' => 'part.manufacturer', + 'label' => $this->translator->trans('part.table.manufacturer'), + 'orderField' => 'NATSORT(manufacturer.name)', + ]) + ->add('mountnames', TextColumn::class, [ + 'label' => 'assembly.bom.mountnames', + 'render' => function ($value, AssemblyBOMEntry $context) { + $html = ''; + + foreach (explode(',', $context->getMountnames()) as $mountname) { + $html .= sprintf('%s ', htmlspecialchars($mountname)); + } + return $html; + }, + ]) + ->add('instockAmount', TextColumn::class, [ + 'label' => 'assembly.bom.instockAmount', + 'visible' => false, + 'render' => function ($value, AssemblyBOMEntry $context) { + if ($context->getPart() !== null) { + return $this->partDataTableHelper->renderAmount($context->getPart()); + } + + return ''; + } + ]) + ->add('storageLocations', TextColumn::class, [ + 'label' => 'part.table.storeLocations', + 'visible' => false, + 'render' => function ($value, AssemblyBOMEntry $context) { + if ($context->getPart() !== null) { + return $this->partDataTableHelper->renderStorageLocations($context->getPart()); + } + + return ''; + } + ]) + ->add('addedDate', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('part.table.addedDate'), + 'visible' => false, + ]) + ->add('lastModified', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('part.table.lastModified'), + 'visible' => false, + ]) + ; + + $dataTable->addOrderBy('name', DataTable::SORT_ASCENDING); + + $dataTable->createAdapter(ORMAdapter::class, [ + 'entity' => Attachment::class, + 'query' => function (QueryBuilder $builder) use ($options): void { + $this->getQuery($builder, $options); + }, + 'criteria' => [ + function (QueryBuilder $builder) use ($options): void { + $this->buildCriteria($builder, $options); + }, + new SearchCriteriaProvider(), + ], + ]); + } + + private function getQuery(QueryBuilder $builder, array $options): void + { + $builder->select('bom_entry') + ->addSelect('part') + ->from(AssemblyBOMEntry::class, 'bom_entry') + ->leftJoin('bom_entry.part', 'part') + ->leftJoin('part.category', 'category') + ->leftJoin('part.footprint', 'footprint') + ->leftJoin('part.manufacturer', 'manufacturer') + ->where('bom_entry.assembly = :assembly') + ->setParameter('assembly', $options['assembly']) + ; + } + + private function buildCriteria(QueryBuilder $builder, array $options): void + { + + } +} diff --git a/src/DataTables/Helpers/AssemblyDataTableHelper.php b/src/DataTables/Helpers/AssemblyDataTableHelper.php new file mode 100644 index 000000000..36f7836b7 --- /dev/null +++ b/src/DataTables/Helpers/AssemblyDataTableHelper.php @@ -0,0 +1,48 @@ +. + */ + +namespace App\DataTables\Helpers; + +use App\Entity\AssemblySystem\Assembly; +use App\Services\EntityURLGenerator; + +/** + * A helper service which contains common code to render columns for assembly related tables + */ +class AssemblyDataTableHelper +{ + public function __construct(private readonly EntityURLGenerator $entityURLGenerator) { + } + + public function renderName(Assembly $context): string + { + $icon = ''; + + return sprintf( + '%s%s', + $this->entityURLGenerator->infoURL($context), + $icon, + htmlspecialchars($context->getName()) + ); + } +} diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index fcb069844..1c7a09e49 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -25,7 +25,9 @@ use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Helpers\AssemblyDataTableHelper; use App\DataTables\Helpers\PartDataTableHelper; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\ProjectBOMEntry; @@ -41,11 +43,15 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface { - public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter) - { + public function __construct( + protected TranslatorInterface $translator, + protected PartDataTableHelper $partDataTableHelper, + protected AssemblyDataTableHelper $assemblyDataTableHelper, + protected EntityURLGenerator $entityURLGenerator, + protected AmountFormatter $amountFormatter + ) { } - public function configure(DataTable $dataTable, array $options): void { $dataTable @@ -84,16 +90,26 @@ public function configure(DataTable $dataTable, array $options): void 'label' => $this->translator->trans('part.table.name'), 'orderField' => 'NATSORT(part.name)', 'render' => function ($value, ProjectBOMEntry $context) { - if(!$context->getPart() instanceof Part) { + if(!$context->getPart() instanceof Part && !$context->getAssembly() instanceof Assembly) { return htmlspecialchars((string) $context->getName()); } - //Part exists if we reach this point - - $tmp = $this->partDataTableHelper->renderName($context->getPart()); - if($context->getName() !== null && $context->getName() !== '') { - $tmp .= '
    '.htmlspecialchars($context->getName()).''; + if ($context->getPart() !== null) { + $tmp = $this->partDataTableHelper->renderName($context->getPart()); + $tmp = $this->translator->trans('part.table.name.value.for_part', ['%value%' => $tmp]); + + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
    '.htmlspecialchars($context->getName()).''; + } + } elseif ($context->getAssembly() !== null) { + $tmp = $this->assemblyDataTableHelper->renderName($context->getAssembly()); + $tmp = $this->translator->trans('part.table.name.value.for_assembly', ['%value%' => $tmp]); + + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
    '.htmlspecialchars($context->getName()).''; + } } + return $tmp; }, ]) diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php new file mode 100644 index 000000000..17a6868f0 --- /dev/null +++ b/src/Entity/AssemblySystem/Assembly.php @@ -0,0 +1,358 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\AssemblySystem; + +use App\Repository\AssemblyRepository; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Attachments\Attachment; +use App\Validator\Constraints\UniqueObjectCollection; +use Doctrine\DBAL\Types\Types; +use App\Entity\Attachments\AssemblyAttachment; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Parameters\AssemblyParameter; +use App\Entity\Parts\Part; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use InvalidArgumentException; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; + +/** + * This class represents a assembly in the database. + * + * @extends AbstractStructuralDBElement + */ +#[ORM\Entity(repositoryClass: AssemblyRepository::class)] +#[ORM\Table(name: 'assemblies')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@assemblies.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['assembly:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['assembly:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/assemblies/{id}/children.{_format}', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'Retrieves the children elements of a assembly.'), + security: 'is_granted("@assemblies.read")' + ) + ], + uriVariables: [ + 'id' => new Link(fromProperty: 'children', fromClass: Assembly::class) + ], + normalizationContext: ['groups' => ['assembly:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] +class Assembly extends AbstractStructuralDBElement +{ + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; + + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['assembly:read', 'assembly:write'])] + #[ApiProperty(readableLink: false, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; + + #[Groups(['assembly:read', 'assembly:write'])] + protected string $comment = ''; + + /** + * @var Collection + */ + #[Assert\Valid] + #[Groups(['extended', 'full', 'import'])] + #[ORM\OneToMany(mappedBy: 'assembly', targetEntity: AssemblyBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])] + #[UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name'])] + protected Collection $bom_entries; + + #[ORM\Column(type: Types::INTEGER)] + protected int $order_quantity = 0; + + /** + * @var string|null The current status of the assembly + */ + #[Assert\Choice(['draft', 'planning', 'in_production', 'finished', 'archived'])] + #[Groups(['extended', 'full', 'assembly:read', 'assembly:write', 'import'])] + #[ORM\Column(type: Types::STRING, length: 64, nullable: true)] + protected ?string $status = null; + + + /** + * @var Part|null The (optional) part that represents the builds of this assembly in the stock + */ + #[ORM\OneToOne(mappedBy: 'built_assembly', targetEntity: Part::class, cascade: ['persist'], orphanRemoval: true)] + #[Groups(['assembly:read', 'assembly:write'])] + protected ?Part $build_part = null; + + #[ORM\Column(type: Types::BOOLEAN)] + protected bool $order_only_missing_parts = false; + + #[Groups(['simple', 'extended', 'full', 'assembly:read', 'assembly:write'])] + #[ORM\Column(type: Types::TEXT)] + protected string $description = ''; + + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'element', targetEntity: AssemblyAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + #[Groups(['assembly:read', 'assembly:write'])] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: AssemblyAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['assembly:read', 'assembly:write'])] + protected ?Attachment $master_picture_attachment = null; + + /** @var Collection + */ + #[ORM\OneToMany(mappedBy: 'element', targetEntity: AssemblyParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] + #[Groups(['assembly:read', 'assembly:write'])] + protected Collection $parameters; + + #[Groups(['assembly:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['assembly:read'])] + protected ?\DateTimeImmutable $lastModified = null; + + + /******************************************************************************** + * + * Getters + * + *********************************************************************************/ + + public function __construct() + { + $this->attachments = new ArrayCollection(); + $this->parameters = new ArrayCollection(); + parent::__construct(); + $this->bom_entries = new ArrayCollection(); + $this->children = new ArrayCollection(); + } + + public function __clone() + { + //When cloning this assembly, we have to clone each bom entry too. + if ($this->id) { + $bom_entries = $this->bom_entries; + $this->bom_entries = new ArrayCollection(); + //Set master attachment is needed + foreach ($bom_entries as $bom_entry) { + $clone = clone $bom_entry; + $this->addBomEntry($clone); + } + } + + //Parent has to be last call, as it resets the ID + parent::__clone(); + } + + /** + * Get the order quantity of this assembly. + * + * @return int the order quantity + */ + public function getOrderQuantity(): int + { + return $this->order_quantity; + } + + /** + * Get the "order_only_missing_parts" attribute. + * + * @return bool the "order_only_missing_parts" attribute + */ + public function getOrderOnlyMissingParts(): bool + { + return $this->order_only_missing_parts; + } + + /******************************************************************************** + * + * Setters + * + *********************************************************************************/ + + /** + * Set the order quantity. + * + * @param int $new_order_quantity the new order quantity + * + * @return $this + */ + public function setOrderQuantity(int $new_order_quantity): self + { + if ($new_order_quantity < 0) { + throw new InvalidArgumentException('The new order quantity must not be negative!'); + } + $this->order_quantity = $new_order_quantity; + + return $this; + } + + /** + * Set the "order_only_missing_parts" attribute. + * + * @param bool $new_order_only_missing_parts the new "order_only_missing_parts" attribute + */ + public function setOrderOnlyMissingParts(bool $new_order_only_missing_parts): self + { + $this->order_only_missing_parts = $new_order_only_missing_parts; + + return $this; + } + + public function getBomEntries(): Collection + { + return $this->bom_entries; + } + + /** + * @return $this + */ + public function addBomEntry(AssemblyBOMEntry $entry): self + { + $entry->setAssembly($this); + $this->bom_entries->add($entry); + return $this; + } + + /** + * @return $this + */ + public function removeBomEntry(AssemblyBOMEntry $entry): self + { + $this->bom_entries->removeElement($entry); + return $this; + } + + public function getDescription(): string + { + return $this->description; + } + + public function setDescription(string $description): Assembly + { + $this->description = $description; + return $this; + } + + /** + * @return string + */ + public function getStatus(): ?string + { + return $this->status; + } + + /** + * @param string $status + */ + public function setStatus(?string $status): void + { + $this->status = $status; + } + + /** + * Checks if this assembly has an associated part representing the builds of this assembly in the stock. + */ + public function hasBuildPart(): bool + { + return $this->build_part instanceof Part; + } + + /** + * Gets the part representing the builds of this assembly in the stock, if it is existing + */ + public function getBuildPart(): ?Part + { + return $this->build_part; + } + + /** + * Sets the part representing the builds of this assembly in the stock. + */ + public function setBuildPart(?Part $build_part): void + { + $this->build_part = $build_part; + if ($build_part instanceof Part) { + $build_part->setBuiltAssembly($this); + } + } + + #[Assert\Callback] + public function validate(ExecutionContextInterface $context, $payload): void + { + //If this assembly has subassemblies, and these have builds part, they must be included in the BOM + foreach ($this->getChildren() as $child) { + if (!$child->getBuildPart() instanceof Part) { + continue; + } + //We have to search all bom entries for the build part + $found = false; + foreach ($this->getBomEntries() as $bom_entry) { + if ($bom_entry->getPart() === $child->getBuildPart()) { + $found = true; + break; + } + } + + //When the build part is not found, we have to add an error + if (!$found) { + $context->buildViolation('assembly.bom_has_to_include_all_subelement_parts') + ->atPath('bom_entries') + ->setParameter('%assembly_name%', $child->getName()) + ->setParameter('%part_name%', $child->getBuildPart()->getName()) + ->addViolation(); + } + } + } +} diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php new file mode 100644 index 000000000..375fef040 --- /dev/null +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -0,0 +1,302 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\AssemblySystem; + +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Doctrine\Orm\Filter\RangeFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Contracts\TimeStampableInterface; +use App\Entity\AssemblySystem\Assembly; +use App\Repository\DBElementRepository; +use App\Validator\UniqueValidatableInterface; +use Doctrine\DBAL\Types\Types; +use App\Entity\Base\AbstractDBElement; +use App\Entity\Base\TimestampTrait; +use App\Entity\Parts\Part; +use App\Entity\PriceInformations\Currency; +use App\Validator\Constraints\BigDecimal\BigDecimalPositive; +use App\Validator\Constraints\Selectable; +use Brick\Math\BigDecimal; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; + +/** + * The AssemblyBOMEntry class represents an entry in a assembly's BOM. + */ +#[ORM\HasLifecycleCallbacks] +#[ORM\Entity(repositoryClass: DBElementRepository::class)] +#[ORM\Table('assembly_bom_entries')] +#[ApiResource( + operations: [ + new Get(uriTemplate: '/assembly_bom_entries/{id}.{_format}', security: 'is_granted("read", object)',), + new GetCollection(uriTemplate: '/assembly_bom_entries.{_format}', security: 'is_granted("@assemblies.read")',), + new Post(uriTemplate: '/assembly_bom_entries.{_format}', securityPostDenormalize: 'is_granted("create", object)',), + new Patch(uriTemplate: '/assembly_bom_entries/{id}.{_format}', security: 'is_granted("edit", object)',), + new Delete(uriTemplate: '/assembly_bom_entries/{id}.{_format}', security: 'is_granted("delete", object)',), + ], + normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['bom_entry:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/assemblies/{id}/bom.{_format}', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'Retrieves the BOM entries of the given assembly.'), + security: 'is_granted("@assemblies.read")' + ) + ], + uriVariables: [ + 'id' => new Link(fromProperty: 'bom_entries', fromClass: Assembly::class) + ], + normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])] +#[ApiFilter(RangeFilter::class, properties: ['quantity'])] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified', 'quantity'])] +class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInterface, TimeStampableInterface +{ + use TimestampTrait; + + #[Assert\Positive] + #[ORM\Column(name: 'quantity', type: Types::FLOAT)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] + protected float $quantity = 1.0; + + /** + * @var string A comma separated list of the names, where this parts should be placed + */ + #[ORM\Column(name: 'mountnames', type: Types::TEXT)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] + protected string $mountnames = ''; + + /** + * @var string|null An optional name describing this BOM entry (useful for non-part entries) + */ + #[Assert\Expression('this.getPart() !== null or this.getName() !== null', message: 'validator.assembly.bom_entry.name_or_part_needed')] + #[ORM\Column(type: Types::STRING, nullable: true)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] + protected ?string $name = null; + + /** + * @var string An optional comment for this BOM entry + */ + #[ORM\Column(type: Types::TEXT)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full'])] + protected string $comment = ''; + + /** + * @var Assembly|null + */ + #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'bom_entries')] + #[ORM\JoinColumn(name: 'id_assembly', nullable: true)] + #[Groups(['bom_entry:read', 'bom_entry:write', ])] + protected ?Assembly $assembly = null; + + /** + * @var Part|null The part associated with this + */ + #[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'assembly_bom_entries')] + #[ORM\JoinColumn(name: 'id_part')] + #[Groups(['bom_entry:read', 'bom_entry:write', 'full'])] + protected ?Part $part = null; + + /** + * @var BigDecimal|null The price of this non-part BOM entry + */ + #[Assert\AtLeastOneOf([new BigDecimalPositive(), new Assert\IsNull()])] + #[ORM\Column(type: 'big_decimal', precision: 11, scale: 5, nullable: true)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full'])] + protected ?BigDecimal $price = null; + + /** + * @var ?Currency The currency for the price of this non-part BOM entry + */ + #[ORM\ManyToOne(targetEntity: Currency::class)] + #[ORM\JoinColumn] + #[Selectable] + protected ?Currency $price_currency = null; + + public function __construct() + { + } + + public function getQuantity(): float + { + return $this->quantity; + } + + public function setQuantity(float $quantity): AssemblyBOMEntry + { + $this->quantity = $quantity; + return $this; + } + + public function getMountnames(): string + { + return $this->mountnames; + } + + public function setMountnames(string $mountnames): AssemblyBOMEntry + { + $this->mountnames = $mountnames; + return $this; + } + + /** + * @return string + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName(?string $name): AssemblyBOMEntry + { + $this->name = $name; + return $this; + } + + public function getComment(): string + { + return $this->comment; + } + + public function setComment(string $comment): AssemblyBOMEntry + { + $this->comment = $comment; + return $this; + } + + public function getAssembly(): ?Assembly + { + return $this->assembly; + } + + public function setAssembly(?Assembly $assembly): AssemblyBOMEntry + { + $this->assembly = $assembly; + return $this; + } + + public function getPart(): ?Part + { + return $this->part; + } + + public function setPart(?Part $part): AssemblyBOMEntry + { + $this->part = $part; + return $this; + } + + /** + * Returns the price of this BOM entry, if existing. + * Prices are only valid on non-Part BOM entries. + */ + public function getPrice(): ?BigDecimal + { + return $this->price; + } + + /** + * Sets the price of this BOM entry. + * Prices are only valid on non-Part BOM entries. + */ + public function setPrice(?BigDecimal $price): void + { + $this->price = $price; + } + + public function getPriceCurrency(): ?Currency + { + return $this->price_currency; + } + + public function setPriceCurrency(?Currency $price_currency): void + { + $this->price_currency = $price_currency; + } + + /** + * Checks whether this BOM entry is a part associated BOM entry or not. + * @return bool True if this BOM entry is a part associated BOM entry, false otherwise. + */ + public function isPartBomEntry(): bool + { + return $this->part instanceof Part; + } + + #[Assert\Callback] + public function validate(ExecutionContextInterface $context, $payload): void + { + //Round quantity to whole numbers, if the part is not a decimal part + if ($this->part instanceof Part && (!$this->part->getPartUnit() || $this->part->getPartUnit()->isInteger())) { + $this->quantity = round($this->quantity); + } + //Non-Part BOM entries are rounded + if (!$this->part instanceof Part) { + $this->quantity = round($this->quantity); + } + + //Check that the part is not the build representation part of this assembly or one of its parents + if ($this->part && $this->part->getBuiltAssembly() instanceof Assembly) { + //Get the associated assembly + $associated_assembly = $this->part->getBuiltAssembly(); + //Check that it is not the same as the current assembly neither one of its parents + $current_assembly = $this->assembly; + while ($current_assembly) { + if ($associated_assembly === $current_assembly) { + $context->buildViolation('assembly.bom_entry.can_not_add_own_builds_part') + ->atPath('part') + ->addViolation(); + } + $current_assembly = $current_assembly->getParent(); + } + } + } + + + public function getComparableFields(): array + { + return [ + 'name' => $this->getName(), + 'part' => $this->getPart()?->getID(), + ]; + } +} diff --git a/src/Entity/Attachments/AssemblyAttachment.php b/src/Entity/Attachments/AssemblyAttachment.php new file mode 100644 index 000000000..bb9a11c8f --- /dev/null +++ b/src/Entity/Attachments/AssemblyAttachment.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Attachments; + +use App\Entity\AssemblySystem\Assembly; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; + +/** + * A attachment attached to a device element. + * @extends Attachment + */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] +class AssemblyAttachment extends Attachment +{ + final public const ALLOWED_ELEMENT_CLASS = Assembly::class; + /** + * @var Assembly|null the element this attachment is associated with + */ + #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AttachmentContainingDBElement $element = null; +} diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index 00cf581a8..808c60623 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -97,7 +97,7 @@ #[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)] abstract class Attachment extends AbstractNamedDBElement { - private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class, + private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class, 'Assembly' => AssemblyAttachment::class, 'AttachmentType' => AttachmentTypeAttachment::class, 'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class, 'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class, @@ -107,7 +107,8 @@ abstract class Attachment extends AbstractNamedDBElement /* * The discriminator map used for API platform. The key should be the same as the api platform short type (the @type JSONLD field). */ - private const API_DISCRIMINATOR_MAP = ["Part" => PartAttachment::class, "Project" => ProjectAttachment::class, "AttachmentType" => AttachmentTypeAttachment::class, + private const API_DISCRIMINATOR_MAP = ["Part" => PartAttachment::class, "Project" => ProjectAttachment::class, "Assembly" => AssemblyAttachment::class, + "AttachmentType" => AttachmentTypeAttachment::class, "Category" => CategoryAttachment::class, "Footprint" => FootprintAttachment::class, "Manufacturer" => ManufacturerAttachment::class, "Currency" => CurrencyAttachment::class, "Group" => GroupAttachment::class, "MeasurementUnit" => MeasurementUnitAttachment::class, "StorageLocation" => StorageLocationAttachment::class, "Supplier" => SupplierAttachment::class, "User" => UserAttachment::class, "LabelProfile" => LabelAttachment::class]; diff --git a/src/Entity/Base/AbstractDBElement.php b/src/Entity/Base/AbstractDBElement.php index 9fb5d6489..ad9c534b0 100644 --- a/src/Entity/Base/AbstractDBElement.php +++ b/src/Entity/Base/AbstractDBElement.php @@ -22,6 +22,9 @@ namespace App\Entity\Base; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentTypeAttachment; @@ -68,7 +71,7 @@ * Every database table which are managed with this class (or a subclass of it) * must have the table row "id"!! The ID is the unique key to identify the elements. */ -#[DiscriminatorMap(typeProperty: 'type', mapping: ['attachment_type' => AttachmentType::class, 'attachment' => Attachment::class, 'attachment_type_attachment' => AttachmentTypeAttachment::class, 'category_attachment' => CategoryAttachment::class, 'currency_attachment' => CurrencyAttachment::class, 'footprint_attachment' => FootprintAttachment::class, 'group_attachment' => GroupAttachment::class, 'label_attachment' => LabelAttachment::class, 'manufacturer_attachment' => ManufacturerAttachment::class, 'measurement_unit_attachment' => MeasurementUnitAttachment::class, 'part_attachment' => PartAttachment::class, 'project_attachment' => ProjectAttachment::class, 'storelocation_attachment' => StorageLocationAttachment::class, 'supplier_attachment' => SupplierAttachment::class, 'user_attachment' => UserAttachment::class, 'category' => Category::class, 'project' => Project::class, 'project_bom_entry' => ProjectBOMEntry::class, 'footprint' => Footprint::class, 'group' => Group::class, 'manufacturer' => Manufacturer::class, 'orderdetail' => Orderdetail::class, 'part' => Part::class, 'pricedetail' => Pricedetail::class, 'storelocation' => StorageLocation::class, 'part_lot' => PartLot::class, 'currency' => Currency::class, 'measurement_unit' => MeasurementUnit::class, 'parameter' => AbstractParameter::class, 'supplier' => Supplier::class, 'user' => User::class])] +#[DiscriminatorMap(typeProperty: 'type', mapping: ['attachment_type' => AttachmentType::class, 'attachment' => Attachment::class, 'attachment_type_attachment' => AttachmentTypeAttachment::class, 'category_attachment' => CategoryAttachment::class, 'currency_attachment' => CurrencyAttachment::class, 'footprint_attachment' => FootprintAttachment::class, 'group_attachment' => GroupAttachment::class, 'label_attachment' => LabelAttachment::class, 'manufacturer_attachment' => ManufacturerAttachment::class, 'measurement_unit_attachment' => MeasurementUnitAttachment::class, 'part_attachment' => PartAttachment::class, 'project_attachment' => ProjectAttachment::class, 'assembly_attachment' => AssemblyAttachment::class, 'storelocation_attachment' => StorageLocationAttachment::class, 'supplier_attachment' => SupplierAttachment::class, 'user_attachment' => UserAttachment::class, 'category' => Category::class, 'project' => Project::class, 'project_bom_entry' => ProjectBOMEntry::class, 'assembly' => Assembly::class, 'assembly_bom_entry' => AssemblyBOMEntry::class, 'footprint' => Footprint::class, 'group' => Group::class, 'manufacturer' => Manufacturer::class, 'orderdetail' => Orderdetail::class, 'part' => Part::class, 'pricedetail' => Pricedetail::class, 'storelocation' => StorageLocation::class, 'part_lot' => PartLot::class, 'currency' => Currency::class, 'measurement_unit' => MeasurementUnit::class, 'parameter' => AbstractParameter::class, 'supplier' => Supplier::class, 'user' => User::class])] #[ORM\MappedSuperclass(repositoryClass: DBElementRepository::class)] abstract class AbstractDBElement implements JsonSerializable { diff --git a/src/Entity/LogSystem/CollectionElementDeleted.php b/src/Entity/LogSystem/CollectionElementDeleted.php index 16bf33f59..be19bb0c8 100644 --- a/src/Entity/LogSystem/CollectionElementDeleted.php +++ b/src/Entity/LogSystem/CollectionElementDeleted.php @@ -41,6 +41,8 @@ namespace App\Entity\LogSystem; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentTypeAttachment; @@ -58,6 +60,7 @@ use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\LogWithEventUndoInterface; use App\Entity\Contracts\NamedElementInterface; +use App\Entity\Parameters\AssemblyParameter; use App\Entity\ProjectSystem\Project; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parameters\AttachmentTypeParameter; @@ -147,6 +150,7 @@ private function resolveAbstractClassToInstantiableClass(string $abstract_class) { if (is_a($abstract_class, AbstractParameter::class, true)) { return match ($this->getTargetClass()) { + Assembly::class => AssemblyParameter::class, AttachmentType::class => AttachmentTypeParameter::class, Category::class => CategoryParameter::class, Currency::class => CurrencyParameter::class, @@ -168,6 +172,7 @@ private function resolveAbstractClassToInstantiableClass(string $abstract_class) Category::class => CategoryAttachment::class, Currency::class => CurrencyAttachment::class, Project::class => ProjectAttachment::class, + Assembly::class => AssemblyAttachment::class, Footprint::class => FootprintAttachment::class, Group::class => GroupAttachment::class, Manufacturer::class => ManufacturerAttachment::class, diff --git a/src/Entity/LogSystem/LogTargetType.php b/src/Entity/LogSystem/LogTargetType.php index 1c6e4f8c0..2bccf14af 100644 --- a/src/Entity/LogSystem/LogTargetType.php +++ b/src/Entity/LogSystem/LogTargetType.php @@ -22,6 +22,8 @@ */ namespace App\Entity\LogSystem; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\LabelSystem\LabelProfile; @@ -67,6 +69,8 @@ enum LogTargetType: int case LABEL_PROFILE = 19; case PART_ASSOCIATION = 20; + case ASSEMBLY = 21; + case ASSEMBLY_BOM_ENTRY = 22; /** * Returns the class name of the target type or null if the target type is NONE. @@ -82,6 +86,8 @@ public function toClass(): ?string self::CATEGORY => Category::class, self::PROJECT => Project::class, self::BOM_ENTRY => ProjectBOMEntry::class, + self::ASSEMBLY => Assembly::class, + self::ASSEMBLY_BOM_ENTRY => AssemblyBOMEntry::class, self::FOOTPRINT => Footprint::class, self::GROUP => Group::class, self::MANUFACTURER => Manufacturer::class, diff --git a/src/Entity/Parameters/AbstractParameter.php b/src/Entity/Parameters/AbstractParameter.php index 39f333dad..b6ef0412b 100644 --- a/src/Entity/Parameters/AbstractParameter.php +++ b/src/Entity/Parameters/AbstractParameter.php @@ -73,7 +73,7 @@ #[ORM\DiscriminatorMap([0 => CategoryParameter::class, 1 => CurrencyParameter::class, 2 => ProjectParameter::class, 3 => FootprintParameter::class, 4 => GroupParameter::class, 5 => ManufacturerParameter::class, 6 => MeasurementUnitParameter::class, 7 => PartParameter::class, 8 => StorageLocationParameter::class, - 9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class])] + 9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class, 11 => AssemblyParameter::class])] #[ORM\Table('parameters')] #[ORM\Index(columns: ['name'], name: 'parameter_name_idx')] #[ORM\Index(columns: ['param_group'], name: 'parameter_group_idx')] @@ -103,7 +103,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu */ private const API_DISCRIMINATOR_MAP = ["Part" => PartParameter::class, "AttachmentType" => AttachmentTypeParameter::class, "Category" => CategoryParameter::class, "Currency" => CurrencyParameter::class, - "Project" => ProjectParameter::class, "Footprint" => FootprintParameter::class, "Group" => GroupParameter::class, + "Project" => ProjectParameter::class, "Assembly" => AssemblyParameter::class, "Footprint" => FootprintParameter::class, "Group" => GroupParameter::class, "Manufacturer" => ManufacturerParameter::class, "MeasurementUnit" => MeasurementUnitParameter::class, "StorageLocation" => StorageLocationParameter::class, "Supplier" => SupplierParameter::class]; diff --git a/src/Entity/Parameters/AssemblyParameter.php b/src/Entity/Parameters/AssemblyParameter.php new file mode 100644 index 000000000..349fa7906 --- /dev/null +++ b/src/Entity/Parameters/AssemblyParameter.php @@ -0,0 +1,65 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 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 . + */ + +namespace App\Entity\Parameters; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Base\AbstractDBElement; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; + +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] +class AssemblyParameter extends AbstractParameter +{ + final public const ALLOWED_ELEMENT_CLASS = Assembly::class; + + /** + * @var Assembly the element this para is associated with + */ + #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; +} diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index 14a7903fc..74bd3c3a4 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -23,6 +23,7 @@ namespace App\Entity\Parts; use App\ApiPlatform\Filter\TagFilter; +use App\Entity\Parts\PartTraits\AssemblyTrait; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; @@ -114,6 +115,7 @@ class Part extends AttachmentContainingDBElement use OrderTrait; use ParametersTrait; use ProjectTrait; + use AssemblyTrait; use AssociationTrait; use EDATrait; @@ -169,6 +171,7 @@ public function __construct() $this->orderdetails = new ArrayCollection(); $this->parameters = new ArrayCollection(); $this->project_bom_entries = new ArrayCollection(); + $this->assembly_bom_entries = new ArrayCollection(); $this->associated_parts_as_owner = new ArrayCollection(); $this->associated_parts_as_other = new ArrayCollection(); diff --git a/src/Entity/Parts/PartTraits/AssemblyTrait.php b/src/Entity/Parts/PartTraits/AssemblyTrait.php new file mode 100644 index 000000000..57f78d352 --- /dev/null +++ b/src/Entity/Parts/PartTraits/AssemblyTrait.php @@ -0,0 +1,83 @@ + $assembly_bom_entries + */ + #[ORM\OneToMany(mappedBy: 'part', targetEntity: AssemblyBOMEntry::class, cascade: ['remove'], orphanRemoval: true)] + protected Collection $assembly_bom_entries; + + /** + * @var Assembly|null If a assembly is set here, then this part is special and represents the builds of an assembly. + */ + #[ORM\OneToOne(inversedBy: 'build_part', targetEntity: Assembly::class)] + #[ORM\JoinColumn] + protected ?Assembly $built_assembly = null; + + /** + * Returns all AssemblyBOMEntry that use this part. + * + * @phpstan-return Collection + */ + public function getAssemblyBomEntries(): Collection + { + return $this->assembly_bom_entries; + } + + /** + * Checks whether this part represents the builds of a assembly + * @return bool True if it represents the builds, false if not + */ + #[Groups(['part:read'])] + public function isAssemblyBuildPart(): bool + { + return $this->built_assembly !== null; + } + + /** + * Returns the assembly that this part represents the builds of, or null if it doesn't + */ + public function getBuiltAssembly(): ?Assembly + { + return $this->built_assembly; + } + + + /** + * Sets the assembly that this part represents the builds of + * @param Assembly|null $built_assembly The assembly that this part represents the builds of, or null if it is not a build part + */ + public function setBuiltAssembly(?Assembly $built_assembly): self + { + $this->built_assembly = $built_assembly; + return $this; + } + + + /** + * Get all assemblies which uses this part. + * + * @return Assembly[] all assemblies which uses this part as a one-dimensional array of Assembly objects + */ + public function getAssemblies(): array + { + $assemblies = []; + + foreach($this->assembly_bom_entries as $entry) { + $assemblies[] = $entry->getAssembly(); + } + + return $assemblies; + } +} diff --git a/src/Entity/ProjectSystem/Project.php b/src/Entity/ProjectSystem/Project.php index a103d6946..36a96377b 100644 --- a/src/Entity/ProjectSystem/Project.php +++ b/src/Entity/ProjectSystem/Project.php @@ -108,6 +108,7 @@ class Project extends AbstractStructuralDBElement #[Groups(['extended', 'full', 'import'])] #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part'])] + #[UniqueObjectCollection(message: 'project.bom_entry.assembly_already_in_bom', fields: ['assembly'])] #[UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name'])] protected Collection $bom_entries; diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index 2a7862ec5..692407730 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -35,6 +35,7 @@ use ApiPlatform\OpenApi\Model\Operation; use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Contracts\TimeStampableInterface; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; @@ -103,7 +104,10 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte /** * @var string|null An optional name describing this BOM entry (useful for non-part entries) */ - #[Assert\Expression('this.getPart() !== null or this.getName() !== null', message: 'validator.project.bom_entry.name_or_part_needed')] + #[Assert\Expression( + 'this.getPart() !== null or this.getAssembly() !== null or (this.getName() !== null and this.getName() != "")', + message: 'validator.project.bom_entry.part_or_assembly_needed' + )] #[ORM\Column(type: Types::STRING, nullable: true)] #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] protected ?string $name = null; @@ -131,6 +135,18 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte #[Groups(['bom_entry:read', 'bom_entry:write', 'full'])] protected ?Part $part = null; + /** + * @var Assembly|null The associated assembly + */ + #[Assert\Expression( + '(this.getPart() === null or this.getAssembly() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))', + message: 'validator.project.bom_entry.only_part_or_assembly_allowed' + )] + #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'assembly_bom_entries')] + #[ORM\JoinColumn(name: 'id_assembly')] + #[Groups(['bom_entry:read', 'bom_entry:write', ])] + protected ?Assembly $assembly = null; + /** * @var BigDecimal|null The price of this non-part BOM entry */ @@ -212,8 +228,6 @@ public function setProject(?Project $project): ProjectBOMEntry return $this; } - - public function getPart(): ?Part { return $this->part; @@ -225,6 +239,16 @@ public function setPart(?Part $part): ProjectBOMEntry return $this; } + public function getAssembly(): ?Assembly + { + return $this->assembly; + } + + public function setAssembly(?Assembly $assembly): void + { + $this->assembly = $assembly; + } + /** * Returns the price of this BOM entry, if existing. * Prices are only valid on non-Part BOM entries. @@ -262,6 +286,15 @@ public function isPartBomEntry(): bool return $this->part instanceof Part; } + /** + * Checks whether this BOM entry is a assembly associated BOM entry or not. + * @return bool True if this BOM entry is a assembly associated BOM entry, false otherwise. + */ + public function isAssemblyBomEntry(): bool + { + return $this->assembly instanceof Assembly; + } + #[Assert\Callback] public function validate(ExecutionContextInterface $context, $payload): void { @@ -323,6 +356,7 @@ public function getComparableFields(): array return [ 'name' => $this->getName(), 'part' => $this->getPart()?->getID(), + 'assembly' => $this->getAssembly()?->getID(), ]; } } diff --git a/src/Form/AdminPages/AssemblyAdminForm.php b/src/Form/AdminPages/AssemblyAdminForm.php new file mode 100644 index 000000000..be1564d21 --- /dev/null +++ b/src/Form/AdminPages/AssemblyAdminForm.php @@ -0,0 +1,64 @@ +. + */ +namespace App\Form\AdminPages; + +use App\Entity\Base\AbstractNamedDBElement; +use App\Form\AssemblySystem\AssemblyBOMEntryCollectionType; +use App\Form\Type\RichTextEditorType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormBuilderInterface; + +class AssemblyAdminForm extends BaseEntityAdminForm +{ + protected function additionalFormElements(FormBuilderInterface $builder, array $options, AbstractNamedDBElement $entity): void + { + $builder->add('description', RichTextEditorType::class, [ + 'required' => false, + 'label' => 'part.edit.description', + 'mode' => 'markdown-single_line', + 'empty_data' => '', + 'attr' => [ + 'placeholder' => 'part.edit.description.placeholder', + 'rows' => 2, + ], + ]); + + $builder->add('bom_entries', AssemblyBOMEntryCollectionType::class); + + $builder->add('status', ChoiceType::class, [ + 'attr' => [ + 'class' => 'form-select', + ], + 'label' => 'assembly.edit.status', + 'required' => false, + 'empty_data' => '', + 'choices' => [ + 'assembly.status.draft' => 'draft', + 'assembly.status.planning' => 'planning', + 'assembly.status.in_production' => 'in_production', + 'assembly.status.finished' => 'finished', + 'assembly.status.archived' => 'archived', + ], + ]); + } +} diff --git a/src/Form/AdminPages/BaseEntityAdminForm.php b/src/Form/AdminPages/BaseEntityAdminForm.php index 5a4ef5bce..e5d69b35c 100644 --- a/src/Form/AdminPages/BaseEntityAdminForm.php +++ b/src/Form/AdminPages/BaseEntityAdminForm.php @@ -22,6 +22,7 @@ namespace App\Form\AdminPages; +use App\Entity\AssemblySystem\Assembly; use App\Entity\PriceInformations\Currency; use App\Entity\ProjectSystem\Project; use App\Entity\UserSystem\Group; @@ -114,7 +115,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ); } - if ($entity instanceof AbstractStructuralDBElement && !($entity instanceof Group || $entity instanceof Project || $entity instanceof Currency)) { + if ($entity instanceof AbstractStructuralDBElement && !($entity instanceof Group || $entity instanceof Project || $entity instanceof Assembly || $entity instanceof Currency)) { $builder->add('alternative_names', TextType::class, [ 'required' => false, 'label' => 'entity.edit.alternative_names.label', diff --git a/src/Form/AssemblySystem/AssemblyAddPartsType.php b/src/Form/AssemblySystem/AssemblyAddPartsType.php new file mode 100644 index 000000000..4d84881f0 --- /dev/null +++ b/src/Form/AssemblySystem/AssemblyAddPartsType.php @@ -0,0 +1,88 @@ +. + */ +namespace App\Form\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Form\Type\StructuralEntityType; +use App\Validator\Constraints\UniqueObjectCollection; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\NotNull; + +class AssemblyAddPartsType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('assembly', StructuralEntityType::class, [ + 'class' => Assembly::class, + 'required' => true, + 'disabled' => $options['assembly'] instanceof Assembly, //If a assembly is given, disable the field + 'data' => $options['assembly'], + 'constraints' => [ + new NotNull() + ] + ]); + $builder->add('bom_entries', AssemblyBOMEntryCollectionType::class, [ + 'entry_options' => [ + 'constraints' => [ + new UniqueEntity(fields: ['part', 'assembly'], message: 'assembly.bom_entry.part_already_in_bom', + entityClass: AssemblyBOMEntry::class), + new UniqueEntity(fields: ['name', 'assembly'], message: 'assembly.bom_entry.name_already_in_bom', + entityClass: AssemblyBOMEntry::class, ignoreNull: true), + ] + ], + 'constraints' => [ + new UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part']), + new UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name']), + ] + ]); + $builder->add('submit', SubmitType::class, ['label' => 'save']); + + //After submit set the assembly for all bom entries, so that it can be validated properly + $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) { + $form = $event->getForm(); + /** @var Assembly $assembly */ + $assembly = $form->get('assembly')->getData(); + $bom_entries = $form->get('bom_entries')->getData(); + + foreach ($bom_entries as $bom_entry) { + $bom_entry->setAssembly($assembly); + } + }); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'assembly' => null, + ]); + + $resolver->setAllowedTypes('assembly', ['null', Assembly::class]); + } +} diff --git a/src/Form/AssemblySystem/AssemblyBOMEntryCollectionType.php b/src/Form/AssemblySystem/AssemblyBOMEntryCollectionType.php new file mode 100644 index 000000000..04293f4e0 --- /dev/null +++ b/src/Form/AssemblySystem/AssemblyBOMEntryCollectionType.php @@ -0,0 +1,32 @@ +setDefaults([ + 'entry_type' => AssemblyBOMEntryType::class, + 'entry_options' => [ + 'label' => false, + ], + 'allow_add' => true, + 'allow_delete' => true, + 'by_reference' => false, + 'reindex_enable' => true, + 'label' => false, + ]); + } +} diff --git a/src/Form/AssemblySystem/AssemblyBOMEntryType.php b/src/Form/AssemblySystem/AssemblyBOMEntryType.php new file mode 100644 index 000000000..9addccb34 --- /dev/null +++ b/src/Form/AssemblySystem/AssemblyBOMEntryType.php @@ -0,0 +1,90 @@ +addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { + $form = $event->getForm(); + /** @var AssemblyBOMEntry $data */ + $data = $event->getData(); + + $form->add('quantity', SIUnitType::class, [ + 'label' => 'assembly.bom.quantity', + 'measurement_unit' => $data && $data->getPart() ? $data->getPart()->getPartUnit() : null, + ]); + }); + + $builder + + ->add('part', PartSelectType::class, [ + 'required' => false, + ]) + + ->add('name', TextType::class, [ + 'label' => 'assembly.bom.name', + 'required' => false, + ]) + ->add('mountnames', TextType::class, [ + 'required' => false, + 'label' => 'assembly.bom.mountnames', + 'empty_data' => '', + 'attr' => [ + 'class' => 'tagsinput', + 'data-controller' => 'elements--tagsinput', + ] + ]) + ->add('comment', RichTextEditorType::class, [ + 'required' => false, + 'label' => 'assembly.bom.comment', + 'empty_data' => '', + 'mode' => 'markdown-single_line', + 'attr' => [ + 'rows' => 2, + ], + ]) + ->add('price', BigDecimalNumberType::class, [ + 'label' => false, + 'required' => false, + 'scale' => 5, + 'html5' => true, + 'attr' => [ + 'min' => 0, + 'step' => 'any', + ], + ]) + ->add('priceCurrency', CurrencyEntityType::class, [ + 'required' => false, + 'label' => false, + 'short' => true, + ]) + + ; + + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => AssemblyBOMEntry::class, + ]); + } +} diff --git a/src/Form/AssemblySystem/AssemblyBuildType.php b/src/Form/AssemblySystem/AssemblyBuildType.php new file mode 100644 index 000000000..8838706d4 --- /dev/null +++ b/src/Form/AssemblySystem/AssemblyBuildType.php @@ -0,0 +1,183 @@ +. + */ +namespace App\Form\AssemblySystem; + +use App\Helpers\Assemblies\AssemblyBuildRequest; +use Symfony\Bundle\SecurityBundle\Security; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Form\Type\PartLotSelectType; +use App\Form\Type\SIUnitType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\Event\PreSetDataEvent; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class AssemblyBuildType extends AbstractType implements DataMapperInterface +{ + public function __construct(private readonly Security $security) + { + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => AssemblyBuildRequest::class + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->setDataMapper($this); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'assembly.build.btn_build', + 'disabled' => !$this->security->isGranted('@parts_stock.withdraw'), + ]); + + $builder->add('dontCheckQuantity', CheckboxType::class, [ + 'label' => 'assembly.build.dont_check_quantity', + 'help' => 'assembly.build.dont_check_quantity.help', + 'required' => false, + 'attr' => [ + 'data-controller' => 'pages--dont-check-quantity-checkbox' + ] + ]); + + $builder->add('comment', TextType::class, [ + 'label' => 'part.info.withdraw_modal.comment', + 'help' => 'part.info.withdraw_modal.comment.hint', + 'empty_data' => '', + 'required' => false, + ]); + + + //The form is initially empty, we have to define the fields after we know the data + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { + $form = $event->getForm(); + /** @var AssemblyBuildRequest $build_request */ + $build_request = $event->getData(); + + $form->add('addBuildsToBuildsPart', CheckboxType::class, [ + 'label' => 'assembly.build.add_builds_to_builds_part', + 'required' => false, + 'disabled' => !$build_request->getAssembly()->getBuildPart() instanceof Part, + ]); + + if ($build_request->getAssembly()->getBuildPart() instanceof Part) { + $form->add('buildsPartLot', PartLotSelectType::class, [ + 'label' => 'assembly.build.builds_part_lot', + 'required' => false, + 'part' => $build_request->getAssembly()->getBuildPart(), + 'placeholder' => 'assembly.build.buildsPartLot.new_lot' + ]); + } + + foreach ($build_request->getPartBomEntries() as $bomEntry) { + //Every part lot has a field to specify the number of parts to take from this lot + foreach ($build_request->getPartLotsForBOMEntry($bomEntry) as $lot) { + $form->add('lot_' . $lot->getID(), SIUnitType::class, [ + 'label' => false, + 'measurement_unit' => $bomEntry->getPart()->getPartUnit(), + 'max' => min($build_request->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()), + 'disabled' => !$this->security->isGranted('withdraw', $lot), + ]); + } + } + + }); + } + + public function mapDataToForms($data, \Traversable $forms): void + { + if (!$data instanceof AssemblyBuildRequest) { + throw new \RuntimeException('Data must be an instance of ' . AssemblyBuildRequest::class); + } + + /** @var FormInterface[] $forms */ + $forms = iterator_to_array($forms); + foreach ($forms as $key => $form) { + //Extract the lot id from the form name + $matches = []; + if (preg_match('/^lot_(\d+)$/', $key, $matches)) { + $lot_id = (int) $matches[1]; + $form->setData($data->getLotWithdrawAmount($lot_id)); + } + } + + $forms['comment']->setData($data->getComment()); + $forms['dontCheckQuantity']->setData($data->isDontCheckQuantity()); + $forms['addBuildsToBuildsPart']->setData($data->getAddBuildsToBuildsPart()); + if (isset($forms['buildsPartLot'])) { + $forms['buildsPartLot']->setData($data->getBuildsPartLot()); + } + + } + + public function mapFormsToData(\Traversable $forms, &$data): void + { + if (!$data instanceof AssemblyBuildRequest) { + throw new \RuntimeException('Data must be an instance of ' . AssemblyBuildRequest::class); + } + + /** @var FormInterface[] $forms */ + $forms = iterator_to_array($forms); + + foreach ($forms as $key => $form) { + //Extract the lot id from the form name + $matches = []; + if (preg_match('/^lot_(\d+)$/', $key, $matches)) { + $lot_id = (int) $matches[1]; + $data->setLotWithdrawAmount($lot_id, (float) $form->getData()); + } + } + + $data->setComment($forms['comment']->getData()); + $data->setDontCheckQuantity($forms['dontCheckQuantity']->getData()); + + if (isset($forms['buildsPartLot'])) { + $lot = $forms['buildsPartLot']->getData(); + if (!$lot) { //When the user selected "Create new lot", create a new lot + $lot = new PartLot(); + $description = 'Build ' . date('Y-m-d H:i:s'); + if ($data->getComment() !== '') { + $description .= ' (' . $data->getComment() . ')'; + } + $lot->setDescription($description); + + $data->getAssembly()->getBuildPart()->addPartLot($lot); + } + + $data->setBuildsPartLot($lot); + } + //This has to be set after the builds part lot, so that it can disable the option + $data->setAddBuildsToBuildsPart($forms['addBuildsToBuildsPart']->getData()); + } +} diff --git a/src/Form/Filters/AttachmentFilterType.php b/src/Form/Filters/AttachmentFilterType.php index ff80bd384..a44588955 100644 --- a/src/Form/Filters/AttachmentFilterType.php +++ b/src/Form/Filters/AttachmentFilterType.php @@ -23,6 +23,7 @@ namespace App\Form\Filters; use App\DataTables\Filters\AttachmentFilter; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Attachments\CategoryAttachment; @@ -80,6 +81,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'category.label' => CategoryAttachment::class, 'currency.label' => CurrencyAttachment::class, 'project.label' => ProjectAttachment::class, + 'assembly.label' => AssemblyAttachment::class, 'footprint.label' => FootprintAttachment::class, 'group.label' => GroupAttachment::class, 'label_profile.label' => LabelAttachment::class, diff --git a/src/Form/ProjectSystem/ProjectAddPartsType.php b/src/Form/ProjectSystem/ProjectAddPartsType.php index 61f72c41d..c5dbe99f3 100644 --- a/src/Form/ProjectSystem/ProjectAddPartsType.php +++ b/src/Form/ProjectSystem/ProjectAddPartsType.php @@ -59,6 +59,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ], 'constraints' => [ new UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part']), + new UniqueObjectCollection(message: 'project.bom_entry.assembly_already_in_bom', fields: ['assembly']), new UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name']), ] ]); diff --git a/src/Form/ProjectSystem/ProjectBOMEntryType.php b/src/Form/ProjectSystem/ProjectBOMEntryType.php index cac362fbb..de8eb789c 100644 --- a/src/Form/ProjectSystem/ProjectBOMEntryType.php +++ b/src/Form/ProjectSystem/ProjectBOMEntryType.php @@ -5,6 +5,7 @@ namespace App\Form\ProjectSystem; use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Form\Type\AssemblySelectType; use App\Form\Type\BigDecimalNumberType; use App\Form\Type\CurrencyEntityType; use App\Form\Type\PartSelectType; @@ -22,8 +23,6 @@ class ProjectBOMEntryType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { - - $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { $form = $event->getForm(); /** @var ProjectBOMEntry $data */ @@ -36,11 +35,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void }); $builder - ->add('part', PartSelectType::class, [ + 'label' => 'project.bom.part', + 'required' => false, + ]) + ->add('assembly', AssemblySelectType::class, [ + 'label' => 'project.bom.assembly', 'required' => false, ]) - ->add('name', TextType::class, [ 'label' => 'project.bom.name', 'required' => false, @@ -77,10 +79,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'label' => false, 'short' => true, - ]) - - ; - + ]); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Form/ProjectSystem/ProjectBuildType.php b/src/Form/ProjectSystem/ProjectBuildType.php index 2b7b52e28..d0d4e3433 100644 --- a/src/Form/ProjectSystem/ProjectBuildType.php +++ b/src/Form/ProjectSystem/ProjectBuildType.php @@ -22,6 +22,7 @@ */ namespace App\Form\ProjectSystem; +use App\Helpers\Assemblies\AssemblyBuildRequest; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; @@ -38,10 +39,11 @@ use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; class ProjectBuildType extends AbstractType implements DataMapperInterface { - public function __construct(private readonly Security $security) + public function __construct(private readonly Security $security, private readonly TranslatorInterface $translator) { } @@ -82,36 +84,54 @@ public function buildForm(FormBuilderInterface $builder, array $options): void //The form is initially empty, we have to define the fields after we know the data $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { $form = $event->getForm(); - /** @var ProjectBuildRequest $build_request */ - $build_request = $event->getData(); + /** @var ProjectBuildRequest $projectBuildRequest */ + $projectBuildRequest = $event->getData(); $form->add('addBuildsToBuildsPart', CheckboxType::class, [ 'label' => 'project.build.add_builds_to_builds_part', 'required' => false, - 'disabled' => !$build_request->getProject()->getBuildPart() instanceof Part, + 'disabled' => !$projectBuildRequest->getProject()->getBuildPart() instanceof Part, ]); - if ($build_request->getProject()->getBuildPart() instanceof Part) { + if ($projectBuildRequest->getProject()->getBuildPart() instanceof Part) { $form->add('buildsPartLot', PartLotSelectType::class, [ 'label' => 'project.build.builds_part_lot', 'required' => false, - 'part' => $build_request->getProject()->getBuildPart(), + 'part' => $projectBuildRequest->getProject()->getBuildPart(), 'placeholder' => 'project.build.buildsPartLot.new_lot' ]); } - foreach ($build_request->getPartBomEntries() as $bomEntry) { + foreach ($projectBuildRequest->getPartBomEntries() as $bomEntry) { //Every part lot has a field to specify the number of parts to take from this lot - foreach ($build_request->getPartLotsForBOMEntry($bomEntry) as $lot) { + foreach ($projectBuildRequest->getPartLotsForBOMEntry($bomEntry) as $lot) { $form->add('lot_' . $lot->getID(), SIUnitType::class, [ 'label' => false, 'measurement_unit' => $bomEntry->getPart()->getPartUnit(), - 'max' => min($build_request->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()), + 'max' => min($projectBuildRequest->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()), 'disabled' => !$this->security->isGranted('withdraw', $lot), ]); } } + foreach ($projectBuildRequest->getAssemblyBomEntries() as $bomEntry) { + $assemblyBuildRequest = new AssemblyBuildRequest($bomEntry->getAssembly(), $projectBuildRequest->getNumberOfBuilds()); + + //Add fields for assembly bom entries + foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) { + foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $lot) { + $form->add('lot_' . $lot->getID(), SIUnitType::class, [ + 'label' => $this->translator->trans('project.build.builds_part_lot_label', [ + '%name%' => $partBomEntry->getPart()->getName(), + '%quantity%' => $partBomEntry->getQuantity() * $projectBuildRequest->getNumberOfBuilds() + ]), + 'measurement_unit' => $partBomEntry->getPart()->getPartUnit(), + 'max' => min($assemblyBuildRequest->getNeededAmountForBOMEntry($partBomEntry), $lot->getAmount()), + 'disabled' => !$this->security->isGranted('withdraw', $lot), + ]); + } + } + } }); } diff --git a/src/Form/Type/AssemblySelectType.php b/src/Form/Type/AssemblySelectType.php new file mode 100644 index 000000000..ee6cf7c2a --- /dev/null +++ b/src/Form/Type/AssemblySelectType.php @@ -0,0 +1,125 @@ +addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { + $form = $event->getForm(); + $config = $form->getConfig()->getOptions(); + $data = $event->getData() ?? []; + + $config['compound'] = false; + $config['choices'] = is_iterable($data) ? $data : [$data]; + $config['error_bubbling'] = true; + + $form->add('autocomplete', EntityType::class, $config); + }); + + //After form submit, we have to add the selected element as choice, otherwise the form will not accept this element + $builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) { + $data = $event->getData(); + $form = $event->getForm(); + $options = $form->get('autocomplete')->getConfig()->getOptions(); + + + if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) { + $options['choices'] = []; + } else { + //Extract the ID from the submitted data + $id = $data['autocomplete']; + //Find the element in the database + $element = $this->em->find($options['class'], $id); + + //Add the element as choice + $options['choices'] = [$element]; + $options['error_bubbling'] = true; + $form->add('autocomplete', EntityType::class, $options); + } + }); + + $builder->setDataMapper($this); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'class' => Assembly::class, + 'choice_label' => 'name', + 'placeholder' => 'None', + 'compound' => true, + 'error_bubbling' => false, + ]); + + error_log($this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__'])); + + $resolver->setDefaults([ + 'attr' => [ + 'data-controller' => 'elements--assembly-select', + 'data-autocomplete' => $this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']), + 'autocomplete' => 'off', + ], + ]); + + $resolver->setDefaults([ + //Prefill the selected choice with the needed data, so the user can see it without an additional Ajax request + 'choice_attr' => ChoiceList::attr($this, function (?Assembly $assembly) { + if($assembly instanceof Assembly) { + //Determine the picture to show: + $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($assembly); + if ($preview_attachment instanceof Attachment) { + $preview_url = $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, + 'thumbnail_sm'); + } else { + $preview_url = ''; + } + } + + return $assembly instanceof Assembly ? [ + 'data-description' => $assembly->getDescription() ? mb_strimwidth($assembly->getDescription(), 0, 127, '...') : '', + 'data-category' => '', + 'data-footprint' => '', + 'data-image' => $preview_url, + ] : []; + }) + ]); + } + + public function mapDataToForms($data, \Traversable $forms): void + { + $form = current(iterator_to_array($forms, false)); + $form->setData($data); + } + + public function mapFormsToData(\Traversable $forms, &$data): void + { + $form = current(iterator_to_array($forms, false)); + $data = $form->getData(); + } + +} diff --git a/src/Form/Type/PartSelectType.php b/src/Form/Type/PartSelectType.php index 34b8fc7c4..c41d6b8f9 100644 --- a/src/Form/Type/PartSelectType.php +++ b/src/Form/Type/PartSelectType.php @@ -50,7 +50,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $options = $form->get('autocomplete')->getConfig()->getOptions(); - if (!isset($data['autocomplete']) || '' === $data['autocomplete']) { + if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) { $options['choices'] = []; } else { //Extract the ID from the submitted data @@ -84,7 +84,6 @@ public function configureOptions(OptionsResolver $resolver): void 'data-autocomplete' => $this->urlGenerator->generate('typeahead_parts', ['query' => '__QUERY__']), //Disable browser autocomplete 'autocomplete' => 'off', - ], ]); @@ -103,7 +102,7 @@ public function configureOptions(OptionsResolver $resolver): void } return $part instanceof Part ? [ - 'data-description' => mb_strimwidth($part->getDescription(), 0, 127, '...'), + 'data-description' => $part->getDescription() ? mb_strimwidth($part->getDescription(), 0, 127, '...') : '', 'data-category' => $part->getCategory() instanceof Category ? $part->getCategory()->getName() : '', 'data-footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '', 'data-image' => $preview_url, diff --git a/src/Helpers/Assemblies/AssemblyBuildRequest.php b/src/Helpers/Assemblies/AssemblyBuildRequest.php new file mode 100644 index 000000000..c33a6f612 --- /dev/null +++ b/src/Helpers/Assemblies/AssemblyBuildRequest.php @@ -0,0 +1,306 @@ +. + */ +namespace App\Helpers\Assemblies; + +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Validator\Constraints\AssemblySystem\ValidAssemblyBuildRequest; + +/** + * @see \App\Tests\Helpers\Assemblies\AssemblyBuildRequestTest + */ +#[ValidAssemblyBuildRequest] +final class AssemblyBuildRequest +{ + private readonly int $number_of_builds; + + /** + * @var array + */ + private array $withdraw_amounts = []; + + private string $comment = ''; + + private ?PartLot $builds_lot = null; + + private bool $add_build_to_builds_part = false; + + private bool $dont_check_quantity = false; + + /** + * @param Assembly $assembly The assembly that should be build + * @param int $number_of_builds The number of builds that should be created + */ + public function __construct(private readonly Assembly $assembly, int $number_of_builds) + { + if ($number_of_builds < 1) { + throw new \InvalidArgumentException('Number of builds must be at least 1!'); + } + $this->number_of_builds = $number_of_builds; + + $this->initializeArray(); + + //By default, use the first available lot of builds part if there is one. + if($assembly->getBuildPart() instanceof Part) { + $this->add_build_to_builds_part = true; + foreach( $assembly->getBuildPart()->getPartLots() as $lot) { + if (!$lot->isInstockUnknown()) { + $this->builds_lot = $lot; + break; + } + } + } + } + + private function initializeArray(): void + { + //Completely reset the array + $this->withdraw_amounts = []; + + //Now create an array for each BOM entry + foreach ($this->getPartBomEntries() as $bom_entry) { + $remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry); + foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) { + //If the lot has instock use it for the build + $this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount()); + $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]); + } + } + } + + /** + * Ensure that the assemblyBOMEntry belongs to the assembly, otherwise throw an exception. + */ + private function ensureBOMEntryValid(AssemblyBOMEntry $entry): void + { + if ($entry->getAssembly() !== $this->assembly) { + throw new \InvalidArgumentException('The given BOM entry does not belong to the assembly!'); + } + } + + /** + * Returns the partlot where the builds should be added to, or null if it should not be added to any lot. + */ + public function getBuildsPartLot(): ?PartLot + { + return $this->builds_lot; + } + + /** + * Return if the builds should be added to the builds part of this assembly as new stock + */ + public function getAddBuildsToBuildsPart(): bool + { + return $this->add_build_to_builds_part; + } + + /** + * Set if the builds should be added to the builds part of this assembly as new stock + * @return $this + */ + public function setAddBuildsToBuildsPart(bool $new_value): self + { + $this->add_build_to_builds_part = $new_value; + + if ($new_value === false) { + $this->builds_lot = null; + } + + return $this; + } + + /** + * Set the partlot where the builds should be added to, or null if it should not be added to any lot. + * The part lot must belong to the assembly build part, or an exception is thrown! + * @return $this + */ + public function setBuildsPartLot(?PartLot $new_part_lot): self + { + //Ensure that this new_part_lot belongs to the assembly + if (($new_part_lot instanceof PartLot && $new_part_lot->getPart() !== $this->assembly->getBuildPart()) || !$this->assembly->getBuildPart() instanceof Part) { + throw new \InvalidArgumentException('The given part lot does not belong to the assemblies build part!'); + } + + if ($new_part_lot instanceof PartLot) { + $this->setAddBuildsToBuildsPart(true); + } + + $this->builds_lot = $new_part_lot; + + return $this; + } + + /** + * Returns the comment where the user can write additional information about the build. + */ + public function getComment(): string + { + return $this->comment; + } + + /** + * Sets the comment where the user can write additional information about the build. + */ + public function setComment(string $comment): void + { + $this->comment = $comment; + } + + /** + * Returns the amount of parts that should be withdrawn from the given lot for the corresponding BOM entry. + * @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdrawal amount should be got + */ + public function getLotWithdrawAmount(PartLot|int $lot): float + { + $lot_id = $lot instanceof PartLot ? $lot->getID() : $lot; + + if (! array_key_exists($lot_id, $this->withdraw_amounts)) { + throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!'); + } + + return $this->withdraw_amounts[$lot_id]; + } + + /** + * Sets the amount of parts that should be withdrawn from the given lot for the corresponding BOM entry. + * @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdrawal amount should be got + * @return $this + */ + public function setLotWithdrawAmount(PartLot|int $lot, float $amount): self + { + if ($lot instanceof PartLot) { + $lot_id = $lot->getID(); + } elseif (is_int($lot)) { + $lot_id = $lot; + } else { + throw new \InvalidArgumentException('The given lot must be an instance of PartLot or an ID of a PartLot!'); + } + + $this->withdraw_amounts[$lot_id] = $amount; + + return $this; + } + + /** + * Returns the sum of all withdraw amounts for the given BOM entry. + */ + public function getWithdrawAmountSum(AssemblyBOMEntry $entry): float + { + $this->ensureBOMEntryValid($entry); + + $sum = 0; + foreach ($this->getPartLotsForBOMEntry($entry) as $lot) { + $sum += $this->getLotWithdrawAmount($lot); + } + + if ($entry->getPart() && !$entry->getPart()->useFloatAmount()) { + $sum = round($sum); + } + + return $sum; + } + + /** + * Returns the number of available lots to take stock from for the given BOM entry. + * @return PartLot[]|null Returns null if the entry is a non-part BOM entry + */ + public function getPartLotsForBOMEntry(AssemblyBOMEntry $assemblyBOMEntry): ?array + { + $this->ensureBOMEntryValid($assemblyBOMEntry); + + if (!$assemblyBOMEntry->getPart() instanceof Part) { + return null; + } + + //Filter out all lots which have unknown instock + return $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); + } + + /** + * Returns the needed amount of parts for the given BOM entry. + */ + public function getNeededAmountForBOMEntry(AssemblyBOMEntry $entry): float + { + $this->ensureBOMEntryValid($entry); + + return $entry->getQuantity() * $this->number_of_builds; + } + + /** + * Returns the list of all bom entries. + * @return AssemblyBOMEntry[] + */ + public function getBomEntries(): array + { + return $this->assembly->getBomEntries()->toArray(); + } + + /** + * Returns all part bom entries. + * @return AssemblyBOMEntry[] + */ + public function getPartBomEntries(): array + { + return $this->assembly->getBomEntries()->filter(fn(AssemblyBOMEntry $entry) => $entry->isPartBomEntry())->toArray(); + } + + /** + * Returns which assembly should be build + */ + public function getAssembly(): Assembly + { + return $this->assembly; + } + + /** + * Returns the number of builds that should be created. + */ + public function getNumberOfBuilds(): int + { + return $this->number_of_builds; + } + + /** + * If Set to true, the given withdraw amounts are used without any checks for requirements. + * @return bool + */ + public function isDontCheckQuantity(): bool + { + return $this->dont_check_quantity; + } + + /** + * Set to true, the given withdraw amounts are used without any checks for requirements. + * @param bool $dont_check_quantity + * @return $this + */ + public function setDontCheckQuantity(bool $dont_check_quantity): AssemblyBuildRequest + { + $this->dont_check_quantity = $dont_check_quantity; + return $this; + } + + +} diff --git a/src/Helpers/Projects/ProjectBuildRequest.php b/src/Helpers/Projects/ProjectBuildRequest.php index 430d37b56..3254565a5 100644 --- a/src/Helpers/Projects/ProjectBuildRequest.php +++ b/src/Helpers/Projects/ProjectBuildRequest.php @@ -22,10 +22,13 @@ */ namespace App\Helpers\Projects; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Helpers\Assemblies\AssemblyBuildRequest; use App\Validator\Constraints\ProjectSystem\ValidProjectBuildRequest; /** @@ -79,7 +82,7 @@ private function initializeArray(): void //Completely reset the array $this->withdraw_amounts = []; - //Now create an array for each BOM entry + //Now create an array for each part BOM entry foreach ($this->getPartBomEntries() as $bom_entry) { $remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry); foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) { @@ -88,6 +91,21 @@ private function initializeArray(): void $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]); } } + + //Now create an array for each assembly BOM entry + foreach ($this->getAssemblyBomEntries() as $assemblyBomEntry) { + $assemblyBuildRequest = new AssemblyBuildRequest($assemblyBomEntry->getAssembly(), $this->number_of_builds); + + //Add fields for assembly bom entries + foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) { + $remaining_amount = $assemblyBuildRequest->getNeededAmountForBOMEntry($partBomEntry) * $assemblyBomEntry->getQuantity(); + + foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $lot) { + $this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount()); + $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]); + } + } + } } /** @@ -230,12 +248,77 @@ public function getPartLotsForBOMEntry(ProjectBOMEntry $projectBOMEntry): ?array { $this->ensureBOMEntryValid($projectBOMEntry); - if (!$projectBOMEntry->getPart() instanceof Part) { + if (!$projectBOMEntry->getPart() instanceof Part && !$projectBOMEntry->getAssembly() instanceof Assembly) { return null; } //Filter out all lots which have unknown instock - return $projectBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); + if ($projectBOMEntry->getPart() instanceof Part) { + return $projectBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); + } elseif ($projectBOMEntry->getAssembly() instanceof Assembly) { + $assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds); + + //Add fields for assembly bom entries + $result = []; + foreach ($assemblyBuildRequest->getPartBomEntries() as $assemblyBOMEntry) { + $tmp = $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); + $result = array_merge($result, $tmp); + } + + return $result; + } + + return null; + } + + /** + * Returns all available assembly BOM-entries with no part assigned. + * @return AssemblyBOMEntry[]|null Returns null if no entries found + */ + public function getAssemblyBomEntriesWithoutPart(ProjectBOMEntry $projectBOMEntry): ?array + { + $this->ensureBOMEntryValid($projectBOMEntry); + + if (!$projectBOMEntry->getAssembly() instanceof Assembly) { + return null; + } + + $assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds); + + $result = []; + + foreach ($assemblyBuildRequest->getBomEntries() as $assemblyBOMEntry) { + if ($assemblyBOMEntry->getPart() === null) { + $result[] = $assemblyBOMEntry; + } + } + + return count($result) > 0 ? $result : null; + } + + /** + * Returns all available assembly BOM-entries with no part assigned. + * @return AssemblyBOMEntry[]|null Returns null if no entries found + */ + public function getAssemblyBomEntriesWithPartNoStock(ProjectBOMEntry $projectBOMEntry): ?array + { + $this->ensureBOMEntryValid($projectBOMEntry); + + if (!$projectBOMEntry->getAssembly() instanceof Assembly) { + return null; + } + + $assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds); + + $result = []; + + foreach ($assemblyBuildRequest->getBomEntries() as $assemblyBOMEntry) { + if ($assemblyBOMEntry->getPart() instanceof Part && $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->count() === 0) { + $result[] = $assemblyBOMEntry; + } + } + + return count($result) > 0 ? $result : null; } /** @@ -266,6 +349,15 @@ public function getPartBomEntries(): array return $this->project->getBomEntries()->filter(fn(ProjectBOMEntry $entry) => $entry->isPartBomEntry())->toArray(); } + /** + * Returns the all assembly bom entries that have to be built. + * @return ProjectBOMEntry[] + */ + public function getAssemblyBomEntries(): array + { + return $this->project->getBomEntries()->filter(fn(ProjectBOMEntry $entry) => $entry->isAssemblyBomEntry())->toArray(); + } + /** * Returns which project should be build */ @@ -301,6 +393,4 @@ public function setDontCheckQuantity(bool $dont_check_quantity): ProjectBuildReq $this->dont_check_quantity = $dont_check_quantity; return $this; } - - -} +} \ No newline at end of file diff --git a/src/Repository/AssemblyRepository.php b/src/Repository/AssemblyRepository.php new file mode 100644 index 000000000..031e6e82b --- /dev/null +++ b/src/Repository/AssemblyRepository.php @@ -0,0 +1,69 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 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 . + */ + +namespace App\Repository; + +use App\Entity\AssemblySystem\Assembly; + +/** + * @template TEntityClass of Assembly + * @extends DBElementRepository + */ +class AssemblyRepository extends StructuralDBElementRepository +{ + /** + * @return Assembly[] + */ + public function autocompleteSearch(string $query, int $max_limits = 50): array + { + $qb = $this->createQueryBuilder('assembly'); + $qb->select('assembly') + ->where('ILIKE(assembly.name, :query) = TRUE') + ->orWhere('ILIKE(assembly.description, :query) = TRUE'); + + $qb->setParameter('query', '%'.$query.'%'); + + $qb->setMaxResults($max_limits); + $qb->orderBy('NATSORT(assembly.name)', 'ASC'); + + return $qb->getQuery()->getResult(); + } +} \ No newline at end of file diff --git a/src/Repository/DBElementRepository.php b/src/Repository/DBElementRepository.php index 2437e8488..23ad296aa 100644 --- a/src/Repository/DBElementRepository.php +++ b/src/Repository/DBElementRepository.php @@ -154,4 +154,14 @@ protected function setField(AbstractDBElement $element, string $field, int $new_ $property->setAccessible(true); $property->setValue($element, $new_value); } + + protected function save(AbstractDBElement $entity, bool $flush = true): void + { + $manager = $this->getEntityManager(); + $manager->persist($entity); + + if ($flush) { + $manager->flush(); + } + } } diff --git a/src/Security/Voter/AttachmentVoter.php b/src/Security/Voter/AttachmentVoter.php index bd7ae4df8..766aaeac4 100644 --- a/src/Security/Voter/AttachmentVoter.php +++ b/src/Security/Voter/AttachmentVoter.php @@ -22,6 +22,7 @@ namespace App\Security\Voter; +use App\Entity\Attachments\AssemblyAttachment; use App\Services\UserSystem\VoterHelper; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Attachments\AttachmentContainingDBElement; @@ -89,6 +90,8 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $ $param = 'currencies'; } elseif (is_a($subject, ProjectAttachment::class, true)) { $param = 'projects'; + } elseif (is_a($subject, AssemblyAttachment::class, true)) { + $param = 'assemblies'; } elseif (is_a($subject, FootprintAttachment::class, true)) { $param = 'footprints'; } elseif (is_a($subject, GroupAttachment::class, true)) { diff --git a/src/Security/Voter/StructureVoter.php b/src/Security/Voter/StructureVoter.php index ad0299a79..8079757cd 100644 --- a/src/Security/Voter/StructureVoter.php +++ b/src/Security/Voter/StructureVoter.php @@ -22,6 +22,7 @@ namespace App\Security\Voter; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\AttachmentType; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; @@ -47,6 +48,7 @@ final class StructureVoter extends Voter AttachmentType::class => 'attachment_types', Category::class => 'categories', Project::class => 'projects', + Assembly::class => 'assemblies', Footprint::class => 'footprints', Manufacturer::class => 'manufacturers', StorageLocation::class => 'storelocations', diff --git a/src/Services/AssemblySystem/AssemblyBuildHelper.php b/src/Services/AssemblySystem/AssemblyBuildHelper.php new file mode 100644 index 000000000..8c95a4b66 --- /dev/null +++ b/src/Services/AssemblySystem/AssemblyBuildHelper.php @@ -0,0 +1,154 @@ +. + */ +namespace App\Services\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Parts\Part; +use App\Helpers\Assemblies\AssemblyBuildRequest; +use App\Services\Parts\PartLotWithdrawAddHelper; + +/** + * @see \App\Tests\Services\AssemblySystem\AssemblyBuildHelperTest + */ +class AssemblyBuildHelper +{ + public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper) + { + } + + /** + * Returns the maximum buildable amount of the given BOM entry based on the stock of the used parts. + * This function only works for BOM entries that are associated with a part. + */ + public function getMaximumBuildableCountForBOMEntry(AssemblyBOMEntry $assemblyBOMEntry): int + { + $part = $assemblyBOMEntry->getPart(); + + if (!$part instanceof Part) { + throw new \InvalidArgumentException('This function cannot determine the maximum buildable count for a BOM entry without a part!'); + } + + if ($assemblyBOMEntry->getQuantity() <= 0) { + throw new \RuntimeException('The quantity of the BOM entry must be greater than 0!'); + } + + $amount_sum = $part->getAmountSum(); + + return (int) floor($amount_sum / $assemblyBOMEntry->getQuantity()); + } + + /** + * Returns the maximum buildable amount of the given assembly, based on the stock of the used parts in the BOM. + */ + public function getMaximumBuildableCount(Assembly $assembly): int + { + $maximum_buildable_count = PHP_INT_MAX; + foreach ($assembly->getBomEntries() as $bom_entry) { + //Skip BOM entries without a part (as we can not determine that) + if (!$bom_entry->isPartBomEntry()) { + continue; + } + + //The maximum buildable count for the whole assembly is the minimum of all BOM entries + $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); + } + + return $maximum_buildable_count; + } + + /** + * Checks if the given assembly can be built with the current stock. + * This means that the maximum buildable count is greater or equal than the requested $number_of_assemblies + * @param int $number_of_builds + */ + public function isAssemblyBuildable(Assembly $assembly, int $number_of_builds = 1): bool + { + return $this->getMaximumBuildableCount($assembly) >= $number_of_builds; + } + + /** + * Check if the given BOM entry can be built with the current stock. + * This means that the maximum buildable count is greater or equal than the requested $number_of_assemblies + */ + public function isBOMEntryBuildable(AssemblyBOMEntry $bom_entry, int $number_of_builds = 1): bool + { + return $this->getMaximumBuildableCountForBOMEntry($bom_entry) >= $number_of_builds; + } + + /** + * Returns the assembly BOM entries for which parts are missing in the stock for the given number of builds + * @param Assembly $assembly The assembly for which the BOM entries should be checked + * @param int $number_of_builds How often should the assembly be build? + * @return AssemblyBOMEntry[] + */ + public function getNonBuildableAssemblyBomEntries(Assembly $assembly, int $number_of_builds = 1): array + { + if ($number_of_builds < 1) { + throw new \InvalidArgumentException('The number of builds must be greater than 0!'); + } + + $non_buildable_entries = []; + + foreach ($assembly->getBomEntries() as $bomEntry) { + $part = $bomEntry->getPart(); + + //Skip BOM entries without a part (as we can not determine that) + if (!$part instanceof Part) { + continue; + } + + $amount_sum = $part->getAmountSum(); + + if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { + $non_buildable_entries[] = $bomEntry; + } + } + + return $non_buildable_entries; + } + + /** + * Withdraw the parts from the stock using the given AssemblyBuildRequest and create the build parts entries, if needed. + * The AssemblyBuildRequest has to be validated before!! + * You have to flush changes to DB afterward + */ + public function doBuild(AssemblyBuildRequest $buildRequest): void + { + $message = $buildRequest->getComment(); + $message .= ' (Assembly build: '.$buildRequest->getAssembly()->getName().')'; + + foreach ($buildRequest->getPartBomEntries() as $bom_entry) { + foreach ($buildRequest->getPartLotsForBOMEntry($bom_entry) as $part_lot) { + $amount = $buildRequest->getLotWithdrawAmount($part_lot); + if ($amount > 0) { + $this->withdraw_add_helper->withdraw($part_lot, $amount, $message); + } + } + } + + if ($buildRequest->getAddBuildsToBuildsPart()) { + $this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message); + } + } +} diff --git a/src/Services/AssemblySystem/AssemblyBuildPartHelper.php b/src/Services/AssemblySystem/AssemblyBuildPartHelper.php new file mode 100644 index 000000000..9a5503505 --- /dev/null +++ b/src/Services/AssemblySystem/AssemblyBuildPartHelper.php @@ -0,0 +1,40 @@ +setBuiltAssembly($assembly); + + //Set the name of the part to the name of the assembly + $part->setName($assembly->getName()); + + //Set the description of the part to the description of the assembly + $part->setDescription($assembly->getDescription()); + + //Add a tag to the part that indicates that it is a build part + $part->setTags('assembly-build'); + + //Associate the part with the assembly + $assembly->setBuildPart($part); + + return $part; + } +} diff --git a/src/Services/Attachments/AssemblyPreviewGenerator.php b/src/Services/Attachments/AssemblyPreviewGenerator.php new file mode 100644 index 000000000..9ecbbd070 --- /dev/null +++ b/src/Services/Attachments/AssemblyPreviewGenerator.php @@ -0,0 +1,93 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\Attachments; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\Attachment; + +class AssemblyPreviewGenerator +{ + public function __construct(protected AttachmentManager $attachmentHelper) + { + } + + /** + * Returns a list of attachments that can be used for previewing the assembly ordered by priority. + * + * @param Assembly $assembly the assembly for which the attachments should be determined + * + * @return (Attachment|null)[] + * + * @psalm-return list + */ + public function getPreviewAttachments(Assembly $assembly): array + { + $list = []; + + //Master attachment has top priority + $attachment = $assembly->getMasterPictureAttachment(); + if ($this->isAttachmentValidPicture($attachment)) { + $list[] = $attachment; + } + + //Then comes the other images of the assembly + foreach ($assembly->getAttachments() as $attachment) { + //Dont show the master attachment twice + if ($this->isAttachmentValidPicture($attachment) && $attachment !== $assembly->getMasterPictureAttachment()) { + $list[] = $attachment; + } + } + + return $list; + } + + /** + * Determines what attachment should be used for previewing a assembly (especially in assembly table). + * The returned attachment is guaranteed to be existing and be a picture. + * + * @param Assembly $assembly The assembly for which the attachment should be determined + */ + public function getTablePreviewAttachment(Assembly $assembly): ?Attachment + { + $attachment = $assembly->getMasterPictureAttachment(); + if ($this->isAttachmentValidPicture($attachment)) { + return $attachment; + } + + return null; + } + + /** + * Checks if a attachment is exising and a valid picture. + * + * @param Attachment|null $attachment the attachment that should be checked + * + * @return bool true if the attachment is valid + */ + protected function isAttachmentValidPicture(?Attachment $attachment): bool + { + return $attachment instanceof Attachment + && $attachment->isPicture() + && $this->attachmentHelper->isFileExisting($attachment); + } +} diff --git a/src/Services/Attachments/AttachmentSubmitHandler.php b/src/Services/Attachments/AttachmentSubmitHandler.php index a30163ae1..81b998bec 100644 --- a/src/Services/Attachments/AttachmentSubmitHandler.php +++ b/src/Services/Attachments/AttachmentSubmitHandler.php @@ -22,6 +22,7 @@ namespace App\Services\Attachments; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentType; @@ -81,6 +82,7 @@ public function __construct( CategoryAttachment::class => 'category', CurrencyAttachment::class => 'currency', ProjectAttachment::class => 'project', + AssemblyAttachment::class => 'assembly', FootprintAttachment::class => 'footprint', GroupAttachment::class => 'group', ManufacturerAttachment::class => 'manufacturer', diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php index 142471457..db495cc7e 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -22,6 +22,8 @@ namespace App\Services; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; @@ -64,6 +66,8 @@ public function __construct(protected TranslatorInterface $translator, private r AttachmentType::class => $this->translator->trans('attachment_type.label'), Project::class => $this->translator->trans('project.label'), ProjectBOMEntry::class => $this->translator->trans('project_bom_entry.label'), + Assembly::class => $this->translator->trans('assembly.label'), + AssemblyBOMEntry::class => $this->translator->trans('assembly_bom_entry.label'), Footprint::class => $this->translator->trans('footprint.label'), Manufacturer::class => $this->translator->trans('manufacturer.label'), MeasurementUnit::class => $this->translator->trans('measurement_unit.label'), @@ -178,6 +182,8 @@ public function formatLabelHTMLForEntity(AbstractDBElement $entity, bool $includ $on = $entity->getOrderdetail()->getPart(); } elseif ($entity instanceof ProjectBOMEntry && $entity->getProject() instanceof Project) { $on = $entity->getProject(); + } elseif ($entity instanceof AssemblyBOMEntry && $entity->getAssembly() instanceof Assembly) { + $on = $entity->getAssembly(); } if (isset($on) && $on instanceof NamedElementInterface) { diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php index 78db06f07..8e1704b46 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -22,6 +22,7 @@ namespace App\Services; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\PartAttachment; @@ -98,6 +99,7 @@ public function timeTravelURL(AbstractDBElement $entity, \DateTimeInterface $dat AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', Project::class => 'project_edit', + Assembly::class => 'assembly_edit', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', StorageLocation::class => 'store_location_edit', @@ -204,6 +206,7 @@ public function infoURL(AbstractDBElement $entity): string AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', Project::class => 'project_info', + Assembly::class => 'assembly_info', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', StorageLocation::class => 'store_location_edit', @@ -234,6 +237,7 @@ public function editURL(AbstractDBElement $entity): string AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', Project::class => 'project_edit', + Assembly::class => 'assembly_edit', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', StorageLocation::class => 'store_location_edit', @@ -265,6 +269,7 @@ public function createURL(AbstractDBElement|string $entity): string AttachmentType::class => 'attachment_type_new', Category::class => 'category_new', Project::class => 'project_new', + Assembly::class => 'assembly_new', Supplier::class => 'supplier_new', Manufacturer::class => 'manufacturer_new', StorageLocation::class => 'store_location_new', @@ -296,6 +301,7 @@ public function cloneURL(AbstractDBElement $entity): string AttachmentType::class => 'attachment_type_clone', Category::class => 'category_clone', Project::class => 'device_clone', + Assembly::class => 'assembly_clone', Supplier::class => 'supplier_clone', Manufacturer::class => 'manufacturer_clone', StorageLocation::class => 'store_location_clone', @@ -323,6 +329,7 @@ public function listPartsURL(AbstractDBElement $entity): string { $map = [ Project::class => 'project_info', + Assembly::class => 'assembly_info', Category::class => 'part_list_category', Footprint::class => 'part_list_footprint', @@ -341,6 +348,7 @@ public function deleteURL(AbstractDBElement $entity): string AttachmentType::class => 'attachment_type_delete', Category::class => 'category_delete', Project::class => 'project_delete', + Assembly::class => 'assembly_delete', Supplier::class => 'supplier_delete', Manufacturer::class => 'manufacturer_delete', StorageLocation::class => 'store_location_delete', diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 862fa463f..47ba90c90 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -22,15 +22,25 @@ */ namespace App\Services\ImportExportSystem; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Parts\Category; +use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Repository\DBElementRepository; +use App\Repository\PartRepository; +use App\Repository\Parts\CategoryRepository; +use App\Repository\Parts\ManufacturerRepository; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use League\Csv\Reader; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\OptionsResolver\OptionsResolver; +use RuntimeException; +use UnexpectedValueException; /** * @see \App\Tests\Services\ImportExportSystem\BOMImporterTest @@ -47,17 +57,29 @@ class BOMImporter 5 => 'Supplier and ref', ]; + private readonly PartRepository $partRepository; + + private readonly ManufacturerRepository $manufacturerRepository; + + private readonly CategoryRepository $categoryRepository; + + private readonly DBElementRepository $assemblyBOMEntryRepository; + public function __construct( private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, private readonly BOMValidationService $validationService ) { + $this->partRepository = $entityManager->getRepository(Part::class); + $this->manufacturerRepository = $entityManager->getRepository(Manufacturer::class); + $this->categoryRepository = $entityManager->getRepository(Category::class); + $this->assemblyBOMEntryRepository = $entityManager->getRepository(AssemblyBOMEntry::class); } protected function configureOptions(OptionsResolver $resolver): OptionsResolver { $resolver->setRequired('type'); - $resolver->setAllowedValues('type', ['kicad_pcbnew', 'kicad_schematic']); + $resolver->setAllowedValues('type', ['kicad_pcbnew', 'kicad_schematic', 'json']); // For flexible schematic import with field mapping $resolver->setDefined(['field_mapping', 'field_priorities', 'delimiter']); @@ -87,13 +109,30 @@ public function importFileIntoProject(File $file, Project $project, array $optio return $bom_entries; } + /** + * Converts the given file into an array of BOM entries using the given options and save them into the given assembly. + * The changes are not saved into the database yet. + * @return AssemblyBOMEntry[] + */ + public function importFileIntoAssembly(File $file, Assembly $assembly, array $options): array + { + $bomEntries = $this->fileToBOMEntries($file, $options, AssemblyBOMEntry::class); + + //Assign the bom_entries to the assembly + foreach ($bomEntries as $bom_entry) { + $assembly->addBomEntry($bom_entry); + } + + return $bomEntries; + } + /** * Converts the given file into an array of BOM entries using the given options. - * @return ProjectBOMEntry[] + * @return ProjectBOMEntry[]|AssemblyBOMEntry[] */ - public function fileToBOMEntries(File $file, array $options): array + public function fileToBOMEntries(File $file, array $options, string $objectType = ProjectBOMEntry::class): array { - return $this->stringToBOMEntries($file->getContent(), $options); + return $this->stringToBOMEntries($file->getContent(), $options, $objectType); } /** @@ -117,22 +156,22 @@ public function validateBOMData(string $data, array $options): array * Import string data into an array of BOM entries, which are not yet assigned to a project. * @param string $data The data to import * @param array $options An array of options - * @return ProjectBOMEntry[] An array of imported entries + * @return ProjectBOMEntry[]|AssemblyBOMEntry[] An array of imported entries */ - public function stringToBOMEntries(string $data, array $options): array + public function stringToBOMEntries(string $data, array $options, string $objectType = ProjectBOMEntry::class): array { $resolver = new OptionsResolver(); $resolver = $this->configureOptions($resolver); $options = $resolver->resolve($options); return match ($options['type']) { - 'kicad_pcbnew' => $this->parseKiCADPCB($data), - 'kicad_schematic' => $this->parseKiCADSchematic($data, $options), + 'kicad_pcbnew' => $this->parseKiCADPCB($data, $objectType), + 'json' => $this->parseJson($data, $options, $objectType), default => throw new InvalidArgumentException('Invalid import type!'), }; } - private function parseKiCADPCB(string $data): array + private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntry::class): array { $csv = Reader::createFromString($data); $csv->setDelimiter(';'); @@ -158,8 +197,13 @@ private function parseKiCADPCB(string $data): array throw new \UnexpectedValueException('Quantity missing at line ' . ($offset + 1) . '!'); } - $bom_entry = new ProjectBOMEntry(); - $bom_entry->setName($entry['Designation'] . ' (' . $entry['Package'] . ')'); + $bom_entry = $objectType === ProjectBOMEntry::class ? new ProjectBOMEntry() : new AssemblyBOMEntry(); + if ($objectType === ProjectBOMEntry::class) { + $bom_entry->setName($entry['Designation'] . ' (' . $entry['Package'] . ')'); + } else { + $bom_entry->setName($entry['Designation']); + } + $bom_entry->setMountnames($entry['Designator'] ?? ''); $bom_entry->setComment($entry['Supplier and ref'] ?? ''); $bom_entry->setQuantity((float) ($entry['Quantity'] ?? 1)); @@ -227,6 +271,174 @@ private function validateKiCADSchematicData(string $data, array $options): array return $this->validationService->validateBOMEntries($mapped_entries, $options); } + private function parseJson(string $data, array $options = [], string $objectType = ProjectBOMEntry::class): array + { + $result = []; + + $data = json_decode($data, true); + + foreach ($data as $entry) { + // Check quantity + if (!isset($entry['quantity'])) { + throw new UnexpectedValueException('quantity missing'); + } + if (!is_float($entry['quantity']) || $entry['quantity'] <= 0) { + throw new UnexpectedValueException('quantity expected as float greater than 0.0'); + } + + // Check name + if (isset($entry['name']) && !is_string($entry['name'])) { + throw new UnexpectedValueException('name of part list entry expected as string'); + } + + // Check if part is assigned with relevant information + if (isset($entry['part'])) { + if (!is_array($entry['part'])) { + throw new UnexpectedValueException('The property "part" should be an array'); + } + + $partIdValid = isset($entry['part']['id']) && is_int($entry['part']['id']) && $entry['part']['id'] > 0; + $partNameValid = isset($entry['part']['name']) && is_string($entry['part']['name']) && trim($entry['part']['name']) !== ''; + $partMpnrValid = isset($entry['part']['mpnr']) && is_string($entry['part']['mpnr']) && trim($entry['part']['mpnr']) !== ''; + $partIpnValid = isset($entry['part']['ipn']) && is_string($entry['part']['ipn']) && trim($entry['part']['ipn']) !== ''; + + if (!$partIdValid && !$partNameValid && !$partMpnrValid && !$partIpnValid) { + throw new UnexpectedValueException( + 'The property "part" must have either assigned: "id" as integer greater than 0, "name", "mpnr", or "ipn" as non-empty string' + ); + } + + $part = $partIdValid ? $this->partRepository->findOneBy(['id' => $entry['part']['id']]) : null; + $part = $part ?? ($partMpnrValid ? $this->partRepository->findOneBy(['manufacturer_product_number' => trim($entry['part']['mpnr'])]) : null); + $part = $part ?? ($partIpnValid ? $this->partRepository->findOneBy(['ipn' => trim($entry['part']['ipn'])]) : null); + $part = $part ?? ($partNameValid ? $this->partRepository->findOneBy(['name' => trim($entry['part']['name'])]) : null); + + if ($part === null) { + $part = new Part(); + $part->setName($entry['part']['name']); + } + + if ($partNameValid && $part->getName() !== trim($entry['part']['name'])) { + throw new RuntimeException(sprintf('Part name does not match exact the given name. Given for import: %s, found part: %s', $entry['part']['name'], $part->getName())); + } + + if ($partIpnValid && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) { + throw new RuntimeException(sprintf('Part mpnr does not match exact the given mpnr. Given for import: %s, found part: %s', $entry['part']['mpnr'], $part->getManufacturerProductNumber())); + } + + if ($partIpnValid && $part->getIpn() !== trim($entry['part']['ipn'])) { + throw new RuntimeException(sprintf('Part ipn does not match exact the given ipn. Given for import: %s, found part: %s', $entry['part']['ipn'], $part->getIpn())); + } + + // Part: Description check + if (isset($entry['part']['description']) && !is_null($entry['part']['description'])) { + if (!is_string($entry['part']['description']) || trim($entry['part']['description']) === '') { + throw new UnexpectedValueException('The property path "part.description" must be a non-empty string if not null'); + } + } + $partDescription = $entry['part']['description'] ?? ''; + + // Part: Manufacturer check + $manufacturerIdValid = false; + $manufacturerNameValid = false; + if (array_key_exists('manufacturer', $entry['part'])) { + if (!is_array($entry['part']['manufacturer'])) { + throw new UnexpectedValueException('The property path "part.manufacturer" must be an array'); + } + + $manufacturerIdValid = isset($entry['part']['manufacturer']['id']) && is_int($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] > 0; + $manufacturerNameValid = isset($entry['part']['manufacturer']['name']) && is_string($entry['part']['manufacturer']['name']) && trim($entry['part']['manufacturer']['name']) !== ''; + + // Stellen sicher, dass mindestens eine Bedingung für manufacturer erfüllt sein muss + if (!$manufacturerIdValid && !$manufacturerNameValid) { + throw new UnexpectedValueException( + 'The property "manufacturer" must have either assigned: "id" as integer greater than 0, or "name" as non-empty string' + ); + } + } + + $manufacturer = $manufacturerIdValid ? $this->manufacturerRepository->findOneBy(['id' => $entry['part']['manufacturer']['id']]) : null; + $manufacturer = $manufacturer ?? ($manufacturerNameValid ? $this->manufacturerRepository->findOneBy(['name' => trim($entry['part']['manufacturer']['name'])]) : null); + + if ($manufacturer === null) { + throw new RuntimeException( + 'Manufacturer not found' + ); + } + + if ($manufacturerNameValid && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) { + throw new RuntimeException(sprintf('Manufacturer name does not match exact the given name. Given for import: %s, found manufacturer: %s', $entry['manufacturer']['name'], $manufacturer->getName())); + } + + // Part: Category check + $categoryIdValid = false; + $categoryNameValid = false; + if (array_key_exists('category', $entry['part'])) { + if (!is_array($entry['part']['category'])) { + throw new UnexpectedValueException('part.category must be an array'); + } + + $categoryIdValid = isset($entry['part']['category']['id']) && is_int($entry['part']['category']['id']) && $entry['part']['category']['id'] > 0; + $categoryNameValid = isset($entry['part']['category']['name']) && is_string($entry['part']['category']['name']) && trim($entry['part']['category']['name']) !== ''; + + if (!$categoryIdValid && !$categoryNameValid) { + throw new UnexpectedValueException( + 'The property "category" must have either assigned: "id" as integer greater than 0, or "name" as non-empty string' + ); + } + } + + $category = $categoryIdValid ? $this->categoryRepository->findOneBy(['id' => $entry['part']['category']['id']]) : null; + $category = $category ?? ($categoryNameValid ? $this->categoryRepository->findOneBy(['name' => trim($entry['part']['category']['name'])]) : null); + + if ($category === null) { + throw new RuntimeException( + 'Category not found' + ); + } + + if ($categoryNameValid && $category->getName() !== trim($entry['part']['category']['name'])) { + throw new RuntimeException(sprintf('Category name does not match exact the given name. Given for import: %s, found category: %s', $entry['category']['name'], $category->getName())); + } + + $part->setDescription($partDescription); + $part->setManufacturer($manufacturer); + $part->setCategory($category); + + if ($partMpnrValid) { + $part->setManufacturerProductNumber($entry['part']['mpnr'] ?? ''); + } + if ($partIpnValid) { + $part->setIpn($entry['part']['ipn'] ?? ''); + } + + if ($objectType === AssemblyBOMEntry::class) { + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['part' => $part]); + + if ($bomEntry === null) { + $name = isset($entry['name']) && $entry['name'] !== null ? trim($entry['name']) : ''; + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $name]); + + if ($bomEntry === null) { + $bomEntry = new AssemblyBOMEntry(); + } + } + } else { + $bomEntry = new ProjectBOMEntry(); + } + + $bomEntry->setQuantity($entry['quantity']); + $bomEntry->setName($entry['name'] ?? ''); + + $bomEntry->setPart($part); + } + + $result[] = $bomEntry; + } + + return $result; + } + /** * This function uses the order of the fields in the CSV files to make them locale independent. * @param array $entry @@ -243,7 +455,7 @@ private function normalizeColumnNames(array $entry): array } //@phpstan-ignore-next-line We want to keep this check just to be safe when something changes - $new_index = self::MAP_KICAD_PCB_FIELDS[$index] ?? throw new \UnexpectedValueException('Invalid field index!'); + $new_index = self::MAP_KICAD_PCB_FIELDS[$index] ?? throw new UnexpectedValueException('Invalid field index!'); $out[$new_index] = $field; } diff --git a/src/Services/ProjectSystem/ProjectBuildHelper.php b/src/Services/ProjectSystem/ProjectBuildHelper.php index 269c7e4c2..d7ba9e6c9 100644 --- a/src/Services/ProjectSystem/ProjectBuildHelper.php +++ b/src/Services/ProjectSystem/ProjectBuildHelper.php @@ -22,10 +22,13 @@ */ namespace App\Services\ProjectSystem; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Helpers\Assemblies\AssemblyBuildRequest; use App\Helpers\Projects\ProjectBuildRequest; +use App\Services\AssemblySystem\AssemblyBuildHelper; use App\Services\Parts\PartLotWithdrawAddHelper; /** @@ -33,8 +36,10 @@ */ class ProjectBuildHelper { - public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper) - { + public function __construct( + private readonly PartLotWithdrawAddHelper $withdrawAddHelper, + private readonly AssemblyBuildHelper $assemblyBuildHelper + ) { } /** @@ -66,12 +71,16 @@ public function getMaximumBuildableCount(Project $project): int $maximum_buildable_count = PHP_INT_MAX; foreach ($project->getBomEntries() as $bom_entry) { //Skip BOM entries without a part (as we can not determine that) - if (!$bom_entry->isPartBomEntry()) { + if (!$bom_entry->isPartBomEntry() && $bom_entry->getAssembly() === null) { continue; } //The maximum buildable count for the whole project is the minimum of all BOM entries - $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); + if ($bom_entry->getPart() !== null) { + $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); + } elseif ($bom_entry->getAssembly() !== null) { + $maximum_buildable_count = min($maximum_buildable_count, $this->assemblyBuildHelper->getMaximumBuildableCount($bom_entry->getAssembly())); + } } return $maximum_buildable_count; @@ -97,10 +106,10 @@ public function isBOMEntryBuildable(ProjectBOMEntry $bom_entry, int $number_of_b } /** - * Returns the project BOM entries for which parts are missing in the stock for the given number of builds + * Returns the project or assembly BOM entries for which parts are missing in the stock for the given number of builds * @param Project $project The project for which the BOM entries should be checked * @param int $number_of_builds How often should the project be build? - * @return ProjectBOMEntry[] + * @return ProjectBOMEntry[]|AssemblyBOMEntry[] */ public function getNonBuildableProjectBomEntries(Project $project, int $number_of_builds = 1): array { @@ -108,24 +117,29 @@ public function getNonBuildableProjectBomEntries(Project $project, int $number_o throw new \InvalidArgumentException('The number of builds must be greater than 0!'); } - $non_buildable_entries = []; + $nonBuildableEntries = []; foreach ($project->getBomEntries() as $bomEntry) { $part = $bomEntry->getPart(); //Skip BOM entries without a part (as we can not determine that) - if (!$part instanceof Part) { + if (!$part instanceof Part && $bomEntry->getAssembly() === null) { continue; } - $amount_sum = $part->getAmountSum(); + if ($bomEntry->getPart() !== null) { + $amount_sum = $part->getAmountSum(); - if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { - $non_buildable_entries[] = $bomEntry; + if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { + $nonBuildableEntries[] = $bomEntry; + } + } elseif ($bomEntry->getAssembly() !== null) { + $nonBuildableAssemblyEntries = $this->assemblyBuildHelper->getNonBuildableAssemblyBomEntries($bomEntry->getAssembly(), $number_of_builds); + $nonBuildableEntries = array_merge($nonBuildableEntries, $nonBuildableAssemblyEntries); } } - return $non_buildable_entries; + return $nonBuildableEntries; } /** @@ -133,22 +147,37 @@ public function getNonBuildableProjectBomEntries(Project $project, int $number_o * The ProjectBuildRequest has to be validated before!! * You have to flush changes to DB afterward */ - public function doBuild(ProjectBuildRequest $buildRequest): void + public function doBuild(ProjectBuildRequest $projectBuildRequest): void { - $message = $buildRequest->getComment(); - $message .= ' (Project build: '.$buildRequest->getProject()->getName().')'; + $message = $projectBuildRequest->getComment(); + $message .= ' (Project build: '.$projectBuildRequest->getProject()->getName().')'; - foreach ($buildRequest->getPartBomEntries() as $bom_entry) { - foreach ($buildRequest->getPartLotsForBOMEntry($bom_entry) as $part_lot) { - $amount = $buildRequest->getLotWithdrawAmount($part_lot); + foreach ($projectBuildRequest->getPartBomEntries() as $bomEntry) { + foreach ($projectBuildRequest->getPartLotsForBOMEntry($bomEntry) as $partLot) { + $amount = $projectBuildRequest->getLotWithdrawAmount($partLot); if ($amount > 0) { - $this->withdraw_add_helper->withdraw($part_lot, $amount, $message); + $this->withdrawAddHelper->withdraw($partLot, $amount, $message); + } + } + } + + foreach ($projectBuildRequest->getAssemblyBomEntries() as $bomEntry) { + $assemblyBuildRequest = new AssemblyBuildRequest($bomEntry->getAssembly(), $projectBuildRequest->getNumberOfBuilds()); + + //Add fields for assembly bom entries + foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) { + foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $partLot) { + //Read amount from build configuration of the projectBuildRequest + $amount = $projectBuildRequest->getLotWithdrawAmount($partLot); + if ($amount > 0) { + $this->withdrawAddHelper->withdraw($partLot, $amount, $message); + } } } } - if ($buildRequest->getAddBuildsToBuildsPart()) { - $this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message); + if ($projectBuildRequest->getAddBuildsToBuildsPart()) { + $this->withdrawAddHelper->add($projectBuildRequest->getBuildsPartLot(), $projectBuildRequest->getNumberOfBuilds(), $message); } } } diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index f7a9d1c40..5f08b8183 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -22,6 +22,7 @@ namespace App\Services\Trees; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\AttachmentType; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; @@ -170,6 +171,12 @@ protected function getEditNodes(): array $this->urlGenerator->generate('project_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-archive'); } + if ($this->security->isGranted('read', new Assembly())) { + $nodes[] = (new TreeViewNode( + $this->translator->trans('tree.tools.edit.assemblies'), + $this->urlGenerator->generate('assembly_new') + ))->setIcon('fa-fw fa-treeview fa-solid fa-list'); + } if ($this->security->isGranted('read', new Supplier())) { $nodes[] = (new TreeViewNode( $this->translator->trans('tree.tools.edit.suppliers'), diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index 73ffa5baf..fa9935c8f 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -22,6 +22,7 @@ namespace App\Services\Trees; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; @@ -154,6 +155,10 @@ private function getTreeViewUncached( $href_type = 'list_parts'; } + if ($mode === 'assemblies') { + $href_type = 'list_parts'; + } + $generic = $this->getGenericTree($class, $parent); $treeIterator = new TreeViewNodeIterator($generic); $recursiveIterator = new RecursiveIteratorIterator($treeIterator, RecursiveIteratorIterator::SELF_FIRST); @@ -219,6 +224,7 @@ protected function entityClassToRootNodeString(string $class): string Manufacturer::class => $this->translator->trans('manufacturer.labelp'), Supplier::class => $this->translator->trans('supplier.labelp'), Project::class => $this->translator->trans('project.labelp'), + Assembly::class => $this->translator->trans('assembly.labelp'), default => $this->translator->trans('tree.root_node.text'), }; } @@ -233,6 +239,7 @@ protected function entityClassToRootNodeIcon(string $class): ?string Manufacturer::class => $icon.'fa-industry', Supplier::class => $icon.'fa-truck', Project::class => $icon.'fa-archive', + Assembly::class => $icon.'fa-list', default => null, }; } diff --git a/src/Twig/EntityExtension.php b/src/Twig/EntityExtension.php index 762ebb094..086b21c5a 100644 --- a/src/Twig/EntityExtension.php +++ b/src/Twig/EntityExtension.php @@ -22,6 +22,7 @@ */ namespace App\Twig; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Base\AbstractDBElement; use App\Entity\ProjectSystem\Project; @@ -108,6 +109,7 @@ public function getEntityType(object $entity): ?string Manufacturer::class => 'manufacturer', Category::class => 'category', Project::class => 'device', + Assembly::class => 'assembly', Attachment::class => 'attachment', Supplier::class => 'supplier', User::class => 'user', diff --git a/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequest.php b/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequest.php new file mode 100644 index 000000000..dd3bc19ee --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequest.php @@ -0,0 +1,37 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use Symfony\Component\Validator\Constraint; + +/** + * This constraint checks that the given ValidAssemblyBuildRequest is valid. + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class ValidAssemblyBuildRequest extends Constraint +{ + public function getTargets(): string + { + return self::CLASS_CONSTRAINT; + } +} diff --git a/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequestValidator.php b/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequestValidator.php new file mode 100644 index 000000000..9d8c2e56a --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequestValidator.php @@ -0,0 +1,84 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use App\Entity\Parts\PartLot; +use App\Helpers\Assemblies\AssemblyBuildRequest; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; + +class ValidAssemblyBuildRequestValidator extends ConstraintValidator +{ + private function buildViolationForLot(PartLot $partLot, string $message): ConstraintViolationBuilderInterface + { + return $this->context->buildViolation($message) + ->atPath('lot_' . $partLot->getID()) + ->setParameter('{{ lot }}', $partLot->getName()); + } + + public function validate($value, Constraint $constraint): void + { + if (!$constraint instanceof ValidAssemblyBuildRequest) { + throw new UnexpectedTypeException($constraint, ValidAssemblyBuildRequest::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!$value instanceof AssemblyBuildRequest) { + throw new UnexpectedTypeException($value, AssemblyBuildRequest::class); + } + + foreach ($value->getPartBomEntries() as $bom_entry) { + $withdraw_sum = $value->getWithdrawAmountSum($bom_entry); + $needed_amount = $value->getNeededAmountForBOMEntry($bom_entry); + + foreach ($value->getPartLotsForBOMEntry($bom_entry) as $lot) { + $withdraw_amount = $value->getLotWithdrawAmount($lot); + + if ($withdraw_amount < 0) { + $this->buildViolationForLot($lot, 'validator.assembly_build.lot_must_not_smaller_0') + ->addViolation(); + } + + if ($withdraw_amount > $lot->getAmount()) { + $this->buildViolationForLot($lot, 'validator.assembly_build.lot_must_not_bigger_than_stock') + ->addViolation(); + } + + if ($withdraw_sum > $needed_amount && $value->isDontCheckQuantity() === false) { + $this->buildViolationForLot($lot, 'validator.assembly_build.lot_bigger_than_needed') + ->addViolation(); + } + + if ($withdraw_sum < $needed_amount && $value->isDontCheckQuantity() === false) { + $this->buildViolationForLot($lot, 'validator.assembly_build.lot_smaller_than_needed') + ->addViolation(); + } + } + } + } +} diff --git a/templates/admin/assembly_admin.html.twig b/templates/admin/assembly_admin.html.twig new file mode 100644 index 000000000..d8b3ab257 --- /dev/null +++ b/templates/admin/assembly_admin.html.twig @@ -0,0 +1,62 @@ +{% extends "admin/base_admin.html.twig" %} + +{# @var entity App\Entity\AssemblySystem\Assembly #} + +{% block card_title %} + {% trans %}assembly.caption{% endtrans %} +{% endblock %} + +{% block edit_title %} + {% trans %}assembly.edit{% endtrans %}: {{ entity.name }} +{% endblock %} + +{% block new_title %} + {% trans %}assembly.new{% endtrans %} +{% endblock %} + +{% block additional_pills %} + +{% endblock %} + +{% block quick_links %} +
    +
    + +
    +
    +{% endblock %} + +{% block additional_controls %} + {{ form_row(form.description) }} + {{ form_row(form.status) }} + {% if entity.id %} +
    + +
    + {% if entity.buildPart %} + {{ entity.buildPart.name }} + {% else %} + {% trans %}assembly.edit.associated_build_part.add{% endtrans %} + {% endif %} +

    {% trans %}assembly.edit.associated_build.hint{% endtrans %}

    +
    +
    + {% endif %} + +{% endblock %} + +{% block additional_panes %} +
    + {% form_theme form.bom_entries with ['form/collection_types_layout_assembly.html.twig'] %} + {{ form_errors(form.bom_entries) }} + {{ form_widget(form.bom_entries) }} + {% if entity.id %} + + + {% trans %}assembly.edit.bom.import_bom{% endtrans %} + + {% endif %} +
    +{% endblock %} \ No newline at end of file diff --git a/templates/admin/project_admin.html.twig b/templates/admin/project_admin.html.twig index 1a9950691..dcf8c64cf 100644 --- a/templates/admin/project_admin.html.twig +++ b/templates/admin/project_admin.html.twig @@ -36,7 +36,7 @@ {% if entity.buildPart %} {{ entity.buildPart.name }} {% else %} - {% trans %}project.edit.associated_build_part.add{% endtrans %} {% endif %}

    {% trans %}project.edit.associated_build.hint{% endtrans %}

    diff --git a/templates/assemblies/add_parts.html.twig b/templates/assemblies/add_parts.html.twig new file mode 100644 index 000000000..d8d8e657f --- /dev/null +++ b/templates/assemblies/add_parts.html.twig @@ -0,0 +1,22 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{% trans %}assembly.add_parts_to_assembly{% endtrans %}{% endblock %} + +{% block card_title %} + + {% trans %}assembly.add_parts_to_assembly{% endtrans %}{% if assembly %}: {{ assembly.name }}{% endif %} +{% endblock %} + +{% block card_content %} + + {{ form_start(form) }} + + {{ form_row(form.assembly) }} + {% form_theme form.bom_entries with ['form/collection_types_layout_assembly.html.twig'] %} + {{ form_widget(form.bom_entries) }} + + {{ form_row(form.submit) }} + + {{ form_end(form) }} + +{% endblock %} \ No newline at end of file diff --git a/templates/assemblies/build/_form.html.twig b/templates/assemblies/build/_form.html.twig new file mode 100644 index 000000000..0123ab010 --- /dev/null +++ b/templates/assemblies/build/_form.html.twig @@ -0,0 +1,88 @@ +{% import "helper.twig" as helper %} + +{{ form_start(form) }} + + + + + + + + + + + + {% for bom_entry in build_request.bomEntries %} + {# 1st row basic infos about the BOM entry #} + + + + + + + + + + {% endfor %} + +
    +
    + +
    +
    {% trans %}part.table.name{% endtrans %}{% trans %}assembly.bom.mountnames{% endtrans %}{% trans %}assembly.build.required_qty{% endtrans %}
    +
    + + {#
    +
    + {% if bom_entry.part %} + {{ bom_entry.part.name }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} + {% else %} + {{ bom_entry.name }} + {% endif %} + + {% for tag in bom_entry.mountnames|split(',') %} + {{ tag | trim }} + {% endfor %} + + {{ build_request.neededAmountForBOMEntry(bom_entry) | format_amount(bom_entry.part.partUnit ?? null) }} {% trans %}assembly.builds.needed{% endtrans %} + (= {{ number_of_builds }} x {{ bom_entry.quantity | format_amount(bom_entry.part.partUnit ?? null) }}) +
    + {% set lots = build_request.partLotsForBOMEntry(bom_entry) %} + {% if lots is not null %} + {% for lot in lots %} + {# @var lot \App\Entity\Parts\PartLot #} +
    + +
    + {{ form_errors(form["lot_"~lot.id]) }} + {{ form_widget(form["lot_"~lot.id]) }} +
    +
    + / {{ lot.amount | format_amount(lot.part.partUnit) }} {% trans %}assembly.builds.stocked{% endtrans %} +
    +
    + {% endfor %} + {% endif %} +
    + +{{ form_row(form.comment) }} +
    +{{ form_row(form.dontCheckQuantity) }} +
    + +{{ form_row(form.addBuildsToBuildsPart) }} +{% if form.buildsPartLot is defined %} + {{ form_row(form.buildsPartLot) }} +{% endif %} + +{{ form_row(form.submit) }} + +{{ form_end(form) }} \ No newline at end of file diff --git a/templates/assemblies/build/build.html.twig b/templates/assemblies/build/build.html.twig new file mode 100644 index 000000000..8f01607cb --- /dev/null +++ b/templates/assemblies/build/build.html.twig @@ -0,0 +1,40 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{% trans %}assembly.info.builds.label{% endtrans %}: {{ number_of_builds }}x {{ assembly.name }}{% endblock %} + +{% block card_title %} + + {% trans %}assembly.info.builds.label{% endtrans %}: {{ number_of_builds }}x {{ assembly.name }} +{% endblock %} + +{% block card_content %} + {% set can_build = buildHelper.assemblyBuildable(assembly, number_of_builds) %} + {% import "components/assemblies.macro.html.twig" as assembly_macros %} + + {% if assembly.status is not empty and assembly.status != "in_production" %} + + {% endif %} + + + +

    {% trans %}assembly.build.help{% endtrans %}

    + + {% include 'assemblies/build/_form.html.twig' %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/assemblies/import_bom.html.twig b/templates/assemblies/import_bom.html.twig new file mode 100644 index 000000000..53168b438 --- /dev/null +++ b/templates/assemblies/import_bom.html.twig @@ -0,0 +1,60 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{% trans %}assembly.import_bom{% endtrans %}{% endblock %} + +{% block before_card %} + {% if errors %} +
    +

    {% trans %}parts.import.errors.title{% endtrans %}

    +
      + {% for violation in errors %} +
    • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators') }} +
    • + {% endfor %} +
    +
    + {% endif %} +{% endblock %} + + +{% block card_title %} + + {% trans %}assembly.import_bom{% endtrans %}{% if assembly %}: {{ assembly.name }}{% endif %} +{% endblock %} + +{% block card_content %} + {{ form(form) }} +{% endblock %} + +{% block additional_content %} +
    +
    +
    +
    + {% trans %}assembly.import_bom.template.header.json{% endtrans %} +
    +
    +
    {{ jsonTemplate|json_encode(constant('JSON_PRETTY_PRINT') b-or constant('JSON_UNESCAPED_UNICODE')) }}
    + + {{ 'assembly.bom_import.template.json.table'|trans|raw }} +
    +
    +
    +
    +
    +
    + {% trans %}assembly.import_bom.template.header.kicad_pcbnew{% endtrans %} +
    +
    + {{ 'assembly.bom_import.template.kicad_pcbnew.exptected_columns'|trans }} +
    Id;Designator;Package;Quantity;Designation;Supplier and ref
    + {{ 'assembly.bom_import.template.kicad_pcbnew.exptected_columns.note'|trans|raw }} + + {{ 'assembly.bom_import.template.json.table'|trans|raw }} +
    +
    +
    +
    +{% endblock %} diff --git a/templates/assemblies/info/_bom.html.twig b/templates/assemblies/info/_bom.html.twig new file mode 100644 index 000000000..6a2ca3e03 --- /dev/null +++ b/templates/assemblies/info/_bom.html.twig @@ -0,0 +1,22 @@ +{% import "components/datatables.macro.html.twig" as datatables %} + + + +{{ datatables.datatable(datatable, 'elements/datatables/datatables', 'assemblies') }} \ No newline at end of file diff --git a/templates/assemblies/info/_builds.html.twig b/templates/assemblies/info/_builds.html.twig new file mode 100644 index 000000000..780c8c609 --- /dev/null +++ b/templates/assemblies/info/_builds.html.twig @@ -0,0 +1,40 @@ +{% set can_build = buildHelper.assemblyBuildable(assembly) %} + +{% import "components/assemblies.macro.html.twig" as assembly_macros %} + +{% if assembly.status is not empty and assembly.status != "in_production" %} + +{% endif %} + + + + +
    +
    +
    + + + +
    +
    +
    + + +{% if assembly.buildPart %} +

    {% trans %}assembly.builds.no_stocked_builds{% endtrans %}: {{ assembly.buildPart.amountSum }}

    +{% endif %} \ No newline at end of file diff --git a/templates/assemblies/info/_info.html.twig b/templates/assemblies/info/_info.html.twig new file mode 100644 index 000000000..495072b8a --- /dev/null +++ b/templates/assemblies/info/_info.html.twig @@ -0,0 +1,77 @@ +{% import "helper.twig" as helper %} + +
    +
    +
    +
    + {% if assembly.masterPictureAttachment %} + + + + {% else %} + Part main image + {% endif %} +
    +
    +

    {{ assembly.name }} + {# You need edit permission to use the edit button #} + {% if is_granted('edit', assembly) %} + + {% endif %} +

    +
    {{ assembly.description|format_markdown(true) }}
    + {% if assembly.buildPart %} +
    {% trans %}assembly.edit.associated_build_part{% endtrans %}:
    + {{ assembly.buildPart.name }} + {% endif %} + +
    +
    +
    + + +
    {# Sidebar panel with infos about last creation date, etc. #} +
    + + {{ helper.date_user_combination(assembly, true) }} + +
    + + {{ helper.date_user_combination(assembly, false) }} + +
    + +
    +
    + {{ helper.assemblies_status_to_badge(assembly.status) }} +
    +
    +
    +
    + + + {{ assembly.bomEntries | length }} + {% trans %}assembly.info.bom_entries_count{% endtrans %} + +
    +
    + {% if assembly.children is not empty %} +
    +
    + + + {{ assembly.children | length }} + {% trans %}assembly.info.sub_assemblies_count{% endtrans %} + +
    +
    + {% endif %} +
    + + {% if assembly.comment is not empty %} +

    +

    {% trans %}comment.label{% endtrans %}:
    + {{ assembly.comment|format_markdown }} +

    + {% endif %} +
    \ No newline at end of file diff --git a/templates/assemblies/info/_info_card.html.twig b/templates/assemblies/info/_info_card.html.twig new file mode 100644 index 000000000..508b2b069 --- /dev/null +++ b/templates/assemblies/info/_info_card.html.twig @@ -0,0 +1,133 @@ +{% import "helper.twig" as helper %} +{% import "label_system/dropdown_macro.html.twig" as dropdown %} + +{{ helper.breadcrumb_entity_link(assembly) }} + +
    +
    +
    + +
    +
    +
    + {% if assembly.description is not empty %} + {{ assembly.description|format_markdown }} + {% endif %} +
    + +
    +
    +
    +
    +
    +
    + + {{ assembly.name }} +
    +
    + + + {% if assembly.parent %} + {{ assembly.parent.fullPath }} + {% else %} + - + {% endif %} + +
    +
    +
    + {% block quick_links %}{% endblock %} + + + {% trans %}entity.edit.btn{% endtrans %} + +
    + + {{ assembly.lastModified | format_datetime("short") }} + +
    + + {{ assembly.addedDate | format_datetime("short") }} + +
    +
    +
    +
    +
    +
    +
    + + {{ assembly.children | length }} +
    +
    + + {{ assembly.bomEntries | length }} +
    +
    +
    + + {% if assembly.attachments is not empty %} +
    + {% include "parts/info/_attachments_info.html.twig" with {"part": assembly} %} +
    + {% endif %} + + {% if assembly.parameters is not empty %} +
    + {% for name, parameters in assembly.groupedParameters %} + {% if name is not empty %}
    {{ name }}
    {% endif %} + {{ helper.parameters_table(assembly) }} + {% endfor %} +
    + {% endif %} + + {% if assembly.comment is not empty %} +
    +
    + {{ assembly.comment|format_markdown }} +
    +
    + {% endif %} +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/assemblies/info/_subassemblies.html.twig b/templates/assemblies/info/_subassemblies.html.twig new file mode 100644 index 000000000..8c92c5e91 --- /dev/null +++ b/templates/assemblies/info/_subassemblies.html.twig @@ -0,0 +1,28 @@ + + + + + + + + + + + {% for subassembly in assembly.children %} + + + + + + + {% endfor %} + +
    {% trans %}name.label{% endtrans %}{% trans %}description.label{% endtrans %}# {% trans %}assembly.info.bom_entries_count{% endtrans %}# {% trans %}assembly.info.sub_assemblies_count{% endtrans %}
    {# Name #} + {{ subassembly.name }} + {# Description #} + {{ subassembly.description | format_markdown }} + + {{ subassembly.bomEntries | length }} + + {{ subassembly.children | length }} +
    \ No newline at end of file diff --git a/templates/assemblies/info/info.html.twig b/templates/assemblies/info/info.html.twig new file mode 100644 index 000000000..f5dac1e63 --- /dev/null +++ b/templates/assemblies/info/info.html.twig @@ -0,0 +1,105 @@ +{% extends "main_card.html.twig" %} +{% import "helper.twig" as helper %} + +{% block title %} + {% trans %}assembly.info.title{% endtrans %}: {{ assembly.name }} +{% endblock %} + +{% block content %} + + {{ helper.breadcrumb_entity_link(assembly) }} + {{ parent() }} +{% endblock %} + +{% block card_title %} + {% if assembly.masterPictureAttachment is not null and attachment_manager.isFileExisting(assembly.masterPictureAttachment) %} + + {% else %} + {{ helper.entity_icon(assembly, "me-1") }} + {% endif %} + {% trans %}assembly.info.title{% endtrans %}: {{ assembly.name }} +{% endblock %} + +{% block card_content %} + + +
    +
    + {% include "assemblies/info/_info.html.twig" %} +
    + {% if assembly.children is not empty %} +
    + {% include "assemblies/info/_subassemblies.html.twig" %} +
    + {% endif %} +
    + {% include "assemblies/info/_bom.html.twig" %} +
    +
    + {% include "assemblies/info/_builds.html.twig" %} +
    +
    + {% include "parts/info/_attachments_info.html.twig" with {"part": assembly} %} +
    +
    + {% for name, parameters in assembly.groupedParameters %} + {% if name is not empty %}
    {{ name }}
    {% endif %} + {{ helper.parameters_table(assembly.parameters) }} + {% endfor %} +
    +
    + +{% endblock %} \ No newline at end of file diff --git a/templates/components/assemblies.macro.html.twig b/templates/components/assemblies.macro.html.twig new file mode 100644 index 000000000..d59005e05 --- /dev/null +++ b/templates/components/assemblies.macro.html.twig @@ -0,0 +1,8 @@ +{% macro assembly_bom_entry_with_missing_instock(assembly_bom_entry, number_of_builds = 1) %} + {# @var \App\Entity\AssemblySystem\AssemblyBOMEntry assembly_bom_entry #} + {{ assembly_bom_entry.part.name }} + {% if assembly_bom_entry.name %} ({{ assembly_bom_entry.name }}){% endif %}: + {{ assembly_bom_entry.part.amountSum | format_amount(assembly_bom_entry.part.partUnit) }} {% trans %}assembly.builds.stocked{% endtrans %} + / + {{ (assembly_bom_entry.quantity * number_of_builds) | format_amount(assembly_bom_entry.part.partUnit) }} {% trans %}assembly.builds.needed{% endtrans %} +{% endmacro %} \ No newline at end of file diff --git a/templates/components/tree_macros.html.twig b/templates/components/tree_macros.html.twig index 366d42fe8..2e55147a1 100644 --- a/templates/components/tree_macros.html.twig +++ b/templates/components/tree_macros.html.twig @@ -7,6 +7,7 @@ ['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')], ['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')], ['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')], + ['assembly', path('tree_assembly_root'), 'assembly.labelp', is_granted('@assemblies.read')], ['tools', path('tree_tools'), 'tools.label', true], ] %} diff --git a/templates/form/collection_types_layout.html.twig b/templates/form/collection_types_layout.html.twig index 96b71bf0f..def235000 100644 --- a/templates/form/collection_types_layout.html.twig +++ b/templates/form/collection_types_layout.html.twig @@ -6,7 +6,7 @@ {# expand button #} {% trans %}project.bom.quantity{% endtrans %} - {% trans %}project.bom.part{% endtrans %} + {% trans %}project.bom.partOrAssembly{% endtrans %} {% trans %}project.bom.name{% endtrans %} {# Remove button #} @@ -41,9 +41,11 @@ {{ form_widget(form.quantity) }} {{ form_errors(form.quantity) }} - - {{ form_widget(form.part) }} + + {{ form_row(form.part) }} {{ form_errors(form.part) }} + {{ form_widget(form.assembly) }} + {{ form_errors(form.assembly) }} {{ form_widget(form.name) }} diff --git a/templates/form/collection_types_layout_assembly.html.twig b/templates/form/collection_types_layout_assembly.html.twig new file mode 100644 index 000000000..c5acebda0 --- /dev/null +++ b/templates/form/collection_types_layout_assembly.html.twig @@ -0,0 +1,80 @@ +{% block assembly_bom_entry_collection_widget %} + {% import 'components/collection_type.macro.html.twig' as collection %} +
    + + + + {# expand button #} + + + + {# Remove button #} + + + + + {% for entry in form %} + {{ form_widget(entry) }} + {% endfor %} + +
    {% trans %}assembly.bom.quantity{% endtrans %}{% trans %}assembly.bom.part{% endtrans %}{% trans %}assembly.bom.name{% endtrans %}
    + + +
    + +{% endblock %} + +{% block assembly_bom_entry_widget %} + {% set target_id = 'expand_row-' ~ form.vars.name %} + + {% import 'components/collection_type.macro.html.twig' as collection %} + + + + + + {{ form_widget(form.quantity) }} + {{ form_errors(form.quantity) }} + + + {{ form_widget(form.part) }} + {{ form_errors(form.part) }} + + + {{ form_widget(form.name) }} + {{ form_errors(form.name) }} + + + + {{ form_errors(form) }} + + + + + +
    + {{ form_row(form.mountnames) }} +
    + +
    +
    + {{ form_widget(form.price) }} + {{ form_widget(form.priceCurrency) }} +
    + {{ form_errors(form.price) }} + {{ form_errors(form.priceCurrency) }} +
    +
    + {{ form_row(form.comment) }} +
    + + +{% endblock %} \ No newline at end of file diff --git a/templates/helper.twig b/templates/helper.twig index bd1d2aa7a..3ddb4f7fa 100644 --- a/templates/helper.twig +++ b/templates/helper.twig @@ -76,6 +76,21 @@ {% endif %} {% endmacro %} +{% macro assemblies_status_to_badge(status, class="badge") %} + {% if status is not empty %} + {% set color = " bg-secondary" %} + + {% if status == "in_production" %} + {% set color = " bg-success" %} + {% endif %} + + + + {{ ("assembly.status." ~ status) | trans }} + + {% endif %} +{% endmacro %} + {% macro structural_entity_link(entity, link_type = "list_parts") %} {# @var entity \App\Entity\Base\StructuralDBElement #} {% if entity %} @@ -101,6 +116,7 @@ "category": ["fa-solid fa-tags", "category.label"], "currency": ["fa-solid fa-coins", "currency.label"], "device": ["fa-solid fa-archive", "project.label"], + "assembly": ["fa-solid fa-list", "assembly.label"], "footprint": ["fa-solid fa-microchip", "footprint.label"], "group": ["fa-solid fa-users", "group.label"], "label_profile": ["fa-solid fa-qrcode", "label_profile.label"], diff --git a/templates/projects/build/_form.html.twig b/templates/projects/build/_form.html.twig index a8f772e97..340b86700 100644 --- a/templates/projects/build/_form.html.twig +++ b/templates/projects/build/_form.html.twig @@ -27,7 +27,9 @@ {% if bom_entry.part %} - {{ bom_entry.part.name }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} + {{ 'projects.build.form.part'|trans({'%name%': bom_entry.part.name}) }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} + {% elseif bom_entry.assembly %} + {{ 'projects.build.form.assembly'|trans({'%name%': bom_entry.assembly.name}) }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} {% else %} {{ bom_entry.name }} {% endif %} @@ -45,9 +47,29 @@ {% set lots = build_request.partLotsForBOMEntry(bom_entry) %} + {% set assemblyBomEntriesWithoutPart = build_request.assemblyBomEntriesWithoutPart(bom_entry) %} + {% set assemblyBomEntriesWithPartNoStock = build_request.assemblyBomEntriesWithPartNoStock(bom_entry) %} {% if lots is not null %} + {% set previousLabel = null %} + {% for lot in lots %} {# @var lot \App\Entity\Parts\PartLot #} + + {% set label = '' %} + {% if form["lot_"~lot.id].vars.label is defined and form["lot_"~lot.id].vars.label is not empty %} + {% set label = form["lot_"~lot.id].vars.label %} + {% endif %} + + {% if label != '' and (previousLabel is null or label != previousLabel) %} +
    + +
    + {% endif %} + + {% set previousLabel = label %} +
    -
    +
    / {{ lot.amount | format_amount(lot.part.partUnit) }} {% trans %}project.builds.stocked{% endtrans %}
    {% endfor %} {% endif %} + {% if assemblyBomEntriesWithoutPart is not null %} + {% for bomEntryWithoutPart in assemblyBomEntriesWithoutPart %} +
    + +
    +
    + / {% trans %}project.builds.no_stock{% endtrans %} +
    +
    + {% endfor %} + {% endif %} + {% if assemblyBomEntriesWithPartNoStock is not null %} + {% for bomEntryWithPartNoStock in assemblyBomEntriesWithPartNoStock %} +
    +
    + +
    +
    + / {% trans %}project.builds.no_stock{% endtrans %} +
    +
    +
    + {% endfor %} + {% endif %} {% endfor %} diff --git a/tests/Entity/Attachments/AttachmentTest.php b/tests/Entity/Attachments/AttachmentTest.php index 00a68d7d4..699648eb7 100644 --- a/tests/Entity/Attachments/AttachmentTest.php +++ b/tests/Entity/Attachments/AttachmentTest.php @@ -24,6 +24,8 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Depends; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentTypeAttachment; @@ -81,6 +83,7 @@ public static function subClassesDataProvider(): \Iterator yield [CategoryAttachment::class, Category::class]; yield [CurrencyAttachment::class, Currency::class]; yield [ProjectAttachment::class, Project::class]; + yield [AssemblyAttachment::class, Assembly::class]; yield [FootprintAttachment::class, Footprint::class]; yield [GroupAttachment::class, Group::class]; yield [ManufacturerAttachment::class, Manufacturer::class]; diff --git a/tests/Helpers/Assemblies/AssemblyBuildRequestTest.php b/tests/Helpers/Assemblies/AssemblyBuildRequestTest.php new file mode 100644 index 000000000..210e33018 --- /dev/null +++ b/tests/Helpers/Assemblies/AssemblyBuildRequestTest.php @@ -0,0 +1,177 @@ +. + */ +namespace App\Tests\Helpers\Assemblies; + +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Helpers\Assemblies\AssemblyBuildRequest; +use PHPUnit\Framework\TestCase; + +class AssemblyBuildRequestTest extends TestCase +{ + + /** @var MeasurementUnit $float_unit */ + private MeasurementUnit $float_unit; + + /** @var Assembly */ + private Assembly $assembly1; + /** @var AssemblyBOMEntry */ + private AssemblyBOMEntry $bom_entry1a; + /** @var AssemblyBOMEntry */ + private AssemblyBOMEntry $bom_entry1b; + /** @var AssemblyBOMEntry */ + private AssemblyBOMEntry $bom_entry1c; + + private PartLot $lot1a; + private PartLot $lot1b; + private PartLot $lot2; + + /** @var Part */ + private Part $part1; + /** @var Part */ + private Part $part2; + + + public function setUp(): void + { + $this->float_unit = new MeasurementUnit(); + $this->float_unit->setName('float'); + $this->float_unit->setUnit('f'); + $this->float_unit->setIsInteger(false); + $this->float_unit->setUseSIPrefix(true); + + //Setup some example parts and part lots + $this->part1 = new Part(); + $this->part1->setName('Part 1'); + $this->lot1a = new class extends PartLot { + public function getID(): ?int + { + return 1; + } + }; + $this->part1->addPartLot($this->lot1a); + $this->lot1a->setAmount(10); + $this->lot1a->setDescription('Lot 1a'); + + $this->lot1b = new class extends PartLot { + public function getID(): ?int + { + return 2; + } + }; + $this->part1->addPartLot($this->lot1b); + $this->lot1b->setAmount(20); + $this->lot1b->setDescription('Lot 1b'); + + $this->part2 = new Part(); + + $this->part2->setName('Part 2'); + $this->part2->setPartUnit($this->float_unit); + $this->lot2 = new PartLot(); + $this->part2->addPartLot($this->lot2); + $this->lot2->setAmount(2.5); + $this->lot2->setDescription('Lot 2'); + + $this->bom_entry1a = new AssemblyBOMEntry(); + $this->bom_entry1a->setPart($this->part1); + $this->bom_entry1a->setQuantity(2); + + $this->bom_entry1b = new AssemblyBOMEntry(); + $this->bom_entry1b->setPart($this->part2); + $this->bom_entry1b->setQuantity(1.5); + + $this->bom_entry1c = new AssemblyBOMEntry(); + $this->bom_entry1c->setName('Non-part BOM entry'); + $this->bom_entry1c->setQuantity(4); + + + $this->assembly1 = new Assembly(); + $this->assembly1->setName('Assembly 1'); + $this->assembly1->addBomEntry($this->bom_entry1a); + $this->assembly1->addBomEntry($this->bom_entry1b); + $this->assembly1->addBomEntry($this->bom_entry1c); + } + + public function testInitialization(): void + { + //The values should be already prefilled correctly + $request = new AssemblyBuildRequest($this->assembly1, 10); + //We need totally 20: Take 10 from the first (maximum 10) and 10 from the second (maximum 20) + $this->assertEqualsWithDelta(10.0, $request->getLotWithdrawAmount($this->lot1a), PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(10.0, $request->getLotWithdrawAmount($this->lot1b), PHP_FLOAT_EPSILON); + + //If the needed amount is higher than the maximum, we should get the maximum + $this->assertEqualsWithDelta(2.5, $request->getLotWithdrawAmount($this->lot2), PHP_FLOAT_EPSILON); + } + + public function testGetNumberOfBuilds(): void + { + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + $this->assertSame(5, $build_request->getNumberOfBuilds()); + } + + public function testGetAssembly(): void + { + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + $this->assertEquals($this->assembly1, $build_request->getAssembly()); + } + + public function testGetNeededAmountForBOMEntry(): void + { + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + $this->assertEqualsWithDelta(10.0, $build_request->getNeededAmountForBOMEntry($this->bom_entry1a), PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(7.5, $build_request->getNeededAmountForBOMEntry($this->bom_entry1b), PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(20.0, $build_request->getNeededAmountForBOMEntry($this->bom_entry1c), PHP_FLOAT_EPSILON); + } + + public function testGetSetLotWithdrawAmount(): void + { + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + + //We can set the amount for a lot either via the lot object or via the ID + $build_request->setLotWithdrawAmount($this->lot1a, 2); + $build_request->setLotWithdrawAmount($this->lot1b->getID(), 3); + + //And it should be possible to get the amount via the lot object or via the ID + $this->assertEqualsWithDelta(2.0, $build_request->getLotWithdrawAmount($this->lot1a->getID()), PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(3.0, $build_request->getLotWithdrawAmount($this->lot1b), PHP_FLOAT_EPSILON); + } + + public function testGetWithdrawAmountSum(): void + { + //The sum of all withdraw amounts for an BOM entry (over all lots of the associated part) should be correct + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + + $build_request->setLotWithdrawAmount($this->lot1a, 2); + $build_request->setLotWithdrawAmount($this->lot1b, 3); + + $this->assertEqualsWithDelta(5.0, $build_request->getWithdrawAmountSum($this->bom_entry1a), PHP_FLOAT_EPSILON); + $build_request->setLotWithdrawAmount($this->lot2, 1.5); + $this->assertEqualsWithDelta(1.5, $build_request->getWithdrawAmountSum($this->bom_entry1b), PHP_FLOAT_EPSILON); + } + + +} diff --git a/tests/Services/AssemblySystem/AssemblyBuildHelperTest.php b/tests/Services/AssemblySystem/AssemblyBuildHelperTest.php new file mode 100644 index 000000000..c513ed8de --- /dev/null +++ b/tests/Services/AssemblySystem/AssemblyBuildHelperTest.php @@ -0,0 +1,117 @@ +. + */ +namespace App\Tests\Services\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Services\AssemblySystem\AssemblyBuildHelper; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class AssemblyBuildHelperTest extends WebTestCase +{ + /** @var AssemblyBuildHelper */ + protected $service; + + protected function setUp(): void + { + self::bootKernel(); + $this->service = self::getContainer()->get(AssemblyBuildHelper::class); + } + + public function testGetMaximumBuildableCountForBOMEntryNonPartBomEntry(): void + { + $bom_entry = new AssemblyBOMEntry(); + $bom_entry->setPart(null); + $bom_entry->setQuantity(10); + $bom_entry->setName('Test'); + + $this->expectException(\InvalidArgumentException::class); + $this->service->getMaximumBuildableCountForBOMEntry($bom_entry); + } + + public function testGetMaximumBuildableCountForBOMEntry(): void + { + $assembly_bom_entry = new AssemblyBOMEntry(); + $assembly_bom_entry->setQuantity(10); + + $part = new Part(); + $lot1 = new PartLot(); + $lot1->setAmount(120); + $lot2 = new PartLot(); + $lot2->setAmount(5); + $part->addPartLot($lot1); + $part->addPartLot($lot2); + + $assembly_bom_entry->setPart($part); + + //We have 125 parts in stock, so we can build 12 times the assembly (125 / 10 = 12.5) + $this->assertSame(12, $this->service->getMaximumBuildableCountForBOMEntry($assembly_bom_entry)); + + + $lot1->setAmount(0); + //We have 5 parts in stock, so we can build 0 times the assembly (5 / 10 = 0.5) + $this->assertSame(0, $this->service->getMaximumBuildableCountForBOMEntry($assembly_bom_entry)); + } + + public function testGetMaximumBuildableCount(): void + { + $assembly = new Assembly(); + + $assembly_bom_entry1 = new AssemblyBOMEntry(); + $assembly_bom_entry1->setQuantity(10); + $part = new Part(); + $lot1 = new PartLot(); + $lot1->setAmount(120); + $lot2 = new PartLot(); + $lot2->setAmount(5); + $part->addPartLot($lot1); + $part->addPartLot($lot2); + $assembly_bom_entry1->setPart($part); + $assembly->addBomEntry($assembly_bom_entry1); + + $assembly_bom_entry2 = new AssemblyBOMEntry(); + $assembly_bom_entry2->setQuantity(5); + $part2 = new Part(); + $lot3 = new PartLot(); + $lot3->setAmount(10); + $part2->addPartLot($lot3); + $assembly_bom_entry2->setPart($part2); + $assembly->addBomEntry($assembly_bom_entry2); + + $assembly->addBomEntry((new AssemblyBOMEntry())->setName('Non part entry')->setQuantity(1)); + + //Restricted by the few parts in stock of part2 + $this->assertSame(2, $this->service->getMaximumBuildableCount($assembly)); + + $lot3->setAmount(1000); + //Now the build count is restricted by the few parts in stock of part1 + $this->assertSame(12, $this->service->getMaximumBuildableCount($assembly)); + + $lot3->setAmount(0); + //Now the build count must be 0, as we have no parts in stock + $this->assertSame(0, $this->service->getMaximumBuildableCount($assembly)); + + } +} diff --git a/tests/Services/AssemblySystem/AssemblyBuildPartHelperTest.php b/tests/Services/AssemblySystem/AssemblyBuildPartHelperTest.php new file mode 100644 index 000000000..b8aa0ddc3 --- /dev/null +++ b/tests/Services/AssemblySystem/AssemblyBuildPartHelperTest.php @@ -0,0 +1,52 @@ +. + */ +namespace App\Tests\Services\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use App\Services\AssemblySystem\AssemblyBuildPartHelper; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class AssemblyBuildPartHelperTest extends WebTestCase +{ + /** @var AssemblyBuildPartHelper */ + protected $service; + + protected function setUp(): void + { + self::bootKernel(); + $this->service = self::getContainer()->get(AssemblyBuildPartHelper::class); + } + + public function testGetPartInitialization(): void + { + $assembly = new Assembly(); + $assembly->setName('Assembly 1'); + $assembly->setDescription('Description 1'); + + $part = $this->service->getPartInitialization($assembly); + $this->assertSame('Assembly 1', $part->getName()); + $this->assertSame('Description 1', $part->getDescription()); + $this->assertSame($assembly, $part->getBuiltAssembly()); + $this->assertSame($part, $assembly->getBuildPart()); + } +} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 1f234450e..8b1f2a620 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -4741,6 +4741,18 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Název
    + + + project.bom.assembly + Sestava + + + + + project.bom.partOrAssembly + Výběr + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9786,6 +9798,18 @@ Element 3 Díl + + + project.bom.assembly + Baugruppe + + + + + project.bom.partOrAssembly + Auswahl + + project.bom.add_entry @@ -9864,6 +9888,42 @@ Element 3 Archivováno + + + assembly.edit.status + Stav + + + + + assembly.status.draft + Návrh + + + + + assembly.status.planning + Plánování + + + + + assembly.status.in_production + Ve výrobě + + + + + assembly.status.finished + Dokončeno + + + + + assembly.status.archived + Archivováno + + part.new_build_part.error.build_part_already_exists @@ -10140,6 +10200,12 @@ Element 3 k dispozici + + + project.builds.no_stock + není uveden žádný sklad + + project.builds.needed @@ -10212,6 +10278,12 @@ Element 3 Cílový inventář + + + project.build.builds_part_lot_label + %name% (%quantity% požadováno) + + project.builds.number_of_builds @@ -13479,5 +13551,633 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Minimální šířka náhledu (px) + + + part.table.name.value.for_part + %value% (Součást) + + + + + part.table.name.value.for_assembly + %value% (Sestava) + + + + + assembly.label + Sestava + + + + + assembly.caption + Sestava + + + + + perm.assemblies + Sestavy + + + + + assembly_bom_entry.label + Součásti + + + + + assembly.labelp + Sestavy + + + + + assembly.edit + Upravit sestavu + + + + + assembly.new + Nová sestava + + + + + assembly.edit.associated_build_part + Přidružená součást + + + + + assembly.edit.associated_build_part.add + Přidat součást + + + + + assembly.edit.associated_build.hint + Tato součást představuje vyrobené instance sestavy. Zadejte, pokud jsou vyrobené instance potřeba. Pokud ne, počet součástí bude použit až při sestavení daného projektu. + + + + + assembly.edit.bom.import_bom + Importovat součásti + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Sestavy + + + + + assembly.bom_import.flash.success + %count% součástí úspěšně importováno do sestavy. + + + + + assembly.bom_import.flash.invalid_entries + Chyba ověření! Zkontrolujte svůj importovaný soubor! + + + + + assembly.bom_import.flash.invalid_file + Soubor nelze importovat. Zkontrolujte, zda jste vybrali správný typ souboru. Chybová zpráva: %message% + + + + + assembly.bom.quantity + Množství + + + + + assembly.bom.mountnames + Názvy osazení + + + + + assembly.bom.instockAmount + Stav na skladě + + + + + assembly.info.title + Info o sestavě + + + + + assembly.info.info.label + Informace + + + + + assembly.info.sub_assemblies.label + Podskupina + + + + + assembly.info.builds.label + Sestavení + + + + + assembly.info.bom_add_parts + Přidat součásti + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Měli byste zkontrolovat, zda opravdu chcete sestavu postavit s tímto stavem!]]> + + + + + assembly.builds.build_not_possible + Sestavení není možné: Nedostatek součástí + + + + + assembly.builds.following_bom_entries_miss_instock + Není dostatek součástí na skladě pro postavení tohoto projektu %number_of_builds% krát. Následující součásti nejsou skladem v dostatečném množství. + + + + + assembly.builds.build_possible + Sestavení je možné + + + + + assembly.builds.number_of_builds_possible + %max_builds% kusů této sestavy.]]> + + + + + assembly.builds.number_of_builds + Počet sestavení + + + + + assembly.build.btn_build + Sestavit + + + + + assembly.builds.no_stocked_builds + Počet skladovaných vyrobených instancí + + + + + assembly.info.bom_entries_count + Součásti + + + + + assembly.info.sub_assemblies_count + Podskupiny + + + + + assembly.builds.stocked + skladem + + + + + assembly.builds.needed + potřebné + + + + + assembly.add_parts_to_assembly + Přidat součásti do sestavy + + + + + assembly.bom.name + Název + + + + + assembly.bom.comment + Poznámky + + + + + assembly.builds.following_bom_entries_miss_instock_n + Není dostatek součástí na skladě pro sestavení této sestavy %number_of_builds% krát. Následující součásti nejsou skladem: + + + + + assembly.build.help + Vyberte, ze kterých zásob se mají brát potřebné součásti pro sestavení (a v jakém množství). Zaškrtněte políčko u každého dílu, pokud jste jej odebrali, nebo použijte horní políčko k výběru všech naráz. + + + + + assembly.build.required_qty + Požadované množství + + + + + assembly.import_bom + Importovat součásti do sestavy + + + + + assembly.bom.part + Součást + + + + + assembly.bom.add_entry + Přidat položku + + + + + assembly.bom.price + Cena + + + + + assembly.build.dont_check_quantity + Neověřovat množství + + + + + assembly.build.dont_check_quantity.help + Pokud je tato volba vybrána, budou vybraná množství odstraněna ze skladu bez ohledu na to, zda je méně nebo více součástí, než je skutečně potřeba pro sestavení sestavy. + + + + + assembly.build.add_builds_to_builds_part + Přidat vyrobené instance do součásti sestavy + + + + + assembly.bom_import.type + Typ + + + + + assembly.bom_import.type.json + JSON pro sestavu + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Smazat existující položky před importem + + + + + assembly.bom_import.clear_existing_bom.help + Pokud je tato možnost vybrána, budou všechny již existující součásti sestavy smazány a nahrazeny importovanými daty součástí. + + + + + assembly.import_bom.template.header.json + Šablona importu JSON pro sestavu + + + + + assembly.import_bom.template.header.kicad_pcbnew + Šablona importu CSV (KiCAD Pcbnew BOM) pro sestavu + + + + + assembly.bom_import.template.entry.name + Název součásti v sestavě + + + + + assembly.bom_import.template.entry.part.mpnr + Unikátní číslo produktu u výrobce + + + + + assembly.bom_import.template.entry.part.ipn + Unikátní IPN součásti + + + + + assembly.bom_import.template.entry.part.name + Unikátní název součásti + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Unikátní jméno výrobce + + + + + assembly.bom_import.template.entry.part.category.name + Unikátní název kategorie + + + + + assembly.bom_import.template.json.table + + + + + Pole + Podmínka + Datový typ + Popis + + + + + quantity + Povinné + Číslo s plovoucí desetinnou čárkou (Float) + Musí být uvedeno a obsahovat hodnotu s plovoucí desetinnou čárkou (Float) větší než 0,0. + + + name + Volitelné + Řetězec (String) + Pokud je přítomen, musí být neprázdný řetězec. + + + part + Volitelné + Objekt/Array + + Pokud je uvedeno, musí to být objekt/array a minimálně jedno pole musí být vyplněno: +
      +
    • part.id
    • +
    • part.name
    • +
    + + + + part.id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID součástky v Part-DB. + + + part.name + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není zadáno part.mpnr nebo part.ipn. + + + part.mpnr + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není zadáno part.name nebo part.ipn. + + + part.ipn + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není zadáno part.name nebo part.mpnr. + + + part.description + Volitelné + Řetězec nebo null + Pokud je přítomen, musí být neprázdný řetězec nebo null. + + + part.manufacturer + Volitelné + Objekt/Array + + Pokud je přítomen, musí to být objekt/array a minimálně jedno pole musí být vyplněno: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID výrobce. + + + manufacturer.name + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno manufacturer.id. + + + part.category + Volitelné + Objekt/Array + + Pokud je přítomen, musí to být objekt/array a minimálně jedno pole musí být vyplněno: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID kategorie součástky. + + + category.name + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno category.id. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Očekávané sloupce: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Poznámka: Neprobíhá přiřazení ke konkrétním součástem ze správy kategorií.

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Pole + Podmínka + Datový typ + Popis + + + + + Id + Volitelný + Celé číslo (Integer) + Volný údaj. Jedinečné identifikační číslo pro každou součástku. + + + Designator + Volitelný + Řetězec (String) + Volný údaj. Jedinečný referenční označovač součástky na desce plošných spojů, např. „R1“ pro rezistor 1. Používá se pro název osazení součástky v rámci skupiny součástek. + + + Package + Volitelný + Řetězec (String) + Volný údaj. Pouzdro nebo tvar součástky, např. „0805“ pro SMD rezistory. + + + Množství + Povinný + Celé číslo (Integer) + Počet identických součástek, které jsou potřeba k vytvoření jedné instance sestavy. + + + Určení + Povinný + Řetězec (String) + Popis nebo funkce součástky, např. hodnota rezistoru „10kΩ“ nebo hodnota kondenzátoru „100nF“. Používá se pro název položky v BOM. + + + Dodavatel a ref + Volitelný + Řetězec (String) + Volný údaj. Může obsahovat např. specifické údaje distributora. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (součást) + + + + + typeahead.parts.assembly.name + %name% (sestava) + + + + + projects.build.form.part + Součást "%name%" + + + + + projects.build.form.assembly + Sestava "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (potřebné množství: %quantity%) + + + + + projects.build.form.assembly.bom.entry.no.stock + není skladem + + diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index d72589864..f231163a1 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -4748,6 +4748,18 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Navn + + + project.bom.assembly + Montering + + + + + project.bom.partOrAssembly + Valg + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9812,6 +9824,18 @@ Element 3 Komponent + + + project.bom.assembly + Baugruppe + + + + + project.bom.partOrAssembly + Auswahl + + project.bom.add_entry @@ -9890,6 +9914,42 @@ Element 3 Arkiveret + + + assembly.edit.status + Status + + + + + assembly.status.draft + Kladde + + + + + assembly.status.planning + Under planlægning + + + + + assembly.status.in_production + I produktion + + + + + assembly.status.finished + Ophørt + + + + + assembly.status.archived + Arkiveret + + part.new_build_part.error.build_part_already_exists @@ -10166,6 +10226,12 @@ Element 3 På lager + + + project.builds.no_stock + intet lager angivet + + project.builds.needed @@ -10238,6 +10304,12 @@ Element 3 Mål mængde + + + project.build.builds_part_lot_label + %name% (%quantity% påkrævet) + + project.builds.number_of_builds @@ -12196,5 +12268,633 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Du forsøgte at fjerne/tilføje en mængde sat til nul! Der blev ikke foretaget nogen handling. + + + part.table.name.value.for_part + %value% (Del) + + + + + part.table.name.value.for_assembly + %value% (Samlingsenhed) + + + + + assembly.label + Samling + + + + + assembly.caption + Samling + + + + + perm.assemblies + Samlinger + + + + + assembly_bom_entry.label + Komponenter + + + + + assembly.labelp + Samlinger + + + + + assembly.edit + Rediger samling + + + + + assembly.new + Ny samling + + + + + assembly.edit.associated_build_part + Tilknyttet komponent + + + + + assembly.edit.associated_build_part.add + Tilføj komponent + + + + + assembly.edit.associated_build.hint + Denne komponent repræsenterer de fremstillede instanser af samlingen. Angiv, hvis fremstillede instanser er påkrævet. Hvis ikke, vil antallet af komponenter først blive anvendt ved opbygning af det pågældende projekt. + + + + + assembly.edit.bom.import_bom + Importér komponenter + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Samlinger + + + + + assembly.bom_import.flash.success + %count% komponent(er) blev importeret til samlingen med succes. + + + + + assembly.bom_import.flash.invalid_entries + Valideringsfejl! Kontroller venligst den importerede fil! + + + + + assembly.bom_import.flash.invalid_file + Filen kunne ikke importeres. Kontrollér, at du har valgt den korrekte filtype. Fejlmeddelelse: %message% + + + + + assembly.bom.quantity + Mængde + + + + + assembly.bom.mountnames + Monteringsnavne + + + + + assembly.bom.instockAmount + Antal på lager + + + + + assembly.info.title + Samleinfo + + + + + assembly.info.info.label + Info + + + + + assembly.info.sub_assemblies.label + Undergruppe + + + + + assembly.info.builds.label + Byggeri + + + + + assembly.info.bom_add_parts + Tilføj dele + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Du bør kontrollere, om du virkelig ønsker at bygge samlingen med denne status!]]> + + + + + assembly.builds.build_not_possible + Opbygning ikke mulig: Ikke nok komponenter til rådighed + + + + + assembly.builds.following_bom_entries_miss_instock + Der er ikke nok dele på lager til at bygge dette projekt %number_of_builds% gange. Følgende dele mangler på lager: + + + + + assembly.builds.build_possible + Byggeri muligt + + + + + assembly.builds.number_of_builds_possible + %max_builds% eksemplarer af denne samling.]]> + + + + + assembly.builds.number_of_builds + Antal opbygninger + + + + + assembly.build.btn_build + Byg + + + + + assembly.builds.no_stocked_builds + Antal lagrede byggede enheder + + + + + assembly.info.bom_entries_count + Komponenter + + + + + assembly.info.sub_assemblies_count + Undergrupper + + + + + assembly.builds.stocked + på lager + + + + + assembly.builds.needed + nødvendig + + + + + assembly.add_parts_to_assembly + Tilføj dele til samlingen + + + + + assembly.bom.name + Navn + + + + + assembly.bom.comment + Notater + + + + + assembly.builds.following_bom_entries_miss_instock_n + Der er ikke nok dele på lager til at bygge denne samling %number_of_builds% gange. Følgende dele mangler på lager: + + + + + assembly.build.help + Vælg, hvilke lagre de nødvendige dele til bygningen skal tages fra (og i hvilken mængde). Marker afkrydsningsfeltet for hver delpost, når du har fjernet delene, eller brug det øverste afkrydsningsfelt for at markere alle på én gang. + + + + + assembly.build.required_qty + Krævet antal + + + + + assembly.import_bom + Importer dele til samling + + + + + assembly.bom.part + Del + + + + + assembly.bom.add_entry + Tilføj post + + + + + assembly.bom.price + Pris + + + + + assembly.build.dont_check_quantity + Tjek ikke mængder + + + + + assembly.build.dont_check_quantity.help + Hvis denne mulighed vælges, fjernes de valgte mængder fra lageret, uanset om der er mere eller mindre end nødvendigt for at bygge samlingen. + + + + + assembly.build.add_builds_to_builds_part + Tilføj byggede enheder til assemblies del + + + + + assembly.bom_import.type + Type + + + + + assembly.bom_import.type.json + JSON for en samling + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Slet eksisterende poster før import + + + + + assembly.bom_import.clear_existing_bom.help + Hvis dette valg er markeret, slettes alle eksisterende komponentposter i samlingen og erstattes med de importerede. + + + + + assembly.import_bom.template.header.json + JSON-importskabelon til en samling + + + + + assembly.import_bom.template.header.kicad_pcbnew + Importskabelon CSV (KiCAD Pcbnew BOM) til en samling + + + + + assembly.bom_import.template.entry.name + Delens navn i samlingen + + + + + assembly.bom_import.template.entry.part.mpnr + Unik produktnummer hos producenten + + + + + assembly.bom_import.template.entry.part.ipn + Unik IPN for delen + + + + + assembly.bom_import.template.entry.part.name + Unikt komponentnavn + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Unikt producenter navn + + + + + assembly.bom_import.template.entry.part.category.name + Kategoriens unikke navn + + + + + assembly.bom_import.template.json.table + + + + + Felt + Betingelse + Datatype + Beskrivelse + + + + + quantity + Påkrævet + Flydende tal (Float) + Skal være til stede og indeholde en flydende værdi (Float), der er større end 0,0. + + + name + Valgfri + String + Hvis til stede, skal det være en ikke-tom streng. + + + part + Valgfri + Objekt/Array + + Hvis angivet, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: +
      +
    • part.id
    • +
    • part.name
    • +
    + + + + part.id + Valgfri + Heltal (Integer) + Heltal (Integer) > 0. Matcher Part-DB's interne numeriske ID for komponenten. + + + part.name + Valgfri + String + Ikke-tom streng, hvis part.mpnr eller part.ipn ikke er givet. + + + part.mpnr + Valgfri + String + Ikke-tom streng, hvis part.name eller part.ipn ikke er givet. + + + part.ipn + Valgfri + String + Ikke-tom streng, hvis part.name eller part.mpnr ikke er givet. + + + part.description + Valgfri + String eller null + Hvis til stede, skal det være en ikke-tom streng eller null. + + + part.manufacturer + Valgfri + Objekt/Array + + Hvis til stede, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Valgfri + Heltal (Integer) + Heltal (Integer) > 0. Matcher producentens interne numeriske ID. + + + manufacturer.name + Valgfri + String + Ikke-tom streng, hvis manufacturer.id ikke er givet. + + + part.category + Valgfri + Objekt/Array + + Hvis til stede, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Valgfri + Heltal (Integer) + Heltal (Integer) > 0. Matcher komponentkategoriens interne numeriske ID. + + + category.name + Valgfri + String + Ikke-tom streng, hvis category.id ikke er givet. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Forventede kolonner: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Bemærk: Der foretages ingen tildelinger til konkrete komponenter fra kategoristyringen.

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Felt + Betingelse + Datatype + Beskrivelse + + + + + Id + Valgfri + Heltal (Integer) + Fri oplysning. Et unikt identifikationsnummer for hver komponent. + + + Designator + Valgfri + Streng + Fri oplysning. En unik referencebetegnelse for komponenten på printkortet, f.eks. "R1" for modstand 1. Bruges til navngivning af monteringssæt i komponentgruppen. + + + Package + Valgfri + Streng + Fri oplysning. Komponentenheden eller -formatet, f.eks. "0805" for SMD-modstande. + + + Antal + Påkrævet + Heltal (Integer) + Antallet af identiske komponenter, der kræves for at oprette en enkelt instans af samling. + + + Betegnelse + Påkrævet + Streng + Beskrivelse eller funktion for komponenten, f.eks. modstandsværdi "10kΩ" eller kondensatorværdi "100nF". Bruges til navnet i BOM-posten. + + + Leverandør og ref + Valgfri + Streng + Fri oplysning. Kan indeholde f.eks. distributørspecifik værdi. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (del) + + + + + typeahead.parts.assembly.name + %name% (samling) + + + + + projects.build.form.part + Del "%name%" + + + + + projects.build.form.assembly + Samling "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% nødvendig) + + + + + projects.build.form.assembly.bom.entry.no.stock + ikke på lager + + diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 9fb3f6ef6..c32bf46a7 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -4740,6 +4740,18 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr Name + + + part.table.name.value.for_part + %value% (Bauteil) + + + + + part.table.name.value.for_assembly + %value% (Baugruppe) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9788,6 +9800,18 @@ Element 1 -> Element 1.2 Bauteil + + + project.bom.assembly + Baugruppe + + + + + project.bom.partOrAssembly + Auswahl + + project.bom.add_entry @@ -9866,6 +9890,42 @@ Element 1 -> Element 1.2 Archiviert + + + assembly.edit.status + Status + + + + + assembly.status.draft + Entwurf + + + + + assembly.status.planning + In Planung + + + + + assembly.status.in_production + In Produktion + + + + + assembly.status.finished + Abgeschlossen + + + + + assembly.status.archived + Archiviert + + part.new_build_part.error.build_part_already_exists @@ -10142,6 +10202,12 @@ Element 1 -> Element 1.2 vorhanden + + + project.builds.no_stock + kein Lager angegeben + + project.builds.needed @@ -10214,6 +10280,12 @@ Element 1 -> Element 1.2 Ziel-Bestand + + + project.build.builds_part_lot_label + %name% (%quantity% benötigt) + + project.builds.number_of_builds @@ -12851,6 +12923,622 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Externe Version anzeigen + + + assembly.label + Baugruppe + + + + + assembly.caption + Baugruppe + + + + + perm.assemblies + Baugruppen + + + + + assembly_bom_entry.label + Bauteile + + + + + assembly.labelp + Baugruppen + + + + + assembly.edit + Bearbeite Baugruppe + + + + + assembly.new + Neue Baugruppe + + + + + assembly.edit.associated_build_part + Verknüpftes Bauteil + + + + + assembly.edit.associated_build_part.add + Bauteil hinzufügen + + + + + assembly.edit.associated_build.hint + Dieses Bauteil repräsentiert die gebauten Instanzen der Baugruppe. Anzugeben, sofern gebaute Instanzen benötigt werden. Wenn nein, werden die Stückzahlen bzgl. der Baugruppe erst beim Build des jeweiligen Projektes herangezogen. + + + + + assembly.edit.bom.import_bom + Bauteile importieren + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Baugruppen + + + + + assembly.bom_import.flash.success + %count% Part Einträge erfolgreich in Baugruppe importiert. + + + + + assembly.bom_import.flash.invalid_entries + Validierungsfehler! Bitte überprüfen Sie die importierte Datei! + + + + + assembly.bom_import.flash.invalid_file + Datei konnte nicht importiert werden. Überprüfen Sie, dass Sie den richtigen Dateityp gewählt haben. Fehlermeldung: %message% + + + + + assembly.bom.quantity + Menge + + + + + assembly.bom.mountnames + Bestückungsnamen + + + + + assembly.bom.instockAmount + Bestand im Lager + + + + + assembly.info.title + Baugruppen-Info + + + + + assembly.info.info.label + Info + + + + + assembly.info.sub_assemblies.label + Untergruppe + + + + + assembly.info.builds.label + Bau + + + + + assembly.info.bom_add_parts + Bauteile hinzufügen + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Sie sollten überprüfen, ob sie die Baugruppe mit diesem Status wirklich bauen wollen!]]> + + + + + assembly.builds.build_not_possible + Bau nicht möglich: Nicht genügend Bauteile vorhanden + + + + + assembly.builds.following_bom_entries_miss_instock + Es sind nicht genügend Bauteile auf Lager, um dieses Projekt %number_of_builds% mal zu bauen. Von folgenden Bauteilen ist nicht genügend auf Lager. + + + + + assembly.builds.build_possible + Bau möglich + + + + + assembly.builds.number_of_builds_possible + %max_builds% Exemplare dieser Baugruppe zu bauen.]]> + + + + + assembly.builds.number_of_builds + Zu bauende Anzahl + + + + + assembly.build.btn_build + Bauen + + + + + assembly.builds.no_stocked_builds + Anzahl gelagerter gebauter Instanzen + + + + + assembly.info.bom_entries_count + Bauteile + + + + + assembly.info.sub_assemblies_count + Untergruppen + + + + + assembly.builds.stocked + vorhanden + + + + + assembly.builds.needed + benötigt + + + + + assembly.add_parts_to_assembly + Bauteile zur Baugruppe hinzufügen + + + + + assembly.bom.name + Name + + + + + assembly.bom.comment + Notizen + + + + + assembly.builds.following_bom_entries_miss_instock_n + Es sind nicht genügend Bauteile auf Lager, um diese Baugruppe %number_of_builds% mal zu bauen. Von folgenden Bauteilen ist nicht genügend auf Lager: + + + + + assembly.build.help + Wählen Sie aus, aus welchen Beständen die zum Bau notwendigen Bauteile genommen werden sollen (und in welcher Anzahl). Setzen Sie den Haken für jeden Part Eintrag, wenn sie die Bauteile entnommen haben, oder nutzen Sie die oberste Checkbox, um alle Haken auf einmal zu setzen. + + + + + assembly.build.required_qty + Benötigte Anzahl + + + + + assembly.import_bom + Importiere Parts für Baugruppe + + + + + assembly.bom.part + Bauteil + + + + + assembly.bom.add_entry + Eintrag hinzufügen + + + + + assembly.bom.price + Preis + + + + + assembly.build.dont_check_quantity + Mengen nicht überprüfen + + + + + assembly.build.dont_check_quantity.help + Wenn diese Option gewählt wird, werden die gewählten Mengen aus dem Lager entfernt, egal ob mehr oder weniger Bauteile sind, als für den Bau der Baugruppe eigentlich benötigt werden. + + + + + assembly.build.add_builds_to_builds_part + Gebaute Instanzen zum Bauteil der Baugruppe hinzufügen + + + + + assembly.bom_import.type + Typ + + + + + assembly.bom_import.type.json + JSON für eine Baugruppe + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Lösche existierende Bauteil-Einträge vor dem Import + + + + + assembly.bom_import.clear_existing_bom.help + Wenn diese Option ausgewählt ist, werden alle bereits in der Baugruppe existierenden Bauteile gelöscht und mit den importierten Bauteildaten überschrieben. + + + + + assembly.import_bom.template.header.json + Import-Vorlage JSON für eine Baugruppe + + + + + assembly.import_bom.template.header.kicad_pcbnew + Import-Vorlage CSV (KiCAD Pcbnew BOM) für eine Baugruppe + + + + + assembly.bom_import.template.entry.name + Name des Bauteils in der Baugruppe + + + + + assembly.bom_import.template.entry.part.mpnr + Eindeutige Produktnummer innerhalb des Herstellers + + + + + assembly.bom_import.template.entry.part.ipn + Eideutige IPN des Bauteils + + + + + assembly.bom_import.template.entry.part.name + Eindeutiger Name des Bauteils + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Eindeutiger Name des Herstellers + + + + + assembly.bom_import.template.entry.part.category.name + Eindeutiger Name der Kategorie + + + + + assembly.bom_import.template.json.table + + + + + Feld + Bedingung + Datentyp + Beschreibung + + + + + quantity + Pflichtfeld + Gleitkommazahl (Float) + Muss gegeben sein und enthält einen Gleitkommawert (Float), der größer als 0.0 ist. + + + name + Optional + String + Falls vorhanden, muss es ein nicht-leerer String sein. + + + part + Optional + Objekt/Array + + Falls angegeben, muss es ein Objekt/Array sein und mindestens eines der Felder ausgefüllt sein: +
      +
    • part.id
    • +
    • part.name
    • +
    + + + + part.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. + + + part.name + Optional + String + Nicht-leerer String, falls keine part.mpnr- bzw. part.ipn-Angabe gegeben ist. + + + part.mpnr + Optional + String + Nicht-leerer String, falls keine part.name- bzw. part-ipn-Angabe gegeben ist. + + + part.ipn + Optional + String + Nicht-leerer String, falls keine part.name- bzw. part.mpnr-Angabe gegeben ist. + + + part.description + Optional + String oder null + Falls vorhanden, muss es ein nicht-leerer String sein oder null. + + + part.manufacturer + Optional + Objekt/Array + + Falls vorhanden, muss es ein Objekt/Array sein und mindestens eines der Felder ausgefüllt sein: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID des Herstellers. + + + manufacturer.name + Optional + String + Nicht-leerer String, falls keine manufacturer.id-Angabe gegeben ist. + + + part.category + Optional + Objekt/Array + + Falls vorhanden, muss es ein Objekt/Array sein und mindestens eines der Felder ausgefüllt sein: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID der Kategorie des Bauteils. + + + category.name + Optional + String + Nicht-leerer String, falls keine category.id-Angabe gegeben ist. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Erwartete Spalten: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Hinweis: Es findet keine Zuordnung zu konkreten Bauteilen aus der Kategorie-Verwaltung statt.

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Feld + Bedingung + Datentyp + Beschreibung + + + + + Id + Optional + Ganzzahl (Integer) + Offene Angabe. Eine eindeutige Identifikationsnummer für jedes Bauteil. + + + Designator + Optional + String + Offene Angabe. Ein eindeutiger Referenzbezeichner des Bauteils auf der Leiterplatte, z.B. „R1“ für Widerstand 1. Wird für den Bestückungsnamen des Bauteil-Eintrags innerhalb der Bauteilgruppe verwendet. + + + Package + Optional + String + Offene Angabe. Das Gehäuse oder die Bauform des Bauteils, z.B. „0805“ für SMD-Widerstände. + + + Quantity + Pflichtfeld + Ganzzahl (Integer) + Anzahl der identischen Bauteile, die benötigt werden, um eine Instanz der Baugruppe zu erstellen. + + + Designation + Pflichtfeld + String + Beschreibung oder Funktion des Bauteils, z.B. Widerstandswert „10kΩ“ oder Kondensatorwert „100nF“. Wird für den Namen des BOM-Eintrags verwendet. + + + Supplier and ref + Optional + String + Offene Angabe. Kann z.B. Distributor spezifischen Wert enthalten. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (Bauteil) + + + + + typeahead.parts.assembly.name + %name% (Baugruppe) + + + + + projects.build.form.part + Bauteil "%name%" + + + + + projects.build.form.assembly + Baugruppe "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% benötigt) + + + + + projects.build.form.assembly.bom.entry.no.stock + nicht auf Lager + + part.table.actions.error diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index cc17d9be4..8fdb801b7 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1535,5 +1535,693 @@ Επεξεργασία + + + part.table.name.value.for_part + %value% (Μέρος) + + + + + part.table.name.value.for_assembly + %value% (Συναρμολόγηση) + + + + + project.bom.assembly + Συναρμολόγηση + + + + + project.bom.partOrAssembly + Επιλογή + + + + + assembly.edit.status + Κατάσταση + + + + + assembly.status.draft + Προσχέδιο + + + + + assembly.status.planning + Υπό σχεδιασμό + + + + + assembly.status.in_production + Σε παραγωγή + + + + + assembly.status.finished + Ολοκληρώθηκε + + + + + assembly.status.archived + Αρχειοθετήθηκε + + + + + project.builds.no_stock + δεν έχει καθοριστεί απόθεμα + + + + + project.build.builds_part_lot_label + %name% (%quantity% απαιτείται) + + + + + assembly.label + Σύνολο + + + + + assembly.caption + Σύνολο + + + + + perm.assemblies + Συναρμολογήσεις + + + + + assembly_bom_entry.label + Μέρη + + + + + assembly.labelp + Συναρμολογήσεις + + + + + assembly.edit + Επεξεργασία συνόλου + + + + + assembly.new + Νέο σύνολο + + + + + assembly.edit.associated_build_part + Σχετικό μέρος + + + + + assembly.edit.associated_build_part.add + Προσθήκη μέρους + + + + + assembly.edit.associated_build.hint + Αυτό το μέρος αντιπροσωπεύει τις κατασκευασμένες εκδόσεις του συνόλου. Καταχωρίστε το εάν απαιτούνται κατασκευασμένες εκδόσεις. Εάν όχι, οι ποσότητες θα χρησιμοποιηθούν μόνο κατά την κατασκευή του εκάστοτε έργου. + + + + + assembly.edit.bom.import_bom + Εισαγωγή μερών + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Συναρμολογήσεις + + + + + assembly.bom_import.flash.success + %count% εγγραφές εξαρτημάτων εισήχθησαν με επιτυχία στο σύνολο. + + + + + assembly.bom_import.flash.invalid_entries + Σφάλμα επικύρωσης! Ελέγξτε το εισαγόμενο αρχείο! + + + + + assembly.bom_import.flash.invalid_file + Το αρχείο δεν μπόρεσε να εισαχθεί. Ελέγξτε ότι έχετε επιλέξει τον σωστό τύπο αρχείου. Μήνυμα σφάλματος: %message% + + + + + assembly.bom.quantity + Ποσότητα + + + + + assembly.bom.mountnames + Ονόματα συναρμολόγησης + + + + + assembly.bom.instockAmount + Ποσότητα σε απόθεμα + + + + + assembly.info.title + Πληροφορίες συναρμολόγησης + + + + + assembly.info.info.label + Πληροφορίες + + + + + assembly.info.sub_assemblies.label + Υποομάδες + + + + + assembly.info.builds.label + Κατασκευές + + + + + assembly.info.bom_add_parts + Προσθήκη εξαρτημάτων + + + + + assembly.builds.check_assembly_status + «%assembly_status%». Ελέγξτε εάν θέλετε πραγματικά να κατασκευάσετε τη συναρμολόγηση με αυτήν την κατάσταση!]]> + + + + + assembly.builds.build_not_possible + Η κατασκευή δεν είναι δυνατή: Δεν υπάρχουν αρκετά διαθέσιμα εξαρτήματα + + + + + assembly.builds.following_bom_entries_miss_instock + Δεν υπάρχουν αρκετά εξαρτήματα σε απόθεμα για να κατασκευαστεί αυτό το έργο %number_of_builds% φορές. Λείπουν τα ακόλουθα εξαρτήματα: + + + + + assembly.builds.build_possible + Κατασκευή δυνατή + + + + + assembly.builds.number_of_builds_possible + %max_builds% μονάδες αυτής της συναρμολόγησης.]]> + + + + + assembly.builds.number_of_builds + Αριθμός κατασκευών + + + + + assembly.build.btn_build + Κατασκευή + + + + + assembly.builds.no_stocked_builds + Αποθηκευμένα κατασκευασμένα κομμάτια + + + + + assembly.info.bom_entries_count + Εξαρτήματα + + + + + assembly.info.sub_assemblies_count + Υποομάδες + + + + + assembly.builds.stocked + σε απόθεμα + + + + + assembly.builds.needed + απαιτούμενο + + + + + assembly.add_parts_to_assembly + Προσθήκη εξαρτημάτων στη συναρμολόγηση + + + + + assembly.bom.name + Όνομα + + + + + assembly.bom.comment + Σχόλια + + + + + assembly.builds.following_bom_entries_miss_instock_n + Δεν υπάρχουν αρκετά εξαρτήματα σε απόθεμα για να κατασκευαστεί αυτή η συναρμολόγηση %number_of_builds% φορές. Λείπουν τα ακόλουθα εξαρτήματα: + + + + + assembly.build.help + Επιλέξτε από ποια αποθέματα θα αφαιρεθούν τα αναγκαία για την κατασκευή εξαρτήματα (και σε ποια ποσότητα). Σημειώστε το πλαίσιο επιλογής για κάθε εξάρτημα όταν αφαιρέσετε τα εξαρτήματα ή χρησιμοποιήστε το ανώτερο πλαίσιο επιλογής για να τα ελέγξετε όλα ταυτόχρονα. + + + + + assembly.build.required_qty + Απαιτούμενη ποσότητα + + + + + assembly.import_bom + Εισαγωγή εξαρτημάτων συναρμολόγησης + + + + + assembly.bom.part + Εξάρτημα + + + + + assembly.bom.add_entry + Προσθήκη καταχώρησης + + + + + assembly.bom.price + Τιμή + + + + + assembly.build.dont_check_quantity + Μην ελέγχετε την ποσότητα + + + + + assembly.build.dont_check_quantity.help + Εάν επιλεγεί αυτή η επιλογή, οι επιλεγμένες ποσότητες θα αφαιρεθούν από το απόθεμα ανεξάρτητα από το αν είναι περισσότερο ή λιγότερο από το απαιτούμενο για την κατασκευή της συναρμολόγησης. + + + + + assembly.build.add_builds_to_builds_part + Προσθήκη κατασκευασμένων κομματιών στο τμήμα συναρμολόγησης + + + + + assembly.bom_import.type + Τύπος + + + + + assembly.bom_import.type.json + JSON για συναρμολόγηση + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Διαγραφή υπαρχόντων εξαρτημάτων πριν από την εισαγωγή + + + + + assembly.bom_import.clear_existing_bom.help + Εάν επιλεγεί αυτή η επιλογή, όλα τα ήδη υπάρχοντα εξαρτήματα στη συναρμολόγηση θα διαγραφούν και θα αντικατασταθούν με τα δεδομένα εξαρτημάτων που εισάγονται. + + + + + assembly.import_bom.template.header.json + Πρότυπο εισαγωγής JSON για συναρμολόγηση + + + + + assembly.import_bom.template.header.kicad_pcbnew + Πρότυπο εισαγωγής CSV (KiCAD Pcbnew BOM) για συναρμολόγηση + + + + + assembly.bom_import.template.entry.name + Όνομα του εξαρτήματος στη συναρμολόγηση + + + + + assembly.bom_import.template.entry.part.mpnr + Μοναδικός αριθμός προϊόντος από τον κατασκευαστή + + + + + assembly.bom_import.template.entry.part.ipn + Μοναδικός IPN του εξαρτήματος + + + + + assembly.bom_import.template.entry.part.name + Μοναδικό όνομα εξαρτήματος + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Μοναδικό όνομα κατασκευαστή + + + + + assembly.bom_import.template.entry.part.category.name + Μοναδικό όνομα κατηγορίας + + + + + assembly.bom_import.template.json.table + + + + + Πεδίο + Προϋπόθεση + Τύπος Δεδομένων + Περιγραφή + + + + + quantity + Υποχρεωτικό πεδίο + Αριθμός κινητής υποδιαστολής (Float) + Πρέπει να παρέχεται και να περιέχει τιμή κινητής υποδιαστολής (Float) μεγαλύτερη από 0.0. + + + name + Προαιρετικό + Κείμενο (String) + Εάν υπάρχει, πρέπει να είναι μη κενό κείμενο. + + + part + Προαιρετικό + Αντικείμενο/Πίνακας + + Εάν παρέχεται, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα πεδία του να είναι συμπληρωμένο: +
      +
    • part.id
    • +
    • part.name
    • +
    + + + + part.id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος (Integer) > 0. Αντιστοιχεί στην εσωτερική αριθμητική ταυτότητα (ID) του εξαρτήματος στη βάση δεδομένων. + + + part.name + Προαιρετικό + Κείμενο (String) + Μη κενό κείμενο, εάν δεν παρέχονται οι ενδείξεις part.mpnr ή part.ipn. + + + part.mpnr + Προαιρετικό + Κείμενο (String) + Μη κενό κείμενο, εάν δεν παρέχονται οι ενδείξεις part.name ή part.ipn. + + + part.ipn + Προαιρετικό + Κείμενο (String) + Μη κενό κείμενο, εάν δεν παρέχονται οι ενδείξεις part.name ή part.mpnr. + + + part.description + Προαιρετικό + Κείμενο ή null + Εάν υπάρχει, πρέπει να είναι μη κενό κείμενο, ή null. + + + part.manufacturer + Προαιρετικό + Αντικείμενο/Πίνακας + + Εάν υπάρχει, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα πεδία του να είναι συμπληρωμένο: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος (Integer) > 0. Αντιστοιχεί στην εσωτερική αριθμητική ταυτότητα (ID) του κατασκευαστή. + + + manufacturer.name + Προαιρετικό + Κείμενο (String) + Μη κενό κείμενο, εάν δεν παρέχεται η ένδειξη manufacturer.id. + + + part.category + Προαιρετικό + Αντικείμενο/Πίνακας + + Εάν υπάρχει, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα πεδία του να είναι συμπληρωμένο: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος (Integer) > 0. Αντιστοιχεί στην εσωτερική αριθμητική ταυτότητα (ID) της κατηγορίας του εξαρτήματος. + + + category.name + Προαιρετικό + Κείμενο (String) + Μη κενό κείμενο, εάν δεν παρέχεται η ένδειξη category.id. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Αναμενόμενες στήλες: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Σημείωση: Δεν πραγματοποιείται αντιστοίχιση με συγκεκριμένα εξαρτήματα από τη διαχείριση κατηγοριών.

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Πεδίο + Εκπλήρωση + Τύπος δεδομένων + Περιγραφή + + + + + Id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ελεύθερη καταχώρηση. Μοναδικός αριθμός ταυτοποίησης για κάθε εξάρτημα. + + + Σχεδιαστής + Προαιρετικό + Συμβολοσειρά (String) + Ελεύθερη καταχώρηση. Μοναδικός αναγνωριστικός δείκτης του εξαρτήματος στην πλακέτα κυκλώματος, π.χ. "R1" για την αντίσταση 1. Χρησιμοποιείται για το όνομα του εξαρτήματος στο πλαίσιο της ομάδας εξαρτημάτων. + + + Συσκευασία + Προαιρετικό + Συμβολοσειρά (String) + Ελεύθερη καταχώρηση. Ο τύπος ή η μορφή του εξαρτήματος, π.χ. "0805" για αντιστάσεις SMD. + + + Ποσότητα + Υποχρεωτικό + Ακέραιος αριθμός (Integer) + Ο αριθμός των πανομοιότυπων εξαρτημάτων που απαιτούνται για τη δημιουργία μίας μονάδας του συνόλου. + + + Ορισμός + Υποχρεωτικό + Συμβολοσειρά (String) + Περιγραφή ή λειτουργία του εξαρτήματος, π.χ. αντίσταση "10kΩ" ή χωρητικότητα "100nF". Χρησιμοποιείται για το όνομα της εγγραφής στο BOM. + + + Προμηθευτής και παραπομπή + Προαιρετικό + Συμβολοσειρά (String) + Ελεύθερη καταχώρηση. Μπορεί να περιλαμβάνει, π.χ., ειδική τιμή από διανομέα. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (Εξάρτημα) + + + + + typeahead.parts.assembly.name + %name% (Συναρμολόγηση) + + + + + projects.build.form.part + Εξάρτημα "%name%" + + + + + projects.build.form.assembly + Συναρμολόγηση "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% απαιτείται) + + + + + projects.build.form.assembly.bom.entry.no.stock + δεν υπάρχει στο απόθεμα + + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index be1e63488..d267c89ac 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -4741,6 +4741,18 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Name + + + part.table.name.value.for_part + %value% (Part) + + + + + part.table.name.value.for_assembly + %value% (Assembly) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9789,6 +9801,18 @@ Element 1 -> Element 1.2 Part + + + project.bom.assembly + Assembly + + + + + project.bom.partOrAssembly + Selection + + project.bom.add_entry @@ -9867,6 +9891,42 @@ Element 1 -> Element 1.2 Archived + + + assembly.edit.status + Project status + + + + + assembly.status.draft + Draft + + + + + assembly.status.planning + Planning + + + + + assembly.status.in_production + In production + + + + + assembly.status.finished + Finished + + + + + assembly.status.archived + Archived + + part.new_build_part.error.build_part_already_exists @@ -10143,6 +10203,12 @@ Element 1 -> Element 1.2 stocked + + + project.builds.no_stock + no stock specified + + project.builds.needed @@ -10215,6 +10281,12 @@ Element 1 -> Element 1.2 Target lot + + + project.build.builds_part_lot_label + %name% (%quantity% needed) + + project.builds.number_of_builds @@ -12852,6 +12924,622 @@ Please note, that you can not impersonate a disabled user. If you try you will g View external version + + + assembly.label + Assembly + + + + + assembly.caption + Assembly + + + + + assembly_bom_entry.label + Parts + + + + + perm.assemblies + Assemblies + + + + + assembly.labelp + Assemblies + + + + + assembly.edit + Edit assembly + + + + + assembly.new + New assembly + + + + + assembly.edit.associated_build_part + Associated builds part + + + + + assembly.edit.associated_build_part.add + Add builds part + + + + + assembly.edit.associated_build.hint + This part represents the builds of this assembly. To indicate if built instances are required. If not, the number of pieces regarding the assembly are only used for the build of the respective project. + + + + + assembly.edit.bom.import_bom + Import BOM + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Assemblies + + + + + assembly.bom_import.flash.success + Imported %count% parts in assembly successfully. + + + + + assembly.bom_import.flash.invalid_entries + Validation error! Please check your data! + + + + + assembly.bom_import.flash.invalid_file + File could not be imported. Please check that you have selected the right file type. Error message: %message% + + + + + assembly.bom.quantity + Quantity + + + + + assembly.bom.mountnames + Mount names + + + + + assembly.bom.instockAmount + Stocked amount + + + + + assembly.info.title + Assembly info + + + + + assembly.info.info.label + Info + + + + + assembly.info.sub_assemblies.label + Sub-assemblies + + + + + assembly.info.builds.label + Build + + + + + assembly.info.bom_add_parts + Add part entries + + + + + assembly.builds.check_assembly_status + "%assembly_status%". You should check if you really want to build the assembly with this status!]]> + + + + + assembly.builds.build_not_possible + Build not possible: Parts not stocked + + + + + assembly.builds.following_bom_entries_miss_instock + You do not have enough parts stocked to build this assembly %number_of_builds% times. The following parts have missing instock: + + + + + assembly.builds.build_possible + Build possible + + + + + assembly.builds.number_of_builds_possible + %max_builds% builds of this assembly.]]> + + + + + assembly.builds.number_of_builds + Build amount + + + + + assembly.build.btn_build + Build + + + + + assembly.builds.no_stocked_builds + Number of stocked builds + + + + + assembly.info.bom_entries_count + Part entries + + + + + assembly.info.sub_assemblies_count + Sub-assemblies + + + + + assembly.builds.stocked + stocked + + + + + assembly.builds.needed + needed + + + + + assembly.add_parts_to_assembly + Add parts to assembly + + + + + assembly.bom.name + Name + + + + + assembly.bom.comment + Notes + + + + + assembly.builds.following_bom_entries_miss_instock_n + You do not have enough parts stocked to build this assembly %number_of_builds% times. The following parts have missing instock: + + + + + assembly.build.help + Choose from which part lots the stock to build this assembly should be taken (and in which amount). Check the checkbox for each part, when you are finished withdrawing the parts, or use the top checkbox to check all boxes at once. + + + + + assembly.build.required_qty + Required quantity + + + + + assembly.import_bom + Import BOM for project + + + + + assembly.bom.part + Part + + + + + assembly.bom.add_entry + Add entry + + + + + assembly.bom.price + Price + + + + + assembly.build.dont_check_quantity + Do not check quantities + + + + + assembly.build.dont_check_quantity.help + If this option is selected, the given withdraw quantities are used as given, no matter if more or less parts are actually required to build this assembly. + + + + + assembly.build.add_builds_to_builds_part + Add builds to assembly builds part + + + + + assembly.bom_import.type + Type + + + + + assembly.bom_import.type.json + JSON for one assembly + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Clear existing part entries before importing + + + + + assembly.bom_import.clear_existing_bom.help + Selecting this option will remove all existing part entries in the assembly and overwrite them with the imported part data! + + + + + assembly.import_bom.template.header.json + Import template JSON format for one assembly + + + + + assembly.import_bom.template.header.kicad_pcbnew + Import template CSV format (KiCAD Pcbnew BOM) for one assembly + + + + + assembly.bom_import.template.entry.name + Name of the part in the assembly + + + + + assembly.bom_import.template.entry.part.mpnr + Unique product number within the manufacturer + + + + + assembly.bom_import.template.entry.part.ipn + Unique IPN of the part + + + + + assembly.bom_import.template.entry.part.name + Unique name of the part + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Unique name of the manufacturer + + + + + assembly.bom_import.template.entry.part.category.name + Unique name of the category + + + + + assembly.bom_import.template.json.table + + + + + Field + Condition + Data type + Description + + + + + quantity + Required + Floating point (Float) + Must be provided and contains a floating-point value (Float) greater than 0.0. + + + name + Optional + String + If present, it must be a non-empty string. + + + part + Optional + Object/Array + + If provided, it must be an object/array and at least one of the fields must be filled: +
      +
    • part.id
    • +
    • part.name
    • +
    + + + + part.id + Optional + Integer + Integer > 0. Matches the Part-DB internal numeric ID of the component. + + + part.name + Optional + String + Non-empty string if no part.mpnr or part.ipn is provided. + + + part.mpnr + Optional + String + Non-empty string if no part.name or part.ipn is provided. + + + part.ipn + Optional + String + Non-empty string if no part.name or part.mpnr is provided. + + + part.description + Optional + String or null + If present, it must be a non-empty string or null. + + + part.manufacturer + Optional + Object/Array + + If present, it must be an object/array and at least one of the fields must be filled: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Optional + Integer + Integer > 0. Matches the internal numeric ID of the manufacturer. + + + manufacturer.name + Optional + String + Non-empty string if no manufacturer.id is provided. + + + part.category + Optional + Object/Array + + If present, it must be an object/array and at least one of the fields must be filled: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Optional + Integer + Integer > 0. Matches the internal numeric ID of the component's category. + + + category.name + Optional + String + Non-empty string if no category.id is provided. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Expected Columns: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Note: No mapping is performed with specific components from category management.

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Field + Condition + Data Type + Description + + + + + Id + Optional + Integer + Free-form field. A unique identification number for each component. + + + Designator + Optional + String + Free-form field. A unique reference designator of the component on the PCB, e.g., “R1” for resistor 1. Used for naming the placement in the component group. + + + Package + Optional + String + Free-form field. The casing or form factor of the component, e.g., “0805” for SMD resistors. + + + Quantity + Required + Integer + The number of identical components required to create a single instance of an assembly. + + + Designation + Required + String + The description or function of the component, e.g., resistor value “10kΩ” or capacitor value “100nF.” Used for the name in the BOM entry. + + + Supplier and ref + Optional + String + Free-form field. May include, for example, specific distributor information. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (Part) + + + + + typeahead.parts.assembly.name + %name% (Assembly) + + + + + projects.build.form.part + Part "%name%" + + + + + projects.build.form.assembly + Assembly "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% needed) + + + + + projects.build.form.assembly.bom.entry.no.stock + not in stock + + part.table.actions.error diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index fce38e52f..f961630c5 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -4740,6 +4740,18 @@ Subelementos serán desplazados hacia arriba. Nombre + + + part.table.name.value.for_part + %value% (Componente) + + + + + part.table.name.value.for_assembly + %value% (Ensamblaje) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9804,6 +9816,18 @@ Elemento 3 Componente + + + project.bom.assembly + Baugruppe + + + + + project.bom.partOrAssembly + Auswahl + + project.bom.add_entry @@ -9882,6 +9906,42 @@ Elemento 3 Archivado + + + assembly.edit.status + Estatus + + + + + assembly.status.draft + Esbozo + + + + + assembly.status.planning + En planificación + + + + + assembly.status.in_production + En producción + + + + + assembly.status.finished + Completado + + + + + assembly.status.archived + Archivado + + part.new_build_part.error.build_part_already_exists @@ -10158,6 +10218,12 @@ Elemento 3 Almacenado + + + project.builds.no_stock + no se ha especificado stock + + project.builds.needed @@ -10230,6 +10296,12 @@ Elemento 3 Lote objetivo + + + project.build.builds_part_lot_label + %name% (se requiere %quantity%) + + project.builds.number_of_builds @@ -12368,5 +12440,621 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Este componente contiene más de un stock. Cambie la ubicación manualmente para seleccionar el stock deseado. + + + assembly.label + Ensamblaje + + + + + assembly.caption + Ensamblaje + + + + + perm.assemblies + Ensamblajes + + + + + assembly_bom_entry.label + Componentes + + + + + assembly.labelp + Ensamblajes + + + + + assembly.edit + Editar ensamblaje + + + + + assembly.new + Nuevo ensamblaje + + + + + assembly.edit.associated_build_part + Componente asociado + + + + + assembly.edit.associated_build_part.add + Añadir componente + + + + + assembly.edit.associated_build.hint + Este componente representa las instancias fabricadas del ensamblaje. Indique si se necesitan instancias fabricadas. De lo contrario, las cantidades del componente solo se utilizarán cuando se construya el proyecto correspondiente. + + + + + assembly.edit.bom.import_bom + Importar componentes + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Ensamblajes + + + + + assembly.bom_import.flash.success + %count% componente(s) se importaron correctamente al ensamblaje. + + + + + assembly.bom_import.flash.invalid_entries + ¡Error de validación! ¡Revisa el archivo importado! + + + + + assembly.bom_import.flash.invalid_file + No se pudo importar el archivo. Asegúrate de haber seleccionado el tipo de archivo correcto. Mensaje de error: %message% + + + + + assembly.bom.quantity + Cantidad + + + + + assembly.bom.mountnames + Nombres de montaje + + + + + assembly.bom.instockAmount + Cantidad en stock + + + + + assembly.info.title + Información del ensamblaje + + + + + assembly.info.info.label + Información + + + + + assembly.info.sub_assemblies.label + Subconjuntos + + + + + assembly.info.builds.label + Construcciones + + + + + assembly.info.bom_add_parts + Añadir piezas + + + + + assembly.builds.check_assembly_status + "%assembly_status%". ¡Por favor, verifica si realmente deseas construir el ensamblaje con este estado!]]> + + + + + assembly.builds.build_not_possible + Construcción no posible: No hay suficientes componentes disponibles + + + + + assembly.builds.following_bom_entries_miss_instock + No hay suficientes piezas en stock para construir este proyecto %number_of_builds% veces. Faltan las siguientes piezas: + + + + + assembly.builds.build_possible + Construcción posible + + + + + assembly.builds.number_of_builds_possible + %max_builds% unidades de este ensamblaje.]]> + + + + + assembly.builds.number_of_builds + Número de construcciones + + + + + assembly.build.btn_build + Construir + + + + + assembly.builds.no_stocked_builds + Unidades construidas almacenadas + + + + + assembly.info.bom_entries_count + Componentes + + + + + assembly.info.sub_assemblies_count + Subconjuntos + + + + + assembly.builds.stocked + en stock + + + + + assembly.builds.needed + necesario + + + + + assembly.add_parts_to_assembly + Añadir piezas al ensamblaje + + + + + assembly.bom.name + Nombre + + + + + assembly.bom.comment + Comentarios + + + + + assembly.builds.following_bom_entries_miss_instock_n + No hay suficientes piezas en stock para construir este ensamblaje %number_of_builds% veces. Faltan las siguientes piezas: + + + + + assembly.build.help + Seleccione de qué almacenes se tomarán las piezas necesarias para la construcción (y en qué cantidad). Marque la casilla de cada entrada una vez que haya quitado las piezas, o use la casilla superior para marcarlas todas a la vez. + + + + + assembly.build.required_qty + Cantidad requerida + + + + + assembly.import_bom + Importar piezas para ensamblaje + + + + + assembly.bom.part + Pieza + + + + + assembly.bom.add_entry + Añadir entrada + + + + + assembly.bom.price + Precio + + + + + assembly.build.dont_check_quantity + No verificar cantidades + + + + + assembly.build.dont_check_quantity.help + Si se selecciona esta opción, las cantidades seleccionadas se quitarán del inventario independientemente de si hay más o menos de lo necesario para construir el ensamblaje. + + + + + assembly.build.add_builds_to_builds_part + Añadir unidades construidas a la parte del ensamblaje + + + + + assembly.bom_import.type + Tipo + + + + + assembly.bom_import.type.json + JSON para un ensamblaje + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Eliminar entradas de componentes existentes antes de la importación + + + + + assembly.bom_import.clear_existing_bom.help + Si esta opción está seleccionada, se eliminarán todos los componentes existentes en el ensamblaje y serán reemplazados por los datos de los componentes importados. + + + + + assembly.import_bom.template.header.json + Plantilla de importación JSON para un ensamblaje + + + + + assembly.import_bom.template.header.kicad_pcbnew + Plantilla de importación CSV (KiCAD Pcbnew BOM) para un ensamblaje + + + + + assembly.bom_import.template.entry.name + Nombre del componente en el ensamblaje + + + + + assembly.bom_import.template.entry.part.mpnr + Número de parte único dentro del fabricante + + + + + assembly.bom_import.template.entry.part.ipn + IPN único del componente + + + + + assembly.bom_import.template.entry.part.name + Nombre único del componente + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Nombre único del fabricante + + + + + assembly.bom_import.template.entry.part.category.name + Nombre único de la categoría + + + + + assembly.bom_import.template.json.table + + + + + Campo + Condición + Tipo de dato + Descripción + + + + + quantity + Obligatorio + Número decimal (Float) + Debe estar presente y contener un valor decimal (Float) mayor que 0.0. + + + name + Opcional + Cadena de texto (String) + Si está presente, debe ser una cadena de texto no vacía. + + + part + Opcional + Objeto/Array + + Si se proporciona, debe ser un objeto/array y al menos uno de los campos debe estar completado: +
      +
    • part.id
    • +
    • part.name
    • +
    + + + + part.id + Opcional + Entero (Integer) + Entero (Integer) > 0. Corresponde al ID numérico interno del componente en la base de datos. + + + part.name + Opcional + Cadena de texto (String) + Cadena de texto no vacía, si no se proporciona part.mpnr o part.ipn. + + + part.mpnr + Opcional + Cadena de texto (String) + Cadena de texto no vacía, si no se proporciona part.name o part.ipn. + + + part.ipn + Opcional + Cadena de texto (String) + Cadena de texto no vacía, si no se proporciona part.name o part.mpnr. + + + part.description + Opcional + Cadena de texto (String) o null + Si está presente, debe ser una cadena de texto no vacía o null. + + + part.manufacturer + Opcional + Objeto/Array + + Si está presente, debe ser un objeto/array y al menos uno de los campos debe estar completado: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Opcional + Entero (Integer) + Entero (Integer) > 0. Corresponde al ID numérico interno del fabricante. + + + manufacturer.name + Opcional + Cadena de texto (String) + Cadena de texto no vacía, si no se proporciona manufacturer.id. + + + part.category + Opcional + Objeto/Array + + Si está presente, debe ser un objeto/array y al menos uno de los campos debe estar completado: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Opcional + Entero (Integer) + Entero (Integer) > 0. Corresponde al ID numérico interno de la categoría del componente. + + + category.name + Opcional + Cadena de texto (String) + Cadena de texto no vacía, si no se proporciona category.id. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Columnas esperadas: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Nota: No se realiza una asociación con componentes específicos de la gestión de categorías.

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Campo + Condición + Tipo de Datos + Descripción + + + + + Id + Opcional + Entero + Campo libre. Un número de identificación único para cada componente. + + + Designador + Opcional + Cadena de texto + Campo libre. Un designador de referencia único para el componente en la PCB, p. ej., "R1" para la resistencia 1. Se utiliza para nombrar la colocación en el grupo de componentes. + + + Package + Opcional + Cadena de texto + Campo libre. El formato o tipo de encapsulado del componente, p. ej., "0805" para resistencias SMD. + + + Cantidad + Obligatorio + Entero + El número de componentes idénticos necesarios para crear una instancia única de un ensamblaje. + + + Designación + Obligatorio + Cadena de texto + La descripción o función del componente, p. ej., el valor de la resistencia "10kΩ" o el valor del condensador "100nF". Se utiliza para el nombre en la entrada del BOM. + + + Proveedor y referencia + Opcional + Cadena de texto + Campo libre. Puede incluir, por ejemplo, información específica del distribuidor. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (Componente) + + + + + typeahead.parts.assembly.name + %name% (Ensamblaje) + + + + + projects.build.form.part + Componente "%name%" + + + + + projects.build.form.assembly + Ensamblaje "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% necesario) + + + + + projects.build.form.assembly.bom.entry.no.stock + sin stock + + diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 292dbafaa..59b63a989 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -4703,6 +4703,18 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia Nom + + + part.table.name.value.for_part + %value% (Componente) + + + + + part.table.name.value.for_assembly + %value% (Assemblaggio) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9097,5 +9109,681 @@ exemple de ville Si vous avez des questions à propos de Part-DB , rendez vous sur <a href="%href%" class="link-external" target="_blank">Github</a> + + + project.bom.assembly + Assemblage + + + + + project.bom.partOrAssembly + Sélection + + + + + assembly.edit.status + Statut + + + + + assembly.status.draft + Brouillon + + + + + assembly.status.planning + En planification + + + + + assembly.status.in_production + En production + + + + + assembly.status.finished + Terminé + + + + + assembly.status.archived + Archivé + + + + + project.builds.no_stock + aucun stock indiqué + + + + + project.build.builds_part_lot_label + %name% (%quantity% requis) + + + + + assembly.label + Assemblage + + + + + assembly.caption + Assemblage + + + + + perm.assemblies + Assemblages + + + + + assembly_bom_entry.label + Composants + + + + + assembly.labelp + Assemblages + + + + + assembly.edit + Modifier l'assemblage + + + + + assembly.new + Nouvel assemblage + + + + + assembly.edit.associated_build_part + Composant associé + + + + + assembly.edit.associated_build_part.add + Ajouter un composant + + + + + assembly.edit.associated_build.hint + Ce composant représente les instances fabriquées de l'assemblage. Indiquez si des instances fabriquées sont nécessaires. Sinon, les quantités des composants ne seront appliquées que lors de la construction du projet correspondant. + + + + + assembly.edit.bom.import_bom + Importer des composants + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Assemblages + + + + + assembly.bom_import.flash.success + %count% composant(s) ont été importé(s) avec succès dans l'assemblage. + + + + + assembly.bom_import.flash.invalid_entries + Erreur de validation ! Veuillez vérifier le fichier importé ! + + + + + assembly.bom_import.flash.invalid_file + Le fichier n'a pas pu être importé. Veuillez vérifier que vous avez sélectionné le bon type de fichier. Message d'erreur : %message% + + + + + assembly.bom.quantity + Quantité + + + + + assembly.bom.mountnames + Noms de montage + + + + + assembly.bom.instockAmount + Quantité en stock + + + + + assembly.info.title + Informations sur l'assemblage + + + + + assembly.info.info.label + Informations + + + + + assembly.info.sub_assemblies.label + Sous-ensembles + + + + + assembly.info.builds.label + Constructions + + + + + assembly.info.bom_add_parts + Ajouter des pièces + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Vérifiez bien si vous souhaitez construire l'assemblage avec ce statut !]]> + + + + + assembly.builds.build_not_possible + Construction impossible : pièces insuffisantes disponibles + + + + + assembly.builds.following_bom_entries_miss_instock + Il n'y a pas suffisamment de pièces en stock pour construire ce projet %number_of_builds% fois. Les pièces suivantes manquent en stock : + + + + + assembly.builds.build_possible + Construction possible + + + + + assembly.builds.number_of_builds_possible + %max_builds% unités de cet assemblage.]]> + + + + + assembly.builds.number_of_builds + Nombre d'assemblages à construire + + + + + assembly.build.btn_build + Construire + + + + + assembly.builds.no_stocked_builds + Nombre d'instances construites en stock + + + + + assembly.info.bom_entries_count + Composants + + + + + assembly.info.sub_assemblies_count + Sous-ensembles + + + + + assembly.builds.stocked + en stock + + + + + assembly.builds.needed + nécessaire + + + + + assembly.add_parts_to_assembly + Ajouter des pièces à l'assemblage + + + + + assembly.bom.name + Nom + + + + + assembly.bom.comment + Commentaires + + + + + assembly.builds.following_bom_entries_miss_instock_n + Il n'y a pas suffisamment de pièces en stock pour construire cet assemblage %number_of_builds% fois. Les pièces suivantes manquent en stock : + + + + + assembly.build.help + Sélectionnez les stocks à partir desquels les pièces nécessaires à la construction seront prises (et en quelle quantité). Vérifiez chaque pièce en les retirant, ou utilisez la case supérieure pour les sélectionner toutes à la fois. + + + + + assembly.build.required_qty + Quantité requise + + + + + assembly.import_bom + Importer des pièces pour l'assemblage + + + + + assembly.bom.part + Pièce + + + + + assembly.bom.add_entry + Ajouter une ligne + + + + + assembly.bom.price + Prix + + + + + assembly.build.dont_check_quantity + Ne pas vérifier les quantités + + + + + assembly.build.dont_check_quantity.help + Si cette option est activée, les quantités sélectionnées seront retirées du stock, quelle que soit leur suffisance pour l’assemblage. + + + + + assembly.build.add_builds_to_builds_part + Ajouter les unités construites à la pièce assemblée + + + + + assembly.bom_import.type + Type + + + + + assembly.bom_import.type.json + JSON pour un assemblage + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.clear_existing_bom + Supprimer les entrées de pièces existantes avant l’importation + + + + + assembly.bom_import.clear_existing_bom.help + Si cette option est cochée, toutes les pièces existantes dans l’assemblage seront supprimées et remplacées par les données importées. + + + + + assembly.import_bom.template.header.json + Modèle d’importation JSON pour un assemblage + + + + + assembly.import_bom.template.header.kicad_pcbnew + Modèle d’importation CSV (KiCAD Pcbnew BOM) pour un assemblage + + + + + assembly.bom_import.template.entry.name + Nom de la pièce dans l’assemblage + + + + + assembly.bom_import.template.entry.part.mpnr + Numéro unique de la pièce chez le fabricant + + + + + assembly.bom_import.template.entry.part.ipn + Numéro IPN unique de la pièce + + + + + assembly.bom_import.template.entry.part.name + Nom unique de la pièce + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Nom unique du fabricant + + + + + assembly.bom_import.template.entry.part.category.name + Nom unique de la catégorie + + + + + assembly.bom_import.template.json.table + + + + + Champ + Condition + Type de données + Description + + + + + quantity + Obligatoire + Nombre décimal (Float) + Doit être présent et contenir une valeur décimale (Float) supérieure à 0,0. + + + name + Optionnel + Chaîne (String) + Si présent, doit être une chaîne non vide. + + + part + Optionnel + Objet/Tableau + + Si fourni, doit être un objet/un tableau et au moins un des champs doit être rempli : +
      +
    • part.id
    • +
    • part.name
    • +
    + + + + part.id + Optionnel + Entier (Integer) + Entier (Integer) > 0. Correspond à l'ID numérique interne dans Part-DB du composant. + + + part.name + Optionnel + Chaîne (String) + Chaîne non vide si part.mpnr ou part.ipn ne sont pas fournis. + + + part.mpnr + Optionnel + Chaîne (String) + Chaîne non vide si part.name ou part.ipn ne sont pas fournis. + + + part.ipn + Optionnel + Chaîne (String) + Chaîne non vide si part.name ou part.mpnr ne sont pas fournis. + + + part.description + Optionnel + Chaîne ou null + Si présent, doit être une chaîne non vide ou null. + + + part.manufacturer + Optionnel + Objet/Tableau + + Si présent, doit être un objet/un tableau et au moins un des champs doit être rempli : +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Optionnel + Entier (Integer) + Entier (Integer) > 0. Correspond à l'ID numérique interne du fabricant. + + + manufacturer.name + Optionnel + Chaîne (String) + Chaîne non vide si manufacturer.id n'est pas fourni. + + + part.category + Optionnel + Objet/Tableau + + Si présent, doit être un objet/un tableau et au moins un des champs doit être rempli : +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Optionnel + Entier (Integer) + Entier (Integer) > 0. Correspond à l'ID numérique interne de la catégorie du composant. + + + category.name + Optionnel + Chaîne (String) + Chaîne non vide si category.id n'est pas fourni. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Colonnes attendues : + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Remarque : Aucun mappage n'est effectué avec des composants spécifiques issus de la gestion des catégories.

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Champ + Condition + Type de Données + Description + + + + + Id + Optionnel + Entier + Champ libre. Un numéro d'identification unique pour chaque composant. + + + Designeur + Optionnel + Chaîne + Champ libre. Une référence de désignation unique du composant sur le PCB, par exemple, "R1" pour la résistance 1. Utilisé pour nommer la position au sein du groupe de composants. + + + Boîtier + Optionnel + Chaîne + Champ libre. Le type ou format d'encapsulation du composant, par exemple, "0805" pour des résistances CMS. + + + Quantité + Obligatoire + Entier + Le nombre de composants identiques nécessaires pour créer une instance unique d'un ensemble. + + + Désignation + Obligatoire + Chaîne + La description ou la fonction du composant, par exemple, la valeur de résistance "10kΩ" ou la valeur de condensateur "100nF". Utilisé comme nom dans l'entrée de la nomenclature (BOM). + + + Fournisseur et réf + Optionnel + Chaîne + Champ libre. Peut inclure, par exemple, des informations spécifiques au distributeur. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (pièce) + + + + + typeahead.parts.assembly.name + %name% (assemblage) + + + + + projects.build.form.part + Pièce "%name%" + + + + + projects.build.form.assembly + Assemblage "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% nécessaires) + + + + + projects.build.form.assembly.bom.entry.no.stock + Non disponible en stock + + diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 828304eba..0ea57d9f1 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -4742,6 +4742,18 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi Nome + + + part.table.name.value.for_part + %value% (Componente) + + + + + part.table.name.value.for_assembly + %value% (Assemblaggio) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9806,6 +9818,18 @@ Element 3 Componente + + + project.bom.assembly + Assemblaggio + + + + + project.bom.partOrAssembly + Selezione + + project.bom.add_entry @@ -9884,6 +9908,42 @@ Element 3 Archiviato + + + assembly.edit.status + Stato + + + + + assembly.status.draft + Bozza + + + + + assembly.status.planning + In pianificazione + + + + + assembly.status.in_production + In produzione + + + + + assembly.status.finished + Completato + + + + + assembly.status.archived + Archiviato + + part.new_build_part.error.build_part_already_exists @@ -10160,6 +10220,12 @@ Element 3 a magazzino + + + project.builds.no_stock + nessuna scorta specificata + + project.builds.needed @@ -10232,6 +10298,12 @@ Element 3 Lotto target + + + project.build.builds_part_lot_label + %name% (%quantity% richiesti) + + project.builds.number_of_builds @@ -12346,6 +12418,622 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Visualizza la versione esterna + + + assembly.label + Assemblaggio + + + + + assembly.caption + Assemblaggio + + + + + perm.assemblies + Assemblaggi + + + + + assembly_bom_entry.label + Componenti + + + + + assembly.labelp + Assemblaggi + + + + + assembly.edit + Modifica assemblaggio + + + + + assembly.new + Nuovo assemblaggio + + + + + assembly.edit.associated_build_part + Componente associato + + + + + assembly.edit.associated_build_part.add + Aggiungi componente + + + + + assembly.edit.associated_build.hint + Questo componente rappresenta le istanze fabbricate dell'assemblaggio. Specificare se sono necessarie istanze fabbricate. In caso contrario, le quantità di componenti verranno utilizzate solo durante la costruzione del progetto corrispondente. + + + + + assembly.edit.bom.import_bom + Importa componenti + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Assemblaggi + + + + + assembly.bom_import.flash.success + %count% componente(i) importato(i) correttamente nell'assemblaggio. + + + + + assembly.bom_import.flash.invalid_entries + Errore di convalida! Controlla il file importato! + + + + + assembly.bom_import.flash.invalid_file + Impossibile importare il file. Assicurati di aver selezionato il tipo di file corretto. Messaggio di errore: %message% + + + + + assembly.bom.quantity + Quantità + + + + + assembly.bom.mountnames + Nomi di montaggio + + + + + assembly.bom.instockAmount + Quantità in magazzino + + + + + assembly.info.title + Informazioni sul gruppo + + + + + assembly.info.info.label + Info + + + + + assembly.info.sub_assemblies.label + Sotto-gruppi + + + + + assembly.info.builds.label + Costruzioni + + + + + assembly.info.bom_add_parts + Aggiungi componenti + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Controlla se vuoi davvero costruire il gruppo con questo stato!]]> + + + + + assembly.builds.build_not_possible + Costruzione impossibile: componenti insufficienti disponibili + + + + + assembly.builds.following_bom_entries_miss_instock + Non ci sono abbastanza componenti in magazzino per costruire questo progetto %number_of_builds% volte. Mancano i seguenti componenti: + + + + + assembly.builds.build_possible + Costruzione possibile + + + + + assembly.builds.number_of_builds_possible + %max_builds% unità di questo gruppo.]]> + + + + + assembly.builds.number_of_builds + Numero di gruppi da costruire + + + + + assembly.build.btn_build + Costruire + + + + + assembly.builds.no_stocked_builds + Numero di istanze costruite in magazzino + + + + + assembly.info.bom_entries_count + Componenti + + + + + assembly.info.sub_assemblies_count + Sotto-gruppi + + + + + assembly.builds.stocked + disponibile + + + + + assembly.builds.needed + necessari + + + + + assembly.add_parts_to_assembly + Aggiungi componenti al gruppo + + + + + assembly.bom.name + Nome + + + + + assembly.bom.comment + Commenti + + + + + assembly.builds.following_bom_entries_miss_instock_n + Non ci sono abbastanza componenti in magazzino per costruire questo gruppo %number_of_builds% volte. Mancano i seguenti componenti: + + + + + assembly.build.help + Seleziona i magazzini da cui prelevare i componenti necessari per la costruzione (e in che quantità). Spunta ciascun componente una volta prelevato, oppure utilizza la casella superiore per selezionare tutto in una volta. + + + + + assembly.build.required_qty + Quantità necessaria + + + + + assembly.import_bom + Importa componenti per il gruppo + + + + + assembly.bom.part + Componente + + + + + assembly.bom.add_entry + Aggiungi voce + + + + + assembly.bom.price + Prezzo + + + + + assembly.build.dont_check_quantity + Non controllare le quantità + + + + + assembly.build.dont_check_quantity.help + Se abilitata, le quantità selezionate verranno rimosse dal magazzino indipendentemente dalla loro sufficienza per il gruppo. + + + + + assembly.build.add_builds_to_builds_part + Aggiungi istanze costruite al gruppo componenti + + + + + assembly.bom_import.type + Tipo + + + + + assembly.bom_import.type.json + JSON per un gruppo + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.clear_existing_bom + Elimina i componenti esistenti prima di importare + + + + + assembly.bom_import.clear_existing_bom.help + Se abilitata, tutti i componenti esistenti verranno rimossi e sostituiti dai dati importati. + + + + + assembly.import_bom.template.header.json + Template di importazione JSON per un gruppo + + + + + assembly.import_bom.template.header.kicad_pcbnew + Template di importazione CSV (KiCAD Pcbnew BOM) per un gruppo + + + + + assembly.bom_import.template.entry.name + Nome del componente nel gruppo + + + + + assembly.bom_import.template.entry.part.mpnr + Numero univoco del componente del produttore + + + + + assembly.bom_import.template.entry.part.ipn + IPN univoco del componente + + + + + assembly.bom_import.template.entry.part.name + Nome univoco del componente + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Nome univoco del produttore + + + + + assembly.bom_import.template.entry.part.category.name + Nome univoco della categoria + + + + + assembly.bom_import.template.json.table + + + + + Campo + Condizione + Tipo di dato + Descrizione + + + + + quantity + Obbligatorio + Numero decimale (Float) + Deve essere presente e contenere un valore decimale (Float) maggiore di 0,0. + + + name + Opzionale + Stringa (String) + Se presente, deve essere una stringa non vuota. + + + part + Opzionale + Oggetto/Array + + Se fornito, deve essere un oggetto/un array e almeno uno dei campi deve essere compilato: +
      +
    • part.id
    • +
    • part.name
    • +
    + + + + part.id + Opzionale + Intero (Integer) + Intero (Integer) > 0. Corrisponde all'ID numerico interno di Part-DB per il componente. + + + part.name + Opzionale + Stringa (String) + Stringa non vuota se part.mpnr o part.ipn non sono forniti. + + + part.mpnr + Opzionale + Stringa (String) + Stringa non vuota se part.name o part.ipn non sono forniti. + + + part.ipn + Opzionale + Stringa (String) + Stringa non vuota se part.name o part.mpnr non sono forniti. + + + part.description + Opzionale + Stringa o null + Se presente, deve essere una stringa non vuota o null. + + + part.manufacturer + Opzionale + Oggetto/Array + + Se presente, deve essere un oggetto/un array e almeno uno dei campi deve essere compilato: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Opzionale + Intero (Integer) + Intero (Integer) > 0. Corrisponde all'ID numerico interno del produttore. + + + manufacturer.name + Opzionale + Stringa (String) + Stringa non vuota se manufacturer.id non è fornito. + + + part.category + Opzionale + Oggetto/Array + + Se presente, deve essere un oggetto/un array e almeno uno dei campi deve essere compilato: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Opzionale + Intero (Integer) + Intero (Integer) > 0. Corrisponde all'ID numerico interno della categoria del componente. + + + category.name + Opzionale + Stringa (String) + Stringa non vuota se category.id non è fornito. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Colonne previste: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Nota: Non viene eseguita alcuna mappatura con componenti specifici dalla gestione delle categorie.

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Campo + Condizione + Tipo di Dati + Descrizione + + + + + Id + Opzionale + Intero + Campo libero. Un numero identificativo unico per ogni componente. + + + Designatore + Opzionale + Stringa + Campo libero. Un designatore di riferimento unico per il componente sul PCB, ad esempio, "R1" per il resistore 1. Utilizzato per nominare la posizione nel gruppo di componenti. + + + Package + Opzionale + Stringa + Campo libero. Il tipo o formato del contenitore del componente, ad esempio, "0805" per le resistenze SMD. + + + Quantità + Obbligatorio + Intero + Il numero di componenti identici necessari per creare una singola unità di assemblaggio. + + + Designazione + Obbligatorio + Stringa + La descrizione o la funzione del componente, ad esempio, il valore della resistenza "10kΩ" o il valore del condensatore "100nF". Utilizzato per il nome nell'entrata della lista dei materiali (BOM). + + + Fornitore e riferimento + Opzionale + Stringa + Campo libero. Può includere, ad esempio, informazioni specifiche del distributore. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (componente) + + + + + typeahead.parts.assembly.name + %name% (gruppo) + + + + + projects.build.form.part + Componente "%name%" + + + + + projects.build.form.assembly + Gruppo "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% necessari) + + + + + projects.build.form.assembly.bom.entry.no.stock + Non disponibile in magazzino + + part.table.actions.error diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 4becc319c..5de5f83ef 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -4703,6 +4703,18 @@ 名称 + + + part.table.name.value.for_part + %value%(部品) + + + + + part.table.name.value.for_assembly + %value%(アセンブリ) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -8834,5 +8846,645 @@ Exampletown Part-DBについての質問は、<a href="%href%" class="link-external" target="_blank">GitHub</a> にスレッドがあります。 + + + project.bom.assembly + アセンブリ + + + + + project.bom.partOrAssembly + 選択 + + + + + assembly.edit.status + ステータス + + + + + assembly.status.draft + 下書き + + + + + assembly.status.planning + 計画中 + + + + + assembly.status.in_production + 製作中 + + + + + assembly.status.finished + 完成 + + + + + assembly.status.archived + アーカイブ済み + + + + + project.builds.no_stock + nessuna scorta specificata + + + + + project.build.builds_part_lot_label + %name% (必要数: %quantity%) + + + + + assembly.label + アセンブリ + + + + + assembly.caption + アセンブリ + + + + + perm.assemblies + アセンブリ一覧 + + + + + assembly_bom_entry.label + コンポーネント + + + + + assembly.labelp + アセンブリ一覧 + + + + + assembly.edit + アセンブリを編集 + + + + + assembly.new + 新しいアセンブリ + + + + + assembly.edit.associated_build_part + 関連コンポーネント + + + + + assembly.edit.associated_build_part.add + コンポーネントを追加 + + + + + assembly.edit.associated_build.hint + このコンポーネントは、アセンブリの製造されたインスタンスを表します。製造されたインスタンスが必要な場合は登録してください。それ以外の場合、コンポーネントの数量は該当するプロジェクトを構築する際にのみ使用されます。 + + + + + assembly.edit.bom.import_bom + コンポーネントをインポート + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + アセンブリ一覧 + + + + + assembly.bom_import.flash.success + %count% 個のコンポーネントが正常にアセンブリへインポートされました。 + + + + + assembly.bom_import.flash.invalid_entries + 検証エラー! インポートしたファイルを確認してください! + + + + + assembly.bom_import.flash.invalid_file + ファイルをインポートできませんでした。正しいファイル形式を選択しているか確認してください。エラーメッセージ: %message% + + + + + assembly.bom.quantity + 数量 + + + + + assembly.bom.mountnames + 取り付け名 + + + + + assembly.bom.instockAmount + 在庫数量 + + + + + assembly.info.title + アセンブリ情報 + + + + + assembly.info.info.label + 情報 + + + + + assembly.info.sub_assemblies.label + サブアセンブリ + + + + + assembly.info.builds.label + ビルド + + + + + assembly.info.bom_add_parts + 部品を追加 + + + + + assembly.builds.check_assembly_status + "%assembly_status%"です。この状態でビルドを続行してよろしいですか?]]> + + + + + assembly.builds.build_not_possible + ビルド不可能: 必要な部品が不足しています + + + + + assembly.builds.following_bom_entries_miss_instock + %number_of_builds% 回のビルドを行うのに十分な部品在庫がありません。不足している部品: + + + + + assembly.builds.build_possible + ビルド可能 + + + + + assembly.builds.number_of_builds_possible + %max_builds% 回のアセンブリをビルドできます。]]> + + + + + assembly.builds.number_of_builds + ビルドするアセンブリ数 + + + + + assembly.build.btn_build + ビルド + + + + + assembly.builds.no_stocked_builds + 在庫のビルド済みアセンブリ数 + + + + + assembly.info.bom_entries_count + 部品 + + + + + assembly.info.sub_assemblies_count + サブアセンブリ + + + + + assembly.builds.stocked + 在庫あり + + + + + assembly.builds.needed + 必要数量 + + + + + assembly.add_parts_to_assembly + アセンブリに部品を追加 + + + + + assembly.build.required_qty + 必要な数量 + + + + + assembly.build.yes_button + はい + + + + + assembly.build.no_button + いいえ + + + + + assembly.confirmation.required + + + + + + assembly.build.success + ビルドが正常に完了しました! + + + + + assembly.build.cancelled + ビルドがキャンセルされました。 + + + + + assembly.bom_import.type + タイプ + + + + + assembly.bom_import.type.json + アセンブリ用 JSON + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.clear_existing_bom + インポート前に既存の BOM をクリアする + + + + + assembly.bom_import.clear_existing_bom.help + 有効にすると、既存のすべての BOM エントリが削除され、インポートされたデータに置き換えられます。 + + + + + assembly.import_bom.template.header.json + アセンブリ用 JSON テンプレート + + + + + assembly.import_bom.template.header.kicad_pcbnew + アセンブリ用 CSV テンプレート(KiCAD Pcbnew BOM) + + + + + assembly.bom_import.template.entry.name + アセンブリ内の部品名 + + + + + assembly.bom_import.template.entry.part.mpnr + メーカーの部品番号 + + + + + assembly.bom_import.template.entry.part.ipn + 部品の一意の IPN + + + + + assembly.bom_import.template.entry.part.name + 部品名 + + + + + assembly.bom_import.template.entry.part.manufacturer.name + メーカー名 + + + + + assembly.bom_import.template.entry.part.category.name + カテゴリ名 + + + + + assembly.bom_import.template.json.table + + + + + フィールド + 条件 + データ型 + 説明 + + + + + quantity + 必須 + 浮動小数点数 (Float) + 指定され、0.0より大きい浮動小数点値 (Float) を含む必要があります。 + + + name + 任意 + 文字列 (String) + 指定されている場合、空でない文字列でなければなりません。 + + + part + 任意 + オブジェクト/配列 + + 指定された場合、オブジェクト/配列であり、以下のフィールドのうち少なくとも1つが入力されている必要があります: +
      +
    • part.id
    • +
    • part.name
    • +
    + + + + part.id + 任意 + 整数 (Integer) + 整数 (Integer) > 0。部品の Part-DB 内部数値 ID に対応します。 + + + part.name + 任意 + 文字列 (String) + part.mpnr または part.ipn が指定されていない場合、空でない文字列でなければなりません。 + + + part.mpnr + 任意 + 文字列 (String) + part.name または part.ipn が指定されていない場合、空でない文字列でなければなりません。 + + + part.ipn + 任意 + 文字列 (String) + part.name または part.mpnr が指定されていない場合、空でない文字列でなければなりません。 + + + part.description + 任意 + 文字列または null + 指定されている場合、空でない文字列または null でなければなりません。 + + + part.manufacturer + 任意 + オブジェクト/配列 + + 指定されている場合、オブジェクト/配列であり、以下のフィールドのうち少なくとも1つが入力されている必要があります: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + 任意 + 整数 (Integer) + 整数 (Integer) > 0。製造元の内部数値 ID に対応します。 + + + manufacturer.name + 任意 + 文字列 (String) + manufacturer.id が指定されていない場合、空でない文字列でなければなりません。 + + + part.category + 任意 + オブジェクト/配列 + + 指定されている場合、オブジェクト/配列であり、以下のフィールドのうち少なくとも1つが入力されている必要があります: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + 任意 + 整数 (Integer) + 整数 (Integer) > 0。コンポーネントカテゴリの内部数値 ID に対応します。 + + + category.name + 任意 + 文字列 (String) + category.id が指定されていない場合、空でない文字列でなければなりません。 + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + 予想される列: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + 注意: カテゴリ管理から特定のコンポーネントへのマッピングは行われません。

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + フィールド + 条件 + データタイプ + 説明 + + + + + ID + 任意 + 整数 + 自由形式フィールド。各コンポーネントの一意の識別番号。 + + + デジネータ + 任意 + 文字列 + 自由形式フィールド。PCB上のコンポーネントの一意の参照デジネータ(例: 抵抗1の「R1」)。コンポーネントグループ内の配置の命名に使用されます。 + + + パッケージ + 任意 + 文字列 + 自由形式フィールド。コンポーネントのケースまたはフォームファクタ(例: SMD抵抗「0805」)。 + + + 数量 + 必須 + 整数 + アセンブリの単一インスタンスを作成するために必要な同一コンポーネントの数。 + + + 指定 + 必須 + 文字列 + コンポーネントの説明または機能(例: 抵抗値「10kΩ」やコンデンサ値「100nF」)。部品表(BOM)エントリ内の名前として使用されます。 + + + サプライヤーと参照 + 任意 + 文字列 + 自由形式フィールド。たとえば、特定のディストリビューター情報を含む場合があります。 + + + + ]]> + + + + + + typeahead.parts.part.name + %name%(部品) + + + + + typeahead.parts.assembly.name + %name%(アセンブリ) + + + + + projects.build.form.part + 部品「%name%」 + + + + + projects.build.form.assembly + アセンブリ「%name%」 + + + + + projects.build.form.assembly.bom.entry + %name% (必要数量: %quantity%) + + + + + projects.build.form.assembly.bom.entry.no.stock + 在庫なし + + diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 760533d7c..b7392b4d7 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -724,5 +724,729 @@ Weet u zeker dat u wilt doorgaan? + + + part.table.name.value.for_part + %value% (Onderdeel) + + + + + part.table.name.value.for_assembly + %value% (Samenstelling) + + + + + project.bom.assembly + Assemblage + + + + + project.bom.partOrAssembly + Selectie + + + + + assembly.edit.status + Κατάσταση + + + + + assembly.status.draft + Προσχέδιο + + + + + assembly.status.planning + Υπό σχεδιασμό + + + + + assembly.status.in_production + Σε παραγωγή + + + + + assembly.status.finished + Ολοκληρώθηκε + + + + + assembly.status.archived + Αρχειοθετήθηκε + + + + + project.builds.no_stock + geen voorraad opgegeven + + + + + project.build.builds_part_lot_label + %name% (%quantity% vereist) + + + + + assembly.label + Assemblage + + + + + assembly.caption + Assemblage + + + + + perm.assemblies + Assemblages + + + + + assembly_bom_entry.label + Componenten + + + + + assembly.labelp + Assemblages + + + + + assembly.edit + Assemblage bewerken + + + + + assembly.new + Nieuwe assemblage + + + + + assembly.edit.associated_build_part + Geassocieerd onderdeel + + + + + assembly.edit.associated_build_part.add + Onderdeel toevoegen + + + + + assembly.edit.associated_build.hint + Dit onderdeel vertegenwoordigt de vervaardigde exemplaren van de assemblage. Geef aan of vervaardigde exemplaren nodig zijn. Zo niet, dan worden de aantallen onderdelen alleen gebruikt bij het bouwen van het bijbehorende project. + + + + + assembly.edit.bom.import_bom + Componenten importeren + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Assemblages + + + + + assembly.bom_import.flash.success + %count% component(en) zijn succesvol geïmporteerd in de assemblage. + + + + + assembly.bom_import.flash.invalid_entries + Validatiefout! Controleer het geïmporteerde bestand! + + + + + assembly.bom_import.flash.invalid_file + Het bestand kon niet worden geïmporteerd. Controleer of je het correcte bestandstype hebt geselecteerd. Foutmelding: %message% + + + + + assembly.bom.quantity + Aantal + + + + + assembly.bom.mountnames + Montagenamen + + + + + assembly.bom.instockAmount + Beschikbaar in voorraad + + + + + assembly.info.title + Assemblage-informatie + + + + + assembly.info.info.label + Informatie + + + + + assembly.info.sub_assemblies.label + Subassemblages + + + + + assembly.info.builds.label + Bouw + + + + + assembly.info.bom_add_parts + Onderdelen toevoegen + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Bevestig dat je hiermee wilt doorgaan!]]> + + + + + assembly.builds.build_not_possible + Bouwen is niet mogelijk: niet voldoende onderdelen beschikbaar + + + + + assembly.builds.following_bom_entries_miss_instock + Er zijn niet voldoende onderdelen in voorraad om %number_of_builds% keer te bouwen. De volgende onderdelen ontbreken: + + + + + assembly.builds.build_possible + Bouwen mogelijk + + + + + assembly.builds.number_of_builds_possible + %max_builds% assemblages te bouwen.]]> + + + + + assembly.builds.number_of_builds + Aantal te bouwen assemblages + + + + + assembly.build.btn_build + Bouwen + + + + + assembly.builds.no_stocked_builds + Aantal geassembleerde onderdelen op voorraad + + + + + assembly.info.bom_entries_count + Onderdelen + + + + + assembly.info.sub_assemblies_count + Subgroepen + + + + + assembly.builds.stocked + Op voorraad + + + + + assembly.builds.needed + Nodig + + + + + assembly.add_parts_to_assembly + Onderdelen toevoegen aan assemblage + + + + + assembly.bom.name + Naam + + + + + assembly.bom.comment + Notities + + + + + assembly.builds.following_bom_entries_miss_instock_n + Er zijn niet genoeg onderdelen op voorraad om deze assemblage %number_of_builds% keer te bouwen. Van de volgende onderdelen is er niet genoeg op voorraad: + + + + + assembly.build.help + Selecteer uit welke voorraden de benodigde onderdelen voor de bouw gehaald moeten worden (en in welke hoeveelheid). Vink elk onderdeel afzonderlijk aan als het is verwijderd, of gebruik de bovenste selectievak om alle selectievakjes in één keer aan te vinken. + + + + + assembly.build.required_qty + Benodigde hoeveelheid + + + + + assembly.import_bom + Importeer onderdelen voor assemblage + + + + + assembly.bom.part + Onderdeel + + + + + assembly.bom.add_entry + Voer item in + + + + + assembly.bom.price + Prijs + + + + + assembly.build.dont_check_quantity + Hoeveelheden niet controleren + + + + + assembly.build.dont_check_quantity.help + Als deze optie is geselecteerd, worden de geselecteerde hoeveelheden uit de voorraad verwijderd, ongeacht of er meer of minder onderdelen zijn dan nodig is voor de assemblage. + + + + + assembly.build.add_builds_to_builds_part + Gemaakte instanties toevoegen aan onderdeel van assemblage + + + + + assembly.build.required_qty + Benodigd aantal + + + + + assembly.build.yes_button + Ja + + + + + assembly.build.no_button + Nee + + + + + assembly.confirmation.required + + + + + + assembly.build.success + De assemblage is succesvol gebouwd! + + + + + assembly.build.cancelled + De assemblage is geannuleerd. + + + + + assembly.bom_import.type + Type + + + + + assembly.bom_import.type.json + JSON voor assemblage + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.clear_existing_bom + Bestaande BOM wissen vóór importeren + + + + + assembly.bom_import.clear_existing_bom.help + Wanneer dit is ingeschakeld, worden alle bestaande BOM-items verwijderd en vervangen door de geïmporteerde gegevens. + + + + + assembly.import_bom.template.header.json + JSON-sjabloon voor assemblage + + + + + assembly.import_bom.template.header.kicad_pcbnew + CSV-sjabloon voor assemblage (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.template.entry.name + Naam van onderdeel in de assemblage + + + + + assembly.bom_import.template.entry.part.mpnr + Onderdeelnummer van de fabrikant + + + + + assembly.bom_import.template.entry.part.ipn + Unieke IPN van het onderdeel + + + + + assembly.bom_import.template.entry.part.name + Unieke naam van het onderdeel + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Unieke naam van de fabrikant + + + + + assembly.bom_import.template.entry.part.category.name + Unieke naam van de categorie + + + + + assembly.bom_import.template.json.table + + + + + Veld + Voorwaarde + Gegevenstype + Beschrijving + + + + + quantity + Verplicht veld + Kommagetal (Float) + Moet opgegeven zijn en bevat een kommagetal (Float) dat groter is dan 0,0. + + + name + Optioneel + String + Indien aanwezig, moet het een niet-lege string zijn. + + + part + Optioneel + Object/Array + + Indien opgegeven, moet het een object/array zijn en ten minste één van de velden ingevuld zijn: +
      +
    • part.id
    • +
    • part.name
    • +
    + + + + part.id + Optioneel + Geheel getal (Integer) + Geheel getal (Integer) > 0. Komt overeen met de interne numerieke ID van het onderdeel in de Part-DB. + + + part.name + Optioneel + String + Niet-lege string, indien geen part.mpnr- of part.ipn-vermelding is gegeven. + + + part.mpnr + Optioneel + String + Niet-lege string, indien geen part.name- of part.ipn-vermelding is gegeven. + + + part.ipn + Optioneel + String + Niet-lege string, indien geen part.name- of part.mpnr-vermelding is gegeven. + + + part.description + Optioneel + String of null + Indien aanwezig, moet het een niet-lege string zijn of null. + + + part.manufacturer + Optioneel + Object/Array + + Indien aanwezig, moet het een object/array zijn en ten minste één van de velden ingevuld zijn: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Optioneel + Geheel getal (Integer) + Geheel getal (Integer) > 0. Komt overeen met de interne numerieke ID van de fabrikant. + + + manufacturer.name + Optioneel + String + Niet-lege string, indien geen manufacturer.id-vermelding is gegeven. + + + part.category + Optioneel + Object/Array + + Indien aanwezig, moet het een object/array zijn en ten minste één van de velden ingevuld zijn: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Optioneel + Geheel getal (Integer) + Geheel getal (Integer) > 0. Komt overeen met de interne numerieke ID van de categorie van het onderdeel. + + + category.name + Optioneel + String + Niet-lege string, indien geen category.id-vermelding is gegeven. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Verwachte kolommen: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Opmerking: Er wordt geen mapping uitgevoerd met specifieke componenten uit de categoriebeheer.

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Veld + Voorwaarde + Gegevenstype + Beschrijving + + + + + Id + Optioneel + Integer + Vrij veld. Een unieke identificatienummer voor elk onderdeel. + + + Ontwerper + Optioneel + Tekst + Vrij veld. Een unieke referentie-ontwerper voor het onderdeel op de PCB, bijvoorbeeld "R1" voor weerstand 1. Gebruikt voor de naamgeving van de plaatsing in de componentgroep. + + + Omhulsel + Optioneel + Tekst + Vrij veld. Het type of de vormfactor van het onderdeel, bijvoorbeeld "0805" voor SMD-weerstanden. + + + Aantal + Verplicht + Integer + Het aantal identieke onderdelen dat nodig is om een enkele instantie van een assemblage te maken. + + + Aanduiding + Verplicht + Tekst + De beschrijving of functie van het onderdeel, bijvoorbeeld de weerstandswaarde "10kΩ" of de condensatorwaarde "100nF". Wordt gebruikt als naam in de BOM-invoer. + + + Leverancier en referentie + Optioneel + Tekst + Vrij veld. Kan bijvoorbeeld informatie bevatten die specifiek is voor de distributeur. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (Onderdeel) + + + + + typeahead.parts.assembly.name + %name% (Assemblage) + + + + + projects.build.form.part + Onderdelen "%name%" + + + + + projects.build.form.assembly + Assemblage "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (%quantity% benodigd) + + + + + projects.build.form.assembly.bom.entry.no.stock + niet op voorraad + + diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index b769e2737..7290e5fe1 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -4745,6 +4745,18 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo Nazwa + + + part.table.name.value.for_part + %value%(部品) + + + + + part.table.name.value.for_assembly + %value%(アセンブリ) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9809,6 +9821,18 @@ Element 3 Komponent + + + project.bom.assembly + Zespół + + + + + project.bom.partOrAssembly + Wybór + + project.bom.add_entry @@ -9887,6 +9911,42 @@ Element 3 Zarchiwizowany + + + assembly.edit.status + Status + + + + + assembly.status.draft + Wersja robocza + + + + + assembly.status.planning + W planowaniu + + + + + assembly.status.in_production + W produkcji + + + + + assembly.status.finished + Zakończony + + + + + assembly.status.archived + Zarchiwizowany + + part.new_build_part.error.build_part_already_exists @@ -10163,6 +10223,12 @@ Element 3 dostępny + + + project.builds.no_stock + brak podanego stanu magazynowego + + project.builds.needed @@ -10235,6 +10301,12 @@ Element 3 Partia docelowa + + + project.build.builds_part_lot_label + %name% (%quantity% wymagane) + + project.builds.number_of_builds @@ -12223,5 +12295,621 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Wygenerowany kod + + + assembly.label + Zespół + + + + + assembly.caption + Zespół + + + + + perm.assemblies + Zespoły + + + + + assembly_bom_entry.label + Komponenty + + + + + assembly.labelp + Zespoły + + + + + assembly.edit + Edytuj zespół + + + + + assembly.new + Nowy zespół + + + + + assembly.edit.associated_build_part + Powiązany komponent + + + + + assembly.edit.associated_build_part.add + Dodaj komponent + + + + + assembly.edit.associated_build.hint + Ten komponent reprezentuje wyprodukowane instancje zespołu. Określ, czy są potrzebne wyprodukowane instancje. W przeciwnym razie ilości komponentów zostaną zastosowane tylko podczas budowy odpowiedniego projektu. + + + + + assembly.edit.bom.import_bom + Importuj komponenty + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Zespoły + + + + + assembly.bom_import.flash.success + Pomyślnie zaimportowano %count% komponent(ów) do zespołu. + + + + + assembly.bom_import.flash.invalid_entries + Błąd walidacji! Sprawdź zaimportowany plik! + + + + + assembly.bom_import.flash.invalid_file + Nie udało się zaimportować pliku. Sprawdź, czy wybrano poprawny typ pliku. Komunikat błędu: %message% + + + + + assembly.bom.quantity + Ilość + + + + + assembly.bom.mountnames + Nazwy montażu + + + + + assembly.bom.instockAmount + Ilość na magazynie + + + + + assembly.info.title + Informacje o zespole + + + + + assembly.info.info.label + Informacje + + + + + assembly.info.sub_assemblies.label + Podzespoły + + + + + assembly.info.builds.label + Budowa + + + + + assembly.info.bom_add_parts + Dodaj części + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Upewnij się, że chcesz zbudować zespół w tym statusie!]]> + + + + + assembly.builds.build_not_possible + Budowa niemożliwa: niewystarczająca ilość części + + + + + assembly.builds.following_bom_entries_miss_instock + Brakuje wystarczającej ilości części na magazynie, aby zbudować ten projekt %number_of_builds% razy. Brakujące części to: + + + + + assembly.builds.build_possible + Budowa możliwa + + + + + assembly.builds.number_of_builds_possible + %max_builds% egzemplarzy tego zespołu.]]> + + + + + assembly.builds.number_of_builds + Liczba budowanych egzemplarzy + + + + + assembly.build.btn_build + Zbuduj + + + + + assembly.builds.no_stocked_builds + Liczba zbudowanych i zmagazynowanych egzemplarzy + + + + + assembly.info.bom_entries_count + Elementy + + + + + assembly.info.sub_assemblies_count + Podzespoły + + + + + assembly.builds.stocked + na magazynie + + + + + assembly.builds.needed + potrzebne + + + + + assembly.add_parts_to_assembly + Dodaj części do zespołu + + + + + assembly.bom.name + Nazwa + + + + + assembly.bom.comment + Uwagi + + + + + assembly.builds.following_bom_entries_miss_instock_n + Brakuje wystarczającej ilości części na magazynie, aby zbudować ten zespół %number_of_builds% razy. Brakujące części to: + + + + + assembly.build.help + Wybierz, z których magazynów mają być pobrane części potrzebne do budowy (i w jakiej ilości). Zaznacz każdą pozycję, jeśli części zostały pobrane, lub użyj głównego pola wyboru, aby zaznaczyć wszystkie na raz. + + + + + assembly.build.required_qty + Wymagana ilość + + + + + assembly.import_bom + Importuj części dla zespołu + + + + + assembly.bom.part + Część + + + + + assembly.bom.add_entry + Dodaj pozycję + + + + + assembly.bom.price + Cena + + + + + assembly.build.dont_check_quantity + Nie sprawdzaj ilości + + + + + assembly.build.dont_check_quantity.help + Jeśli opcja jest wybrana, zadeklarowana ilość zostanie odjęta z magazynu, niezależnie od tego, czy jest wystarczająca do budowy zespołu. + + + + + assembly.build.add_builds_to_builds_part + Dodaj zbudowane egzemplarze jako część zespołu + + + + + assembly.bom_import.type + Typ + + + + + assembly.bom_import.type.json + JSON dla zespołu + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.clear_existing_bom + Usuń istniejące dane przed importem + + + + + assembly.bom_import.clear_existing_bom.help + Jeśli wybrano, wszystkie istniejące wpisy części zostaną usunięte i zastąpione danymi z importu. + + + + + assembly.import_bom.template.header.json + Szablon importu JSON dla zespołu + + + + + assembly.import_bom.template.header.kicad_pcbnew + Szablon importu CSV (KiCAD Pcbnew BOM) dla zespołu + + + + + assembly.bom_import.template.entry.name + Nazwa części w zespole + + + + + assembly.bom_import.template.entry.part.mpnr + Unikalny numer katalogowy producenta + + + + + assembly.bom_import.template.entry.part.ipn + Unikalny IPN części + + + + + assembly.bom_import.template.entry.part.name + Unikalna nazwa części + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Unikalna nazwa producenta + + + + + assembly.bom_import.template.entry.part.category.name + Unikalna nazwa kategorii + + + + + assembly.bom_import.template.json.table + + + + + Pole + Warunek + Typ danych + Opis + + + + + quantity + Wymagane + Typ zmiennoprzecinkowy (Float) + Musi być obecne i zawierać wartość zmiennoprzecinkową (Float) większą niż 0,0. + + + name + Opcjonalne + Ciąg znaków (String) + Jeśli obecne, musi być niepustym ciągiem znaków. + + + part + Opcjonalne + Obiekt/Tablica + + Jeśli podane, musi być obiektem/tablicą i co najmniej jedno z poniższych pól musi być wypełnione: +
      +
    • part.id
    • +
    • part.name
    • +
    + + + + part.id + Opcjonalne + Liczba całkowita (Integer) + Liczba całkowita (Integer) > 0. Odpowiada wewnętrznemu numerowi ID komponentu w Part-DB. + + + part.name + Opcjonalne + Ciąg znaków (String) + Niepusty ciąg znaków, jeśli part.mpnr ani part.ipn nie są podane. + + + part.mpnr + Opcjonalne + Ciąg znaków (String) + Niepusty ciąg znaków, jeśli part.name ani part.ipn nie są podane. + + + part.ipn + Opcjonalne + Ciąg znaków (String) + Niepusty ciąg znaków, jeśli part.name ani part.mpnr nie są podane. + + + part.description + Opcjonalne + Ciąg znaków lub null + Jeśli obecne, musi być niepustym ciągiem znaków lub null. + + + part.manufacturer + Opcjonalne + Obiekt/Tablica + + Jeśli obecne, musi być obiektem/tablicą i co najmniej jedno z poniższych pól musi być wypełnione: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Opcjonalne + Liczba całkowita (Integer) + Liczba całkowita (Integer) > 0. Odpowiada wewnętrznemu identyfikatorowi numerowemu producenta. + + + manufacturer.name + Opcjonalne + Ciąg znaków (String) + Niepusty ciąg znaków, jeśli manufacturer.id nie jest podane. + + + part.category + Opcjonalne + Obiekt/Tablica + + Jeśli obecne, musi być obiektem/tablicą i co najmniej jedno z poniższych pól musi być wypełnione: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Opcjonalne + Liczba całkowita (Integer) + Liczba całkowita (Integer) > 0. Odpowiada wewnętrznemu numerowi ID kategorii komponentu. + + + category.name + Opcjonalne + Ciąg znaków (String) + Niepusty ciąg znaków, jeśli category.id nie jest podane. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Oczekiwane kolumny: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Uwaga: Nie wykonano mapowania z określonymi komponentami z zarządzania kategoriami.

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Pole + Warunek + Typ Danych + Opis + + + + + Id + Opcjonalne + Liczba całkowita + Pole dowolne. Unikalny numer identyfikacyjny dla każdego komponentu. + + + Designer + Opcjonalne + Łańcuch znaków + Pole dowolne. Unikalny oznacznik referencyjny komponentu na PCB, np. "R1" dla rezystora 1. Używany do nazewnictwa położenia w grupie komponentów. + + + Obudowa + Opcjonalne + Łańcuch znaków + Pole dowolne. Typ lub forma obudowy komponentu, np. "0805" dla rezystorów SMD. + + + Ilość + Wymagane + Liczba całkowita + Liczba identycznych komponentów potrzebnych do stworzenia jednej instancji złożenia. + + + Oznaczenie + Wymagane + Łańcuch znaków + Opis lub funkcja komponentu, np. wartość rezystora "10kΩ" lub wartość kondensatora "100nF". Używane jako nazwa w pozycji na liście materiałowej (BOM). + + + Dostawca i referencja + Opcjonalne + Łańcuch znaków + Pole dowolne. Może zawierać, np. informacje specyficzne dla dystrybutora. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (część) + + + + + typeahead.parts.assembly.name + %name% (zespół) + + + + + projects.build.form.part + Część "%name%" + + + + + projects.build.form.assembly + Zespół "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (wymagana ilość: %quantity%) + + + + + projects.build.form.assembly.bom.entry.no.stock + brak na magazynie + + diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 62570acb0..540b9e355 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -4751,6 +4751,18 @@ Имя + + + part.table.name.value.for_part + %value% (Часть) + + + + + part.table.name.value.for_assembly + %value% (Сборка) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9813,6 +9825,18 @@ Компонент + + + project.bom.assembly + Сборка + + + + + project.bom.partOrAssembly + Выбор + + project.bom.add_entry @@ -9891,6 +9915,42 @@ Архивный + + + assembly.edit.status + Статус + + + + + assembly.status.draft + Черновик + + + + + assembly.status.planning + Планирование + + + + + assembly.status.in_production + В производстве + + + + + assembly.status.finished + Завершен + + + + + assembly.status.archived + Архивный + + part.new_build_part.error.build_part_already_exists @@ -10167,6 +10227,12 @@ запасено + + + project.builds.no_stock + склад не указан + + project.builds.needed @@ -10239,6 +10305,12 @@ Целевой лот + + + project.build.builds_part_lot_label + %name% (требуется: %quantity%) + + project.builds.number_of_builds @@ -12323,5 +12395,621 @@ Профиль сохранен! + + + assembly.label + Сборка + + + + + assembly.caption + Сборка + + + + + perm.assemblies + Сборки + + + + + assembly_bom_entry.label + Компоненты + + + + + assembly.labelp + Сборки + + + + + assembly.edit + Редактировать сборку + + + + + assembly.new + Новая сборка + + + + + assembly.edit.associated_build_part + Связанный компонент + + + + + assembly.edit.associated_build_part.add + Добавить компонент + + + + + assembly.edit.associated_build.hint + Этот компонент представляет изготовленные экземпляры сборки. Укажите, нужны ли изготовленные экземпляры. В противном случае количество компонентов будет использоваться только при создании соответствующего проекта. + + + + + assembly.edit.bom.import_bom + Импортировать компоненты + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + Сборки + + + + + assembly.bom_import.flash.success + %count% компонент(ов) успешно импортировано в сборку. + + + + + assembly.bom_import.flash.invalid_entries + Ошибка валидации! Проверьте импортированный файл! + + + + + assembly.bom_import.flash.invalid_file + Не удалось импортировать файл. Убедитесь, что выбран правильный тип файла. Сообщение об ошибке: %message% + + + + + assembly.bom.quantity + Количество + + + + + assembly.bom.mountnames + Названия монтажей + + + + + assembly.bom.instockAmount + Количество на складе + + + + + assembly.info.title + Информация о сборке + + + + + assembly.info.info.label + Информация + + + + + assembly.info.sub_assemblies.label + Подсборки + + + + + assembly.info.builds.label + Сборка + + + + + assembly.info.bom_add_parts + Добавить детали + + + + + assembly.builds.check_assembly_status + "%assembly_status%". Убедитесь, что действительно хотите выполнить сборку с этим статусом!]]> + + + + + assembly.builds.build_not_possible + Сборка невозможна: недостаточно деталей + + + + + assembly.builds.following_bom_entries_miss_instock + Недостаточно деталей на складе для сборки %number_of_builds% экземпляров. Следующие детали отсутствуют в достаточном количестве: + + + + + assembly.builds.build_possible + Сборка возможна + + + + + assembly.builds.number_of_builds_possible + %max_builds% экземпляров.]]> + + + + + assembly.builds.number_of_builds + Количество сборок + + + + + assembly.build.btn_build + Собрать + + + + + assembly.builds.no_stocked_builds + Собранные экземпляры на складе + + + + + assembly.info.bom_entries_count + Детали + + + + + assembly.info.sub_assemblies_count + Подсборки + + + + + assembly.builds.stocked + На складе + + + + + assembly.builds.needed + Необходимо + + + + + assembly.add_parts_to_assembly + Добавить детали в сборку + + + + + assembly.bom.name + Название + + + + + assembly.bom.comment + Примечания + + + + + assembly.builds.following_bom_entries_miss_instock_n + Недостаточно деталей на складе для сборки %number_of_builds% экземпляров. У следующих деталей недостаточное количество: + + + + + assembly.build.help + Выберите, из каких запасов брать необходимые для сборки детали (и в каком количестве). Установите галочку для каждой позиции, если детали были взяты, или используйте основную галочку, чтобы отметить все позиции сразу. + + + + + assembly.build.required_qty + Необходимое количество + + + + + assembly.import_bom + Импортировать детали для сборки + + + + + assembly.bom.part + Компонент + + + + + assembly.bom.add_entry + Добавить запись + + + + + assembly.bom.price + Цена + + + + + assembly.build.dont_check_quantity + Не проверять количество + + + + + assembly.build.dont_check_quantity.help + Если выбрано, указанные количества будут списаны со склада независимо от того, достаточно их или нет для указанной сборки. + + + + + assembly.build.add_builds_to_builds_part + Добавить собранные экземпляры как компонент для подсборки + + + + + assembly.bom_import.type + Тип + + + + + assembly.bom_import.type.json + JSON для сборки + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.clear_existing_bom + Очистить текущие данные перед импортом + + + + + assembly.bom_import.clear_existing_bom.help + Если выбрано, все существующие записи о деталях будут удалены и заменены импортированными. + + + + + assembly.import_bom.template.header.json + Шаблон импорта JSON для сборки + + + + + assembly.import_bom.template.header.kicad_pcbnew + Шаблон импорта CSV (KiCAD Pcbnew BOM) для сборки + + + + + assembly.bom_import.template.entry.name + Название детали в сборке + + + + + assembly.bom_import.template.entry.part.mpnr + Уникальный каталожный номер производителя + + + + + assembly.bom_import.template.entry.part.ipn + Уникальный IPN компонента + + + + + assembly.bom_import.template.entry.part.name + Уникальное имя компонента + + + + + assembly.bom_import.template.entry.part.manufacturer.name + Уникальное название производителя + + + + + assembly.bom_import.template.entry.part.category.name + Уникальное название категории + + + + + assembly.bom_import.template.json.table + + + + + Поле + Условие + Тип данных + Описание + + + + + quantity + Обязательное + Дробное число (Float) + Поле должно быть заполнено и содержать дробное значение (Float), большее 0,0. + + + name + Опциональное + Строка (String) + Если присутствует, должно быть непустой строкой. + + + part + Опциональное + Объект/Массив + + Если указано, должно быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: +
      +
    • part.id
    • +
    • part.name
    • +
    + + + + part.id + Опциональное + Целое число (Integer) + Целое число (Integer) > 0. Соответствует внутреннему цифровому идентификатору компонента в Part-DB. + + + part.name + Опциональное + Строка (String) + Непустая строка, если part.mpnr или part.ipn не указаны. + + + part.mpnr + Опциональное + Строка (String) + Непустая строка, если part.name или part.ipn не указаны. + + + part.ipn + Опциональное + Строка (String) + Непустая строка, если part.name или part.mpnr не указаны. + + + part.description + Опциональное + Строка или null + Если присутствует, должно быть непустой строкой или null. + + + part.manufacturer + Опциональное + Объект/Массив + + Если присутствует, должно быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Опциональное + Целое число (Integer) + Целое число (Integer) > 0. Соответствует внутреннему идентификатору производителя. + + + manufacturer.name + Опциональное + Строка (String) + Непустая строка, если manufacturer.id не указано. + + + part.category + Опциональное + Объект/Массив + + Если присутствует, должно быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Опциональное + Целое число (Integer) + Целое число (Integer) > 0. Соответствует внутреннему цифровому идентификатору категории компонента. + + + category.name + Опциональное + Строка (String) + Непустая строка, если category.id не указано. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Ожидаемые столбцы: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Примечание: Сопоставление с конкретными компонентами из управления категориями не выполняется.

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Поле + Условие + Тип данных + Описание + + + + + ID + Опционально + Целое число + Произвольное поле. Уникальный идентификационный номер для каждого компонента. + + + Дизигнатор + Опционально + Строка + Произвольное поле. Уникальный референсный обозначитель компонента на печатной плате, например, "R1" для резистора 1. Используется для именования позиции в группе компонентов. + + + Корпус + Опционально + Строка + Произвольное поле. Тип или форм-фактор корпуса компонента, например, "0805" для SMD-резисторов. + + + Количество + Обязательно + Целое число + Количество одинаковых компонентов, необходимое для создания одной единицы сборки. + + + Обозначение + Обязательно + Строка + Описание или функция компонента, например, номинал резистора "10kΩ" или номинал конденсатора "100nF". Используется в качестве имени в позиции списка материалов (BOM). + + + Поставщик и ссылка + Опционально + Строка + Произвольное поле. Может содержать, например, информацию, специфичную для дистрибьютора. + + + + ]]> + + + + + + typeahead.parts.part.name + %name% (Деталь) + + + + + typeahead.parts.assembly.name + %name% (Сборка) + + + + + projects.build.form.part + Компонент "%name%" + + + + + projects.build.form.assembly + Сборка "%name%" + + + + + projects.build.form.assembly.bom.entry + %name% (необходимо: %quantity%) + + + + + projects.build.form.assembly.bom.entry.no.stock + Нет на складе + + diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 668c32f28..bf924444c 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -4749,6 +4749,18 @@ 名称 + + + part.table.name.value.for_part + %value%(部件) + + + + + part.table.name.value.for_assembly + %value%(组件) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9812,6 +9824,18 @@ Element 3 部件 + + + project.bom.assembly + 装配 + + + + + project.bom.partOrAssembly + 选择 + + project.bom.add_entry @@ -9890,6 +9914,42 @@ Element 3 已存档 + + + assembly.edit.status + 状态 + + + + + assembly.status.draft + 草稿 + + + + + assembly.status.planning + 策划 + + + + + assembly.status.in_production + 生产中 + + + + + assembly.status.finished + 已完成 + + + + + assembly.status.archived + 已归档 + + part.new_build_part.error.build_part_already_exists @@ -10166,6 +10226,12 @@ Element 3 在库 + + + project.builds.no_stock + 未指定库存 + + project.builds.needed @@ -10238,6 +10304,12 @@ Element 3 目标批次 + + + project.build.builds_part_lot_label + %name% (需求数量: %quantity%) + + project.builds.number_of_builds @@ -12208,5 +12280,621 @@ Element 3 成功创建 %COUNT% 个元素。 + + + assembly.label + 装配 + + + + + assembly.caption + 装配 + + + + + perm.assemblies + 装配列表 + + + + + assembly_bom_entry.label + 组件 + + + + + assembly.labelp + 装配列表 + + + + + assembly.edit + 编辑装配 + + + + + assembly.new + 新装配 + + + + + assembly.edit.associated_build_part + 关联组件 + + + + + assembly.edit.associated_build_part.add + 添加组件 + + + + + assembly.edit.associated_build.hint + 此组件表示装配的生产实例。指定是否需要生产实例。如果不需要,则组件数量仅在构建相关项目时使用。 + + + + + assembly.edit.bom.import_bom + 导入组件 + + + + + log.database_updated.failed + __log.database_updated.failed + + + + + log.database_updated.old_version + __log.database_updated.old_version + + + + + log.database_updated.new_version + __log.database_updated.new_version + + + + + tree.tools.edit.assemblies + 装配列表 + + + + + assembly.bom_import.flash.success + 成功导入 %count% 个组件到装配中。 + + + + + assembly.bom_import.flash.invalid_entries + 验证错误!请检查导入的文件! + + + + + assembly.bom_import.flash.invalid_file + 文件导入失败。请确保选择了正确的文件格式。错误信息:%message% + + + + + assembly.bom.quantity + 数量 + + + + + assembly.bom.mountnames + 安装名称 + + + + + assembly.bom.instockAmount + 库存数量 + + + + + assembly.info.title + 装配信息 + + + + + assembly.info.info.label + 信息 + + + + + assembly.info.sub_assemblies.label + 子组件 + + + + + assembly.info.builds.label + 构建 + + + + + assembly.info.bom_add_parts + 添加零件 + + + + + assembly.builds.check_assembly_status + "%assembly_status%"。请确认您是否要在该状态下构建组件!]]> + + + + + assembly.builds.build_not_possible + 无法构建:零件数量不足 + + + + + assembly.builds.following_bom_entries_miss_instock + 库存中缺少足够的零件,无法构建 %number_of_builds% 次。缺少的零件包括: + + + + + assembly.builds.build_possible + 可以构建 + + + + + assembly.builds.number_of_builds_possible + %max_builds% 个该组件。]]> + + + + + assembly.builds.number_of_builds + 构建数量 + + + + + assembly.build.btn_build + 构建 + + + + + assembly.builds.no_stocked_builds + 已构建并库存的数量 + + + + + assembly.info.bom_entries_count + 条目 + + + + + assembly.info.sub_assemblies_count + 子组件 + + + + + assembly.builds.stocked + 库存中 + + + + + assembly.builds.needed + 需要 + + + + + assembly.add_parts_to_assembly + 添加零件到组件 + + + + + assembly.bom.name + 名称 + + + + + assembly.bom.comment + 备注 + + + + + assembly.builds.following_bom_entries_miss_instock_n + 库存不足,无法构建 %number_of_builds% 次。缺少零件包括: + + + + + assembly.build.help + 选择部分库存零件及数量用于构建。每项零件使用复选框,如果零件已提取,也可以使用主复选框来选择所有项目。 + + + + + assembly.build.required_qty + 所需数量 + + + + + assembly.import_bom + 导入组件的零件 + + + + + assembly.bom.part + 零件 + + + + + assembly.bom.add_entry + 添加条目 + + + + + assembly.bom.price + 价格 + + + + + assembly.build.dont_check_quantity + 不检查数量 + + + + + assembly.build.dont_check_quantity.help + 如果选中,即使库存不足,系统也会从库存中扣除声明的数量。 + + + + + assembly.build.add_builds_to_builds_part + 将已构建的零件添加到组件 + + + + + assembly.bom_import.type + 类型 + + + + + assembly.bom_import.type.json + JSON 文件(组件) + + + + + assembly.bom_import.type.kicad_pcbnew + CSV 文件(KiCAD Pcbnew) + + + + + assembly.bom_import.clear_existing_bom + 在导入前清空现有数据 + + + + + assembly.bom_import.clear_existing_bom.help + 如果选中,所有现有零件条目将被删除,新的导入数据将取而代之。 + + + + + assembly.import_bom.template.header.json + 装配 JSON 导入模板 + + + + + assembly.import_bom.template.header.kicad_pcbnew + 装配 CSV 模板(KiCAD Pcbnew BOM) + + + + + assembly.bom_import.template.entry.name + 组件的零件名称 + + + + + assembly.bom_import.template.entry.part.mpnr + 唯一制造商零件编号 + + + + + assembly.bom_import.template.entry.part.ipn + 唯一 IPN 序列号 + + + + + assembly.bom_import.template.entry.part.name + 零件名称 + + + + + assembly.bom_import.template.entry.part.manufacturer.name + 制造商名称 + + + + + assembly.bom_import.template.entry.part.category.name + 类别名称 + + + + + assembly.bom_import.template.json.table + + + + + 字段 + 条件 + 数据类型 + 描述 + + + + + quantity + 必填 + 浮点数 (Float) + 必须存在,并包含大于 0.0 的浮点值 (Float)。 + + + name + 可选 + 字符串 (String) + 如果存在,必须是非空字符串。 + + + part + 可选 + 对象/数组 + + 如果提供,则必须是对象/数组,并且以下字段中至少有一个被填写: +
      +
    • part.id
    • +
    • part.name
    • +
    + + + + part.id + 可选 + 整数 (Integer) + 整数 (Integer) > 0。表示组件在 Part-DB 中的内部数字 ID。 + + + part.name + 可选 + 字符串 (String) + 如果未提供 part.mpnr 或 part.ipn,则必须是非空字符串。 + + + part.mpnr + 可选 + 字符串 (String) + 如果未提供 part.name 或 part.ipn,则必须是非空字符串。 + + + part.ipn + 可选 + 字符串 (String) + 如果未提供 part.name 或 part.mpnr,则必须是非空字符串。 + + + part.description + 可选 + 字符串或 null + 如果存在,必须是非空字符串或 null。 + + + part.manufacturer + 可选 + 对象/数组 + + 如果存在,则必须是对象/数组,并且以下字段中至少有一个被填写: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + 可选 + 整数 (Integer) + 整数 (Integer) > 0。表示制造商的内部数字 ID。 + + + manufacturer.name + 可选 + 字符串 (String) + 如果未提供 manufacturer.id,则必须是非空字符串。 + + + part.category + 可选 + 对象/数组 + + 如果存在,则必须是对象/数组,并且以下字段中至少有一个被填写: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + 可选 + 整数 (Integer) + 整数 (Integer) > 0。表示组件类别的内部数字 ID。 + + + category.name + 可选 + 字符串 (String) + 如果未提供 category.id,则必须是非空字符串。 + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + 预期的列: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + 注意: 未对类别管理中的特定组件进行映射。

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + 字段 + 条件 + 数据类型 + 描述 + + + + + ID + 可选 + 整数 + 自由格式字段。每个组件的唯一标识号。 + + + 设计ator + 可选 + 字符串 + 自由格式字段。PCB上组件的唯一参考标识符,例如电阻1的"R1"。用于命名组件组中的位置。 + + + 封装 + 可选 + 字符串 + 自由格式字段。组件的封装类型或形式因子,例如对于SMD电阻"0805"。 + + + 数量 + 必填 + 整数 + 创建一个组装实例所需的相同组件的数量。 + + + 描述 + 必填 + 字符串 + 组件的描述或功能,例如电阻值"10kΩ"或电容值"100nF"。在物料清单(BOM)条目中用作名称。 + + + 供应商和参考 + 可选 + 字符串 + 自由格式字段。例如,可以包含特定分销商的信息。 + + + + ]]> + + + + + + typeahead.parts.part.name + %name%(零件) + + + + + typeahead.parts.assembly.name + %name%(组件) + + + + + projects.build.form.part + 零件“%name%” + + + + + projects.build.form.assembly + 组件“%name%” + + + + + projects.build.form.assembly.bom.entry + %name%(需数量:%quantity%) + + + + + projects.build.form.assembly.bom.entry.no.stock + 库存不足 + + diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index c298266af..06354533e 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -245,6 +245,12 @@ Musíte vybrat díl pro položku BOM dílu nebo nastavit název pro položku BOM bez dílu. + + + validator.project.bom_entry.only_part_or_assembly_allowed + Je povoleno vybrat pouze jednu součástku nebo sestavu. Upravit prosím svůj výběr! + + project.bom_entry.name_already_in_bom @@ -365,5 +371,23 @@ Neplatný kód. Zkontrolujte, zda je vaše ověřovací aplikace správně nastavena a zda je čas správně nastaven jak na serveru, tak na ověřovacím zařízení. + + + assembly.bom_entry.part_already_in_bom + Tato součást již existuje ve skupině! + + + + + assembly.bom_entry.name_already_in_bom + Již existuje součást s tímto názvem! + + + + + validator.assembly.bom_entry.name_or_part_needed + Musíte vybrat součást nebo nastavit název pro nesoučást! + + diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 21149f0e7..9a9dea4cd 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -245,6 +245,12 @@ Du skal vælge en komponent eller angive et navn til en ikke-komponent styklistepost! + + + validator.project.bom_entry.only_part_or_assembly_allowed + Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! + + project.bom_entry.name_already_in_bom @@ -341,5 +347,23 @@ Denne leverandørstregkodeværdi er allerede brugt til en anden beholdning. Stregkoden skal være unik! + + + assembly.bom_entry.part_already_in_bom + Denne del eksisterer allerede i gruppen! + + + + + assembly.bom_entry.name_already_in_bom + Der findes allerede en del med dette navn! + + + + + validator.assembly.bom_entry.name_or_part_needed + Du skal vælge en del eller sætte et navn for en ikke-del! + + diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 9c123fd85..6fde3419a 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -242,7 +242,13 @@ validator.project.bom_entry.name_or_part_needed - Sie müssen ein Bauteil auswählen, oder einen Namen für ein nicht-Bauteil BOM-Eintrag setzen! + Sie müssen ein Bauteil bzw. eine Baugruppe auswählen, oder einen Namen für ein nicht-Bauteil BOM-Eintrag setzen! + + + + + validator.project.bom_entry.only_part_or_assembly_allowed + Es darf nur ein Bauteil oder eine Baugruppe ausgewählt werden. Bitte passen Sie Ihre Auswahl an! @@ -365,5 +371,23 @@ Ungültiger Code. Überprüfen Sie, dass die Authenticator App korrekt eingerichtet ist und dass der Server und das Gerät beide die korrekte Uhrzeit eingestellt haben. + + + assembly.bom_entry.part_already_in_bom + Dieses Bauteil existiert bereits in der Gruppe! + + + + + assembly.bom_entry.name_already_in_bom + Es gibt bereits einen Bauteil mit diesem Namen! + + + + + validator.assembly.bom_entry.name_or_part_needed + Sie müssen ein Bauteil auswählen, oder einen Namen für ein nicht-Bauteil setzen! + + diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index 9ef5b3de4..ee27863c0 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -7,5 +7,29 @@ Ο εσωτερικός αριθμός εξαρτήματος πρέπει να είναι μοναδικός. {{ value }} χρησιμοποιείται ήδη! + + + validator.project.bom_entry.only_part_or_assembly_allowed + Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! + + + + + assembly.bom_entry.part_already_in_bom + Αυτό το εξάρτημα υπάρχει ήδη στην ομάδα! + + + + + assembly.bom_entry.name_already_in_bom + Υπάρχει ήδη ένα εξάρτημα με αυτό το όνομα! + + + + + validator.assembly.bom_entry.name_or_part_needed + Πρέπει να επιλέξετε ένα εξάρτημα ή να βάλετε ένα όνομα για ένα μη εξάρτημα! + + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 6ad144607..86525b6a8 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -242,7 +242,13 @@ validator.project.bom_entry.name_or_part_needed - You have to choose a part for a part BOM entry or set a name for a non-part BOM entry. + You have to select a part or assembly, or set a name for a non-component Bom entry! + + + + + validator.project.bom_entry.only_part_or_assembly_allowed + Only one part or assembly may be selected. Please modify your selection! @@ -365,5 +371,23 @@ Invalid code. Check that your authenticator app is set up correctly and that both the server and authentication device has the time set correctly. + + + assembly.bom_entry.part_already_in_bom + __assembly.bom_entry.part_already_in_bom + + + + + assembly.bom_entry.name_already_in_bom + __assembly.bom_entry.name_already_in_bom + + + + + validator.assembly.bom_entry.name_or_part_needed + __validator.assembly.bom_entry.name_or_part_needed + + diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index e86ab9ccc..e9bf32597 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -203,5 +203,29 @@ L'emplacement de stockage a été marqué comme "Composant seul", par conséquent aucun nouveau composant ne peut être ajouté. + + + validator.project.bom_entry.only_part_or_assembly_allowed + Seule une pièce ou un assemblage peut être sélectionné. Veuillez ajuster votre sélection! + + + + + assembly.bom_entry.part_already_in_bom + Cette pièce existe déjà dans le groupe! + + + + + assembly.bom_entry.name_already_in_bom + Il existe déjà une pièce avec ce nom! + + + + + validator.assembly.bom_entry.name_or_part_needed + Vous devez sélectionner une pièce ou attribuer un nom pour un non-élément ! + + diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 29e32a16c..9c9c3960c 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -245,6 +245,12 @@ Morate odabrati dio za unos u BOM ili postaviti naziv za unos koji nije dio. + + + validator.project.bom_entry.only_part_or_assembly_allowed + Dozvoljeno je odabrati samo jednu komponentu ili sklop. Molimo prilagodite svoj odabir! + + project.bom_entry.name_already_in_bom @@ -359,5 +365,23 @@ Neispravan kod. Provjerite je li vaša aplikacija za autentifikaciju ispravno postavljena i jesu li poslužitelj i uređaj za autentifikaciju ispravno postavili vrijeme. + + + assembly.bom_entry.part_already_in_bom + Ovaj dio već postoji u grupi! + + + + + assembly.bom_entry.name_already_in_bom + Već postoji dio s tim nazivom! + + + + + validator.assembly.bom_entry.name_or_part_needed + Morate odabrati dio ili unijeti naziv za nedio! + + diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index 7043f4f34..2f747bc5c 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -245,6 +245,12 @@ È necessario selezionare un componente o assegnare un nome ad una voce BOM che non indica un componente! + + + validator.project.bom_entry.only_part_or_assembly_allowed + È consentito selezionare solo una parte o un assieme. Si prega di modificare la selezione! + + project.bom_entry.name_already_in_bom @@ -359,5 +365,23 @@ Codice non valido. Controlla che la tua app di autenticazione sia impostata correttamente e che sia il server che il dispositivo di autenticazione abbiano l'ora impostata correttamente. + + + assembly.bom_entry.part_already_in_bom + Questa parte è già presente nel gruppo! + + + + + assembly.bom_entry.name_already_in_bom + Esiste già una parte con questo nome! + + + + + validator.assembly.bom_entry.name_or_part_needed + È necessario selezionare una parte o inserire un nome per un non-parte! + + diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index 01cc3f77b..0156ffefc 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -203,5 +203,29 @@ 新しい部品を追加できません。保管場所は「1つの部品のみ」とマークされています。 + + + validator.project.bom_entry.only_part_or_assembly_allowed + 部品またはアセンブリのみ選択可能です。選択内容を調整してください! + + + + + assembly.bom_entry.part_already_in_bom + この部品はすでにグループに存在します! + + + + + assembly.bom_entry.name_already_in_bom + この名前の部品はすでに存在します! + + + + + validator.assembly.bom_entry.name_or_part_needed + 部品を選択するか、非部品の名前を入力する必要があります! + + diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 6c9977983..2cc4aef43 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -245,6 +245,12 @@ Należy wybrać część dla wpisu BOM części lub ustawić nazwę dla wpisu BOM niebędącego częścią. + + + validator.project.bom_entry.only_part_or_assembly_allowed + Można wybrać tylko jedną część lub zespół. Proszę dostosować swój wybór! + + project.bom_entry.name_already_in_bom @@ -359,5 +365,23 @@ Nieprawidłowy kod. Sprawdź, czy aplikacja uwierzytelniająca jest poprawnie skonfigurowana i czy zarówno serwer, jak i urządzenie uwierzytelniające mają poprawnie ustawiony czas. + + + assembly.bom_entry.part_already_in_bom + Ten element już istnieje w grupie! + + + + + assembly.bom_entry.name_already_in_bom + Element o tej nazwie już istnieje! + + + + + validator.assembly.bom_entry.name_or_part_needed + Musisz wybrać element lub przypisać nazwę dla elementu niestandardowego! + + diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 0f97c4781..4049b453e 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -245,6 +245,12 @@ Вам необходимо выбрать компонент или задать имя для BOM, не относящейся к компоненту! + + + validator.project.bom_entry.only_part_or_assembly_allowed + Можно выбрать только деталь или сборку. Пожалуйста, измените ваш выбор! + + project.bom_entry.name_already_in_bom @@ -359,5 +365,23 @@ Неверный код. Проверьте, что приложение аутентификации настроено правильно и что на сервере и устройстве аутентификации установлено правильное время. + + + assembly.bom_entry.part_already_in_bom + Эта деталь уже существует в группе! + + + + + assembly.bom_entry.name_already_in_bom + Деталь с таким названием уже существует! + + + + + validator.assembly.bom_entry.name_or_part_needed + Необходимо выбрать деталь или ввести название для недетали! + + diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 08c9f014e..3eab5c4e3 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -245,6 +245,12 @@ 您必须为 BOM 条目选择部件,或为非部件 BOM 条目设置名称。 + + + validator.project.bom_entry.only_part_or_assembly_allowed + 只能选择一个零件或组件。请修改您的选择! + + project.bom_entry.name_already_in_bom @@ -347,5 +353,23 @@ 由于技术限制,在32位系统中无法选择2038年1月19日之后的日期! + + + assembly.bom_entry.part_already_in_bom + 此零件已存在于组中! + + + + + assembly.bom_entry.name_already_in_bom + 具有此名称的零件已存在! + + + + + validator.assembly.bom_entry.name_or_part_needed + 必须选择零件或为非零件指定名称! + + From f6663276fc27c6e64f5298f9a16824f56969d303 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 20 Mar 2025 09:55:48 +0100 Subject: [PATCH 35/83] =?UTF-8?q?Default-Sortierung=20f=C3=BCr=20Assemblie?= =?UTF-8?q?s=20per=20YAML-Konfiguration=20einf=C3=BChren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 9 ++++++ config/parameters.yaml | 4 +++ config/services.yaml | 3 ++ .../AssemblyBomEntriesDataTable.php | 28 +++++++++++-------- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/.env b/.env index 982d4bbd0..cfa3bb053 100644 --- a/.env +++ b/.env @@ -60,6 +60,15 @@ ERROR_PAGE_ADMIN_EMAIL='' # If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them... ERROR_PAGE_SHOW_HELP=1 +################################################################################## +# Part table settings +################################################################################## + +# Configure which columns will be visible by default in the specific table (and in which order). +# This is a comma separated list of column names. See documentation for available values. +TABLE_PARTS_DEFAULT_COLUMNS=name,description,category,footprint,manufacturer,storage_location,amount +TABLE_ASSEMBLIES_DEFAULT_COLUMNS=quantity,manufacturer,name,description,category + ################################################################################### # SAML Single sign on-settings diff --git a/config/parameters.yaml b/config/parameters.yaml index 154fbd8a5..a993d2d5e 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -43,6 +43,10 @@ parameters: ###################################################################################################################### partdb.saml.enabled: '%env(bool:SAML_ENABLED)%' # If this is set to true, SAML authentication is enabled + ###################################################################################################################### + # Table settings + ###################################################################################################################### + partdb.table.assemblies.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_DEFAULT_COLUMNS)%' # The default columns in assembly tables and their order ###################################################################################################################### # Miscellaneous diff --git a/config/services.yaml b/config/services.yaml index 17611ceab..be293d389 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -167,6 +167,9 @@ services: #################################################################################################################### # Table settings #################################################################################################################### + App\DataTables\AssemblyBomEntriesDataTable: + arguments: + $visible_columns: '%partdb.table.assemblies.default_columns%' App\DataTables\Helpers\ColumnSortHelper: shared: false # Service has a state so not share it between different tables diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php index 7149ed5f5..a953179a9 100644 --- a/src/DataTables/AssemblyBomEntriesDataTable.php +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -25,6 +25,7 @@ use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Helpers\ColumnSortHelper; use App\DataTables\Helpers\PartDataTableHelper; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; @@ -41,14 +42,19 @@ class AssemblyBomEntriesDataTable implements DataTableTypeInterface { - public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter) - { + public function __construct( + protected TranslatorInterface $translator, + protected PartDataTableHelper $partDataTableHelper, + protected EntityURLGenerator $entityURLGenerator, + protected AmountFormatter $amountFormatter, + private string $visible_columns, + private ColumnSortHelper $csh + ){ } - public function configure(DataTable $dataTable, array $options): void { - $dataTable + $this->csh //->add('select', SelectColumn::class) ->add('picture', TextColumn::class, [ 'label' => '', @@ -62,7 +68,6 @@ public function configure(DataTable $dataTable, array $options): void ]) ->add('id', TextColumn::class, [ 'label' => $this->translator->trans('part.table.id'), - 'visible' => false, ]) ->add('quantity', TextColumn::class, [ 'label' => $this->translator->trans('assembly.bom.quantity'), @@ -97,11 +102,12 @@ public function configure(DataTable $dataTable, array $options): void ->add('ipn', TextColumn::class, [ 'label' => $this->translator->trans('part.table.ipn'), 'orderField' => 'NATSORT(part.ipn)', - 'visible' => false, 'render' => function ($value, AssemblyBOMEntry $context) { if($context->getPart() instanceof Part) { return $context->getPart()->getIpn(); } + + return ''; } ]) ->add('description', MarkdownColumn::class, [ @@ -142,7 +148,6 @@ public function configure(DataTable $dataTable, array $options): void ]) ->add('instockAmount', TextColumn::class, [ 'label' => 'assembly.bom.instockAmount', - 'visible' => false, 'render' => function ($value, AssemblyBOMEntry $context) { if ($context->getPart() !== null) { return $this->partDataTableHelper->renderAmount($context->getPart()); @@ -153,7 +158,6 @@ public function configure(DataTable $dataTable, array $options): void ]) ->add('storageLocations', TextColumn::class, [ 'label' => 'part.table.storeLocations', - 'visible' => false, 'render' => function ($value, AssemblyBOMEntry $context) { if ($context->getPart() !== null) { return $this->partDataTableHelper->renderStorageLocations($context->getPart()); @@ -164,15 +168,17 @@ public function configure(DataTable $dataTable, array $options): void ]) ->add('addedDate', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.addedDate'), - 'visible' => false, ]) ->add('lastModified', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.lastModified'), - 'visible' => false, ]) ; - $dataTable->addOrderBy('name', DataTable::SORT_ASCENDING); + //Apply the user configured order and visibility and add the columns to the table + $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->visible_columns, + "TABLE_ASSEMBLIES_DEFAULT_COLUMNS"); + + $dataTable->addOrderBy('name'); $dataTable->createAdapter(ORMAdapter::class, [ 'entity' => Attachment::class, From 59a2669a05ba6a89d8e7832beaa4b550551dab6a Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 20 Mar 2025 10:02:30 +0100 Subject: [PATCH 36/83] =?UTF-8?q?configuration.md:=20Info=20f=C3=BCr=20Def?= =?UTF-8?q?ault-Sortierung=20zu=20Assemblies=20einf=C3=BCgen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/configuration.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index d4b217816..efa3efd34 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -137,6 +137,10 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`. +* `TABLE_ASSEMBLIES_DEFAULT_COLUMNS`: The columns in assemblies tables, which are visible by default (when loading table for first + time). + Also specify the default order of the columns. This is a comma separated list of column names. Available columns + are: `name`, `id`, `quantity`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `mountnames`, `instockAmount`, `storageLocations`, `addedDate`, `lastModified`. ### History/Eventlog-related settings From bc0df762d96c934835c1f9591a761d292b61e036 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 2 Apr 2025 12:24:14 +0200 Subject: [PATCH 37/83] Anpassungen aus Benutzersicht, um den Fokus auf die reine Baugruppen-Konfiguration zu legen --- .../AdminPages/BaseAdminController.php | 2 + src/Form/Type/AssemblySelectType.php | 1 - templates/admin/assembly_admin.html.twig | 4 +- templates/admin/base_admin.html.twig | 2 +- templates/assemblies/info/_bom.html.twig | 22 ---- .../assemblies/info/_info_card.html.twig | 15 --- templates/assemblies/info/_part.html.twig | 5 + .../assemblies/info/_subassemblies.html.twig | 28 ----- templates/assemblies/info/info.html.twig | 100 ++++++++++-------- .../form/collection_types_layout.html.twig | 1 + 10 files changed, 66 insertions(+), 114 deletions(-) delete mode 100644 templates/assemblies/info/_bom.html.twig create mode 100644 templates/assemblies/info/_part.html.twig delete mode 100644 templates/assemblies/info/_subassemblies.html.twig diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index edc5917ac..8c8d7520d 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -232,6 +232,7 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit 'timeTravel' => $timeTravel_timestamp, 'repo' => $repo, 'partsContainingElement' => $repo instanceof PartsContainingRepositoryInterface, + 'showParameters' => !($this instanceof AssemblyAdminController), ]); } @@ -382,6 +383,7 @@ protected function _new(Request $request, EntityManagerInterface $em, EntityImpo 'import_form' => $import_form, 'mass_creation_form' => $mass_creation_form, 'route_base' => $this->route_base, + 'showParameters' => !($this instanceof AssemblyAdminController), ]); } diff --git a/src/Form/Type/AssemblySelectType.php b/src/Form/Type/AssemblySelectType.php index ee6cf7c2a..10e858f26 100644 --- a/src/Form/Type/AssemblySelectType.php +++ b/src/Form/Type/AssemblySelectType.php @@ -71,7 +71,6 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setDefaults([ 'class' => Assembly::class, 'choice_label' => 'name', - 'placeholder' => 'None', 'compound' => true, 'error_bubbling' => false, ]); diff --git a/templates/admin/assembly_admin.html.twig b/templates/admin/assembly_admin.html.twig index d8b3ab257..57dde7d15 100644 --- a/templates/admin/assembly_admin.html.twig +++ b/templates/admin/assembly_admin.html.twig @@ -15,7 +15,7 @@ {% endblock %} {% block additional_pills %} - + {% endblock %} {% block quick_links %} @@ -47,7 +47,7 @@ {% endblock %} {% block additional_panes %} -
    +
    {% form_theme form.bom_entries with ['form/collection_types_layout_assembly.html.twig'] %} {{ form_errors(form.bom_entries) }} {{ form_widget(form.bom_entries) }} diff --git a/templates/admin/base_admin.html.twig b/templates/admin/base_admin.html.twig index 51790c3c1..e9fc0fb99 100644 --- a/templates/admin/base_admin.html.twig +++ b/templates/admin/base_admin.html.twig @@ -86,7 +86,7 @@ - {% if entity.parameters is defined %} + {% if entity.parameters is defined and showParameters == true %} diff --git a/templates/assemblies/info/_bom.html.twig b/templates/assemblies/info/_bom.html.twig deleted file mode 100644 index 6a2ca3e03..000000000 --- a/templates/assemblies/info/_bom.html.twig +++ /dev/null @@ -1,22 +0,0 @@ -{% import "components/datatables.macro.html.twig" as datatables %} - - - -{{ datatables.datatable(datatable, 'elements/datatables/datatables', 'assemblies') }} \ No newline at end of file diff --git a/templates/assemblies/info/_info_card.html.twig b/templates/assemblies/info/_info_card.html.twig index 508b2b069..2d0c535b2 100644 --- a/templates/assemblies/info/_info_card.html.twig +++ b/templates/assemblies/info/_info_card.html.twig @@ -37,12 +37,6 @@ {% trans %}entity.info.attachments.tab{% endtrans %} {% endif %} - {% if assembly.parameters is not empty %} - - - {% trans %}entity.info.parameters.tab{% endtrans %} - - {% endif %} {% if assembly.comment is not empty %} @@ -108,15 +102,6 @@
    {% endif %} - {% if assembly.parameters is not empty %} -
    - {% for name, parameters in assembly.groupedParameters %} - {% if name is not empty %}
    {{ name }}
    {% endif %} - {{ helper.parameters_table(assembly) }} - {% endfor %} -
    - {% endif %} - {% if assembly.comment is not empty %}
    diff --git a/templates/assemblies/info/_part.html.twig b/templates/assemblies/info/_part.html.twig new file mode 100644 index 000000000..1fa8b90ed --- /dev/null +++ b/templates/assemblies/info/_part.html.twig @@ -0,0 +1,5 @@ +{% import "components/datatables.macro.html.twig" as datatables %} + +
    + +{{ datatables.datatable(datatable, 'elements/datatables/datatables', 'assemblies') }} \ No newline at end of file diff --git a/templates/assemblies/info/_subassemblies.html.twig b/templates/assemblies/info/_subassemblies.html.twig deleted file mode 100644 index 8c92c5e91..000000000 --- a/templates/assemblies/info/_subassemblies.html.twig +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - {% for subassembly in assembly.children %} - - - - - - - {% endfor %} - -
    {% trans %}name.label{% endtrans %}{% trans %}description.label{% endtrans %}# {% trans %}assembly.info.bom_entries_count{% endtrans %}# {% trans %}assembly.info.sub_assemblies_count{% endtrans %}
    {# Name #} - {{ subassembly.name }} - {# Description #} - {{ subassembly.description | format_markdown }} - - {{ subassembly.bomEntries | length }} - - {{ subassembly.children | length }} -
    \ No newline at end of file diff --git a/templates/assemblies/info/info.html.twig b/templates/assemblies/info/info.html.twig index f5dac1e63..166535a64 100644 --- a/templates/assemblies/info/info.html.twig +++ b/templates/assemblies/info/info.html.twig @@ -5,8 +5,49 @@ {% trans %}assembly.info.title{% endtrans %}: {{ assembly.name }} {% endblock %} -{% block content %} +{% block before_card %} +
    +{% endblock %} + +{% block content %} {{ helper.breadcrumb_entity_link(assembly) }} {{ parent() }} {% endblock %} @@ -23,30 +64,20 @@ {% block card_content %}
    -
    - {% include "assemblies/info/_info.html.twig" %} +
    + {% include "assemblies/info/_part.html.twig" %}
    - {% if assembly.children is not empty %} -
    - {% include "assemblies/info/_subassemblies.html.twig" %} -
    - {% endif %} -
    - {% include "assemblies/info/_bom.html.twig" %} +
    + {% include "assemblies/info/_info.html.twig" %}
    {% include "assemblies/info/_builds.html.twig" %} @@ -94,12 +110,6 @@
    {% include "parts/info/_attachments_info.html.twig" with {"part": assembly} %}
    -
    - {% for name, parameters in assembly.groupedParameters %} - {% if name is not empty %}
    {{ name }}
    {% endif %} - {{ helper.parameters_table(assembly.parameters) }} - {% endfor %} -
    {% endblock %} \ No newline at end of file diff --git a/templates/form/collection_types_layout.html.twig b/templates/form/collection_types_layout.html.twig index def235000..0175aeaf6 100644 --- a/templates/form/collection_types_layout.html.twig +++ b/templates/form/collection_types_layout.html.twig @@ -44,6 +44,7 @@ {{ form_row(form.part) }} {{ form_errors(form.part) }} +
    {{ form_widget(form.assembly) }} {{ form_errors(form.assembly) }} From 61257083b78c29e9bf41cfb987ebd3f45d8d6445 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 3 Apr 2025 12:13:24 +0200 Subject: [PATCH 38/83] =?UTF-8?q?=C3=9Cbersetzung=20zu=20"assembly.bom=5Fi?= =?UTF-8?q?mport.template.kicad=5Fpcbnew.table"=20anpassen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/assemblies/import_bom.html.twig | 2 +- translations/messages.cs.xlf | 56 +++++++++++------------ translations/messages.da.xlf | 44 +++++++++--------- translations/messages.de.xlf | 24 +++++----- translations/messages.el.xlf | 52 ++++++++++----------- translations/messages.en.xlf | 30 ++++++------ translations/messages.es.xlf | 34 +++++++------- translations/messages.fr.xlf | 36 +++++++-------- translations/messages.it.xlf | 34 +++++++------- translations/messages.ja.xlf | 38 +++++++-------- translations/messages.nl.xlf | 46 +++++++++---------- translations/messages.pl.xlf | 42 ++++++++--------- translations/messages.ru.xlf | 44 +++++++++--------- translations/messages.zh.xlf | 38 +++++++-------- 14 files changed, 260 insertions(+), 260 deletions(-) diff --git a/templates/assemblies/import_bom.html.twig b/templates/assemblies/import_bom.html.twig index 53168b438..dc9430422 100644 --- a/templates/assemblies/import_bom.html.twig +++ b/templates/assemblies/import_bom.html.twig @@ -52,7 +52,7 @@
    Id;Designator;Package;Quantity;Designation;Supplier and ref
    {{ 'assembly.bom_import.template.kicad_pcbnew.exptected_columns.note'|trans|raw }} - {{ 'assembly.bom_import.template.json.table'|trans|raw }} + {{ 'assembly.bom_import.template.kicad_pcbnew.table'|trans|raw }}
    diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 8b1f2a620..52edddb41 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -14086,56 +14086,56 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz - + assembly.bom_import.template.kicad_pcbnew.table - - Pole - Podmínka - Datový typ - Popis - + + Pole + Podmínka + Datový typ + Popis + Id - Volitelný - Celé číslo (Integer) - Volný údaj. Jedinečné identifikační číslo pro každou součástku. + Volitelné + Celé číslo + Volné pole. Unikátní identifikační číslo pro každou součástku. Designator - Volitelný - Řetězec (String) - Volný údaj. Jedinečný referenční označovač součástky na desce plošných spojů, např. „R1“ pro rezistor 1. Používá se pro název osazení součástky v rámci skupiny součástek. + Volitelné + Řetězec + Volné pole. Unikátní referenční označení součástky na PCB, např. „R1“ pro rezistor 1.
    Používá se pro název umístění v rámci součástkové skupiny. Package - Volitelný - Řetězec (String) - Volný údaj. Pouzdro nebo tvar součástky, např. „0805“ pro SMD rezistory. + Volitelné + Řetězec + Volné pole. Pouzdro nebo formát součástky, např. „0805“ pro SMD rezistory.
    Není použito pro záznam součástky v rámci sestavy. - Množství - Povinný - Celé číslo (Integer) - Počet identických součástek, které jsou potřeba k vytvoření jedné instance sestavy. + Quantity + Povinné pole + Celé číslo + Počet identických součástek potřebných k vytvoření jedné instance sestavy.
    Použito jako počet záznamu součástky v rámci sestavy. - Určení - Povinný - Řetězec (String) - Popis nebo funkce součástky, např. hodnota rezistoru „10kΩ“ nebo hodnota kondenzátoru „100nF“. Používá se pro název položky v BOM. + Designation + Povinné pole + Řetězec + Popis nebo funkce součástky, např. hodnota rezistoru „10kΩ“ nebo hodnota kondenzátoru „100nF“.
    Použito jako název záznamu součástky v rámci sestavy. - Dodavatel a ref - Volitelný - Řetězec (String) - Volný údaj. Může obsahovat např. specifické údaje distributora. + Supplier and ref + Volitelné + Řetězec + Volné pole. Může obsahovat např. specifické informace o distributorovi.
    Používá se jako poznámka k záznamu součástky v rámci sestavy. diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index f231163a1..2461f8c77 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -12803,56 +12803,56 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
    - + assembly.bom_import.template.kicad_pcbnew.table - - Felt - Betingelse - Datatype - Beskrivelse - + + Felt + Betingelse + Datatype + Beskrivelse + Id - Valgfri - Heltal (Integer) - Fri oplysning. Et unikt identifikationsnummer for hver komponent. + Valgfrit + Heltal + Fri tekst. Et unikt identifikationsnummer for hver komponent. Designator - Valgfri + Valgfrit Streng - Fri oplysning. En unik referencebetegnelse for komponenten på printkortet, f.eks. "R1" for modstand 1. Bruges til navngivning af monteringssæt i komponentgruppen. + Fri tekst. En unik referencebetegnelse for komponenten på PCB'en, f.eks. “R1” for modstand 1.
    Bruges som navnet på placeringen i komponentgruppen. Package - Valgfri + Valgfrit Streng - Fri oplysning. Komponentenheden eller -formatet, f.eks. "0805" for SMD-modstande. + Fri tekst. Komponentens kabinet eller formfaktor, f.eks. “0805” for SMD-modstande.
    Ikke inkluderet som komponentoplysning i samlingen. - Antal + Quantity Påkrævet - Heltal (Integer) - Antallet af identiske komponenter, der kræves for at oprette en enkelt instans af samling. + Heltal + Antallet af identiske komponenter, der kræves for at oprette en enkelt forekomst af samlingen.
    Bruges som antal af komponentoplysning i samlingen. - Betegnelse + Designation Påkrævet Streng - Beskrivelse eller funktion for komponenten, f.eks. modstandsværdi "10kΩ" eller kondensatorværdi "100nF". Bruges til navnet i BOM-posten. + Beskrivelse eller funktion af komponenten, f.eks. modstandsværdi “10kΩ” eller kondensatorværdi “100nF”.
    Bruges som navnet på komponentoplysningen i samlingen. - Leverandør og ref - Valgfri + Supplier and ref + Valgfrit Streng - Fri oplysning. Kan indeholde f.eks. distributørspecifik værdi. + Fri tekst. Kan indeholde, for eksempel, specifikke oplysninger fra en distributør.
    Bruges som en note til komponentoplysningen i samlingen. diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index c32bf46a7..bc0cac33f 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -13446,19 +13446,19 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
    - + assembly.bom_import.template.kicad_pcbnew.table - - Feld - Bedingung - Datentyp - Beschreibung - + + Feld + Bedingung + Datentyp + Beschreibung + @@ -13471,31 +13471,31 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Designator Optional String - Offene Angabe. Ein eindeutiger Referenzbezeichner des Bauteils auf der Leiterplatte, z.B. „R1“ für Widerstand 1. Wird für den Bestückungsnamen des Bauteil-Eintrags innerhalb der Bauteilgruppe verwendet. + Offene Angabe. Ein eindeutiger Referenzbezeichner des Bauteils auf der Leiterplatte, z.B. „R1“ für Widerstand 1.
    Wird in den Bestückungsnamen des Bauteil-Eintrags in der Baugruppe übernommen. Package Optional String - Offene Angabe. Das Gehäuse oder die Bauform des Bauteils, z.B. „0805“ für SMD-Widerstände. + Offene Angabe. Das Gehäuse oder die Bauform des Bauteils, z.B. „0805“ für SMD-Widerstände.
    Wird für ein Bauteil-Eintrag innerhalb der Baugruppe nicht übernommen. Quantity Pflichtfeld Ganzzahl (Integer) - Anzahl der identischen Bauteile, die benötigt werden, um eine Instanz der Baugruppe zu erstellen. + Anzahl der identischen Bauteile, die benötigt werden, um eine Instanz der Baugruppe zu erstellen.
    Wird als Anzahl des Bauteil-Eintrags innerhalb der Baugruppe übernommen. Designation Pflichtfeld String - Beschreibung oder Funktion des Bauteils, z.B. Widerstandswert „10kΩ“ oder Kondensatorwert „100nF“. Wird für den Namen des BOM-Eintrags verwendet. + Beschreibung oder Funktion des Bauteils, z.B. Widerstandswert „10kΩ“ oder Kondensatorwert „100nF“.
    Wird in den Namen des Bauteil-Eintrags innerhalb der Baugruppe übernommen. Supplier and ref Optional String - Offene Angabe. Kann z.B. Distributor spezifischen Wert enthalten. + Offene Angabe. Kann z.B. Distributor spezifischen Wert enthalten.
    Wird als Notiz zum Bauteil-Eintrag innerhalb der Baugruppe übernommen. diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 8fdb801b7..b54131896 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -2130,56 +2130,56 @@
    - + assembly.bom_import.template.kicad_pcbnew.table - - Πεδίο - Εκπλήρωση - Τύπος δεδομένων - Περιγραφή - + + Πεδίο + Προϋπόθεση + Τύπος Δεδομένων + Περιγραφή + Id Προαιρετικό - Ακέραιος αριθμός (Integer) - Ελεύθερη καταχώρηση. Μοναδικός αριθμός ταυτοποίησης για κάθε εξάρτημα. + Ακέραιος + Πεδίο ελεύθερης μορφής. Ένας μοναδικός αριθμός ταυτοποίησης για κάθε εξάρτημα. - Σχεδιαστής + Designator Προαιρετικό - Συμβολοσειρά (String) - Ελεύθερη καταχώρηση. Μοναδικός αναγνωριστικός δείκτης του εξαρτήματος στην πλακέτα κυκλώματος, π.χ. "R1" για την αντίσταση 1. Χρησιμοποιείται για το όνομα του εξαρτήματος στο πλαίσιο της ομάδας εξαρτημάτων. + Συμβολοσειρά + Πεδίο ελεύθερης μορφής. Ένας μοναδικός αναγνωριστικός δείκτης για το εξάρτημα στην πλακέτα PCB, π.χ. "R1" για τον αντιστάτη 1.
    Χρησιμοποιείται για την ονομασία της θέσης στην ομάδα εξαρτημάτων. - Συσκευασία + Package Προαιρετικό - Συμβολοσειρά (String) - Ελεύθερη καταχώρηση. Ο τύπος ή η μορφή του εξαρτήματος, π.χ. "0805" για αντιστάσεις SMD. + Συμβολοσειρά + Πεδίο ελεύθερης μορφής. Η θήκη ή ο μορφολογικός τύπος του εξαρτήματος, π.χ. "0805" για τους SMD αντιστάτες.
    Δεν περιλαμβάνεται στις πληροφορίες εξαρτήματος της συναρμολόγησης. - Ποσότητα - Υποχρεωτικό - Ακέραιος αριθμός (Integer) - Ο αριθμός των πανομοιότυπων εξαρτημάτων που απαιτούνται για τη δημιουργία μίας μονάδας του συνόλου. + Quantity + Απαιτείται + Ακέραιος + Ο αριθμός πανομοιότυπων εξαρτημάτων που απαιτούνται για τη δημιουργία μιας μοναδικής παρουσίας της συναρμολόγησης.
    Χρησιμοποιείται ως ποσότητα στις πληροφορίες εξαρτήματος της συναρμολόγησης. - Ορισμός - Υποχρεωτικό - Συμβολοσειρά (String) - Περιγραφή ή λειτουργία του εξαρτήματος, π.χ. αντίσταση "10kΩ" ή χωρητικότητα "100nF". Χρησιμοποιείται για το όνομα της εγγραφής στο BOM. + Designation + Απαιτείται + Συμβολοσειρά + Η περιγραφή ή η λειτουργία του εξαρτήματος, π.χ. τιμή αντιστάτη "10kΩ" ή τιμή πυκνωτή "100nF".
    Χρησιμοποιείται ως το όνομα στις πληροφορίες εξαρτήματος της συναρμολόγησης. - Προμηθευτής και παραπομπή + Supplier and ref Προαιρετικό - Συμβολοσειρά (String) - Ελεύθερη καταχώρηση. Μπορεί να περιλαμβάνει, π.χ., ειδική τιμή από διανομέα. + Συμβολοσειρά + Πεδίο ελεύθερης μορφής. Μπορεί να περιλαμβάνει, για παράδειγμα, συγκεκριμένες πληροφορίες για τον προμηθευτή.
    Χρησιμοποιείται ως σημείωση για τις πληροφορίες εξαρτήματος της συναρμολόγησης. diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index d267c89ac..8ac704001 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13447,56 +13447,56 @@ Please note, that you can not impersonate a disabled user. If you try you will g
    - + assembly.bom_import.template.kicad_pcbnew.table - - Field - Condition - Data Type - Description - + + Field + Condition + Data Type + Description + Id Optional Integer - Free-form field. A unique identification number for each component. + Free-form field. A unique identification number for each part. Designator Optional String - Free-form field. A unique reference designator of the component on the PCB, e.g., “R1” for resistor 1. Used for naming the placement in the component group. + Free-form field. A unique reference designator of the part on the PCB, e.g., “R1” for resistor 1.
    Used for the placement name of the part entry in the assembly. Package Optional String - Free-form field. The casing or form factor of the component, e.g., “0805” for SMD resistors. + Free-form field. The case or form factor of the part, e.g., “0805” for SMD resistors.
    Not adopted for a part entry within the assembly. Quantity - Required + Required field Integer - The number of identical components required to create a single instance of an assembly. + The number of identical parts needed to create an instance of the assembly.
    Adopted as the quantity of the part entry within the assembly. Designation - Required + Required field String - The description or function of the component, e.g., resistor value “10kΩ” or capacitor value “100nF.” Used for the name in the BOM entry. + Description or function of the part, e.g., resistor value “10kΩ” or capacitor value “100nF.”
    Adopted into the name of the part entry within the assembly. Supplier and ref Optional String - Free-form field. May include, for example, specific distributor information. + Free-form field. May, for example, contain distributor-specific information.
    Adopted as a note for the part entry within the assembly. diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index f961630c5..7315f3760 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -12963,7 +12963,7 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S
    - + assembly.bom_import.template.kicad_pcbnew.table @@ -12982,37 +12982,37 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Id Opcional Entero - Campo libre. Un número de identificación único para cada componente. + Campo libre. Un número único de identificación para cada componente. - Designador + Designator Opcional - Cadena de texto - Campo libre. Un designador de referencia único para el componente en la PCB, p. ej., "R1" para la resistencia 1. Se utiliza para nombrar la colocación en el grupo de componentes. + Cadena + Campo libre. Un designador de referencia único para el componente en la PCB, por ejemplo, "R1" para la resistencia 1.
    Se utiliza como el nombre de ubicación en la entrada de componentes dentro del ensamblaje. Package Opcional - Cadena de texto - Campo libre. El formato o tipo de encapsulado del componente, p. ej., "0805" para resistencias SMD. + Cadena + Campo libre. El encapsulado o formato del componente, por ejemplo, "0805" para resistencias SMD.
    No se incluye en la entrada del componente dentro del ensamblaje. - Cantidad - Obligatorio + Quantity + Requerido Entero - El número de componentes idénticos necesarios para crear una instancia única de un ensamblaje. + El número de componentes idénticos necesarios para crear una instancia del ensamblaje.
    Se usa como la cantidad en la entrada de componentes dentro del ensamblaje. - Designación - Obligatorio - Cadena de texto - La descripción o función del componente, p. ej., el valor de la resistencia "10kΩ" o el valor del condensador "100nF". Se utiliza para el nombre en la entrada del BOM. + Designation + Requerido + Cadena + Descripción o función del componente, por ejemplo, valor de resistencia "10kΩ" o valor de condensador "100nF".
    Se usa como el nombre de la entrada del componente dentro del ensamblaje. - Proveedor y referencia + Supplier and ref Opcional - Cadena de texto - Campo libre. Puede incluir, por ejemplo, información específica del distribuidor. + Cadena + Campo libre. Puede contener, por ejemplo, información específica del distribuidor.
    Se usa como una nota en la entrada del componente dentro del ensamblaje. diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 59b63a989..320121909 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -6959,7 +6959,7 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia company.edit.address.placeholder - Ex. 99 exemple de rue + Ex. 99 exemple de rue exemple de ville
    @@ -9692,19 +9692,19 @@ exemple de ville
    - + assembly.bom_import.template.kicad_pcbnew.table - - Champ - Condition - Type de Données - Description - + + Champ + Condition + Type de Données + Description + @@ -9714,34 +9714,34 @@ exemple de ville Champ libre. Un numéro d'identification unique pour chaque composant. - Designeur + Designator Optionnel Chaîne - Champ libre. Une référence de désignation unique du composant sur le PCB, par exemple, "R1" pour la résistance 1. Utilisé pour nommer la position au sein du groupe de composants. + Champ libre. Une désignation de référence unique pour le composant sur le PCB, par exemple, "R1" pour la résistance 1.
    Utilisé comme le nom de l'emplacement de l'entrée composant dans l'assemblage. - Boîtier + Package Optionnel Chaîne - Champ libre. Le type ou format d'encapsulation du composant, par exemple, "0805" pour des résistances CMS. + Champ libre. Le boîtier ou le format du composant, par exemple, "0805" pour les résistances CMS.
    Non inclus dans l'entrée composant pour l'assemblage. - Quantité + Quantity Obligatoire Entier - Le nombre de composants identiques nécessaires pour créer une instance unique d'un ensemble. + Le nombre de composants identiques nécessaires pour créer une instance de l'assemblage.
    Utilisé comme la quantité dans l'entrée composant de l'assemblage. - Désignation + Designation Obligatoire Chaîne - La description ou la fonction du composant, par exemple, la valeur de résistance "10kΩ" ou la valeur de condensateur "100nF". Utilisé comme nom dans l'entrée de la nomenclature (BOM). + La description ou la fonction du composant, par exemple, valeur de résistance "10kΩ" ou valeur de condensateur "100nF".
    Utilisé comme le nom dans l'entrée composant pour l'assemblage. - Fournisseur et réf + Supplier and ref Optionnel Chaîne - Champ libre. Peut inclure, par exemple, des informations spécifiques au distributeur. + Champ libre. Peut contenir par exemple des informations spécifiques sur le fournisseur.
    Utilisé comme une note dans l'entrée composant pour l'assemblage. diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 0ea57d9f1..f6e3c9446 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -12941,56 +12941,56 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a
    - + assembly.bom_import.template.kicad_pcbnew.table - - Campo - Condizione - Tipo di Dati - Descrizione - + + Campo + Condizione + Tipo di Dati + Descrizione + Id Opzionale Intero - Campo libero. Un numero identificativo unico per ogni componente. + Campo libero. Un numero identificativo univoco per ogni componente. - Designatore + Designator Opzionale Stringa - Campo libero. Un designatore di riferimento unico per il componente sul PCB, ad esempio, "R1" per il resistore 1. Utilizzato per nominare la posizione nel gruppo di componenti. + Campo libero. Un riferimento univoco al componente su PCB, ad esempio "R1" per il resistore 1.
    Utilizzato come nome della posizione nella voce componenti all'interno dell'assemblaggio. Package Opzionale Stringa - Campo libero. Il tipo o formato del contenitore del componente, ad esempio, "0805" per le resistenze SMD. + Campo libero. L'involucro o il fattore di forma del componente, ad esempio "0805" per i resistori SMD.
    Non incluso nelle informazioni del componente nell'assemblaggio. - Quantità + Quantity Obbligatorio Intero - Il numero di componenti identici necessari per creare una singola unità di assemblaggio. + Il numero di componenti identici richiesti per creare un'istanza dell'assemblaggio.
    Utilizzato come quantità nella voce componenti dell'assemblaggio. - Designazione + Designation Obbligatorio Stringa - La descrizione o la funzione del componente, ad esempio, il valore della resistenza "10kΩ" o il valore del condensatore "100nF". Utilizzato per il nome nell'entrata della lista dei materiali (BOM). + Descrizione o funzione del componente, ad esempio valore resistore "10kΩ" o valore condensatore "100nF".
    Utilizzato come nome nella voce componenti dell'assemblaggio. - Fornitore e riferimento + Supplier and ref Opzionale Stringa - Campo libero. Può includere, ad esempio, informazioni specifiche del distributore. + Campo libero. Può contenere, ad esempio, informazioni specifiche del fornitore.
    Utilizzato come nota nelle informazioni del componente nell'assemblaggio. diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 5de5f83ef..a8f5f9151 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -9393,56 +9393,56 @@ Exampletown
    - + assembly.bom_import.template.kicad_pcbnew.table - - フィールド - 条件 - データタイプ - 説明 - + + フィールド + 条件 + データ型 + 説明 + - ID + Id 任意 整数 - 自由形式フィールド。各コンポーネントの一意の識別番号。 + 自由形式のフィールド。各コンポーネントのユニークな識別番号。 - デジネータ + Designator 任意 文字列 - 自由形式フィールド。PCB上のコンポーネントの一意の参照デジネータ(例: 抵抗1の「R1」)。コンポーネントグループ内の配置の命名に使用されます。 + 自由形式のフィールド。PCB上のコンポーネントごとの一意のリファレンス識別子。例: 抵抗 "R1"。
    アセンブリ内の部品エントリの配置名として使用。 - パッケージ + Package 任意 文字列 - 自由形式フィールド。コンポーネントのケースまたはフォームファクタ(例: SMD抵抗「0805」)。 + 自由形式のフィールド。コンポーネントのパッケージまたはフォームファクタ。例: 表面実装抵抗器 "0805"。
    アセンブリ内の部品エントリ情報には含まれません。 - 数量 + Quantity 必須 整数 - アセンブリの単一インスタンスを作成するために必要な同一コンポーネントの数。 + アセンブリの一つのインスタンスを作るために必要な同一部品の数。
    アセンブリの部品情報で数量として使用。 - 指定 + Designation 必須 文字列 - コンポーネントの説明または機能(例: 抵抗値「10kΩ」やコンデンサ値「100nF」)。部品表(BOM)エントリ内の名前として使用されます。 + コンポーネントの説明や機能。例: 抵抗の値 "10kΩ" やコンデンサの値 "100nF"。
    アセンブリの部品情報で名称として使用。 - サプライヤーと参照 + Supplier and ref 任意 文字列 - 自由形式フィールド。たとえば、特定のディストリビューター情報を含む場合があります。 + 自由形式フィールド。例: 供給元に関する特定の情報を含む場合がある。
    アセンブリの部品情報の注記として使用。 diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index b7392b4d7..f98e9c784 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1355,19 +1355,19 @@
    - + assembly.bom_import.template.kicad_pcbnew.table - - Veld - Voorwaarde - Gegevenstype - Beschrijving - + + Veld + Voorwaarde + Gegevenstype + Beschrijving + @@ -1377,34 +1377,34 @@ Vrij veld. Een unieke identificatienummer voor elk onderdeel. - Ontwerper + Designator Optioneel - Tekst - Vrij veld. Een unieke referentie-ontwerper voor het onderdeel op de PCB, bijvoorbeeld "R1" voor weerstand 1. Gebruikt voor de naamgeving van de plaatsing in de componentgroep. + String + Vrij veld. Een unieke referentienaam voor het onderdeel op de PCB, bijvoorbeeld "R1" voor weerstand 1.
    Wordt gebruikt als positioneringsnaam in de onderdelenlijst van de assemblage. - Omhulsel + Package Optioneel - Tekst - Vrij veld. Het type of de vormfactor van het onderdeel, bijvoorbeeld "0805" voor SMD-weerstanden. + String + Vrij veld. De behuizing of vormfactor van het onderdeel, bijvoorbeeld "0805" voor SMD-weerstanden.
    Wordt niet opgenomen in de onderdelenlijst binnen de assemblage. - Aantal - Verplicht + Quantity + Vereist Integer - Het aantal identieke onderdelen dat nodig is om een enkele instantie van een assemblage te maken. + Het aantal identieke onderdelen dat nodig is om een assemblage-instantie te creëren.
    Wordt gebruikt als hoeveelheid in de onderdelenlijst binnen de assemblage. - Aanduiding - Verplicht - Tekst - De beschrijving of functie van het onderdeel, bijvoorbeeld de weerstandswaarde "10kΩ" of de condensatorwaarde "100nF". Wordt gebruikt als naam in de BOM-invoer. + Designation + Vereist + String + De beschrijving of functie van het onderdeel, zoals weerstandwaarde "10kΩ" of condensatorwaarde "100nF".
    Wordt gebruikt als de naam in de onderdelenlijst binnen de assemblage. - Leverancier en referentie + Supplier and ref Optioneel - Tekst - Vrij veld. Kan bijvoorbeeld informatie bevatten die specifiek is voor de distributeur. + String + Vrij veld. Kan bijvoorbeeld specifieke informatie over leveranciers bevatten.
    Wordt gebruikt als notitie in de onderdelenlijst binnen de assemblage. diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 7290e5fe1..d1a095072 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -12818,19 +12818,19 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli
    - + assembly.bom_import.template.kicad_pcbnew.table - - Pole - Warunek - Typ Danych - Opis - + + Pole + Warunek + Typ Danych + Opis + @@ -12840,34 +12840,34 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Pole dowolne. Unikalny numer identyfikacyjny dla każdego komponentu. - Designer + Designator Opcjonalne - Łańcuch znaków - Pole dowolne. Unikalny oznacznik referencyjny komponentu na PCB, np. "R1" dla rezystora 1. Używany do nazewnictwa położenia w grupie komponentów. + Tekst + Pole dowolne. Jednoznaczny znacznik referencyjny komponentu na PCB, np. "R1" dla rezystora 1.
    Używane jako nazwa pozycji w pozycji komponentu w montażu. - Obudowa + Package Opcjonalne - Łańcuch znaków - Pole dowolne. Typ lub forma obudowy komponentu, np. "0805" dla rezystorów SMD. + Tekst + Pole dowolne. Obudowa lub forma komponentu, np. "0805" dla rezystorów SMD.
    Niewykorzystywane w pozycji komponentu w montażu. - Ilość + Quantity Wymagane Liczba całkowita - Liczba identycznych komponentów potrzebnych do stworzenia jednej instancji złożenia. + Liczba identycznych komponentów potrzebna do utworzenia jednej instancji montażu.
    Używane jako ilość w pozycji komponentu w montażu. - Oznaczenie + Designation Wymagane - Łańcuch znaków - Opis lub funkcja komponentu, np. wartość rezystora "10kΩ" lub wartość kondensatora "100nF". Używane jako nazwa w pozycji na liście materiałowej (BOM). + Tekst + Opis lub funkcja komponentu, np. wartość rezystora "10kΩ" lub wartość kondensatora "100nF".
    Używane jako nazwa w pozycji komponentu w montażu. - Dostawca i referencja + Supplier and ref Opcjonalne - Łańcuch znaków - Pole dowolne. Może zawierać, np. informacje specyficzne dla dystrybutora. + Tekst + Pole dowolne. Może zawierać np. specyficzne informacje o dostawcy.
    Używane jako notatka w pozycji komponentu w montażu. diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 540b9e355..429a82eca 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -12918,56 +12918,56 @@
    - + assembly.bom_import.template.kicad_pcbnew.table - - Поле - Условие - Тип данных - Описание - + + Поле + Условие + Тип данных + Описание + - ID + Id Опционально - Целое число - Произвольное поле. Уникальный идентификационный номер для каждого компонента. + Целое + Свободное поле. Уникальный идентификационный номер для каждого компонента. - Дизигнатор - Опционально + Package + Designator Строка - Произвольное поле. Уникальный референсный обозначитель компонента на печатной плате, например, "R1" для резистора 1. Используется для именования позиции в группе компонентов. + Свободное поле. Уникальная ссылочная метка компонента на печатной плате, например, "R1" для резистора 1.
    Используется как наименование позиции в компоненте сборки. - Корпус + Package Опционально Строка - Произвольное поле. Тип или форм-фактор корпуса компонента, например, "0805" для SMD-резисторов. + Свободное поле. Тип корпуса или форм-фактор компонента, например, "0805" для SMD-резисторов.
    Не включается в информацию о компоненте сборки. - Количество + Quantity Обязательно - Целое число - Количество одинаковых компонентов, необходимое для создания одной единицы сборки. + Целое + Количество одинаковых компонентов, необходимых для создания одной версии сборки.
    Используется как количество в информации о компоненте сборки. - Обозначение + Designation Обязательно Строка - Описание или функция компонента, например, номинал резистора "10kΩ" или номинал конденсатора "100nF". Используется в качестве имени в позиции списка материалов (BOM). + Описание или функция компонента, например, значение резистора "10kΩ" или значение конденсатора "100nF".
    Используется как наименование в информации о компоненте сборки. - Поставщик и ссылка + Supplier and ref Опционально Строка - Произвольное поле. Может содержать, например, информацию, специфичную для дистрибьютора. + Свободное поле. Может содержать, например, информацию о конкретных поставщиках.
    Используется как примечание в информации о компоненте сборки. diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index bf924444c..7e90daa28 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -12803,56 +12803,56 @@ Element 3
    - + assembly.bom_import.template.kicad_pcbnew.table - - 字段 - 条件 - 数据类型 - 描述 - + + 字段 + 条件 + 数据类型 + 描述 + - ID + Id 可选 整数 - 自由格式字段。每个组件的唯一标识号。 + 自由字段。每个组件的唯一标识号。 - 设计ator + Designator 可选 字符串 - 自由格式字段。PCB上组件的唯一参考标识符,例如电阻1的"R1"。用于命名组件组中的位置。 + 自由字段。PCB上组件的唯一参考标识符,例如电阻 "R1"。
    用于装配部件条目中的位置名称。 - 封装 + Package 可选 字符串 - 自由格式字段。组件的封装类型或形式因子,例如对于SMD电阻"0805"。 + 自由字段。组件的封装类型或外形规格,例如表面贴装电阻器 "0805"。
    不包含在装配部件条目信息中。 - 数量 + Quantity 必填 整数 - 创建一个组装实例所需的相同组件的数量。 + 创建一个装配实例所需的相同部件的数量。
    用于装配部件条目中的数量。 - 描述 + Designation 必填 字符串 - 组件的描述或功能,例如电阻值"10kΩ"或电容值"100nF"。在物料清单(BOM)条目中用作名称。 + 组件的描述或功能,例如电阻的值 "10kΩ" 或电容的值 "100nF"。
    用于装配部件条目中的名称。 - 供应商和参考 + 供应商及参考 可选 字符串 - 自由格式字段。例如,可以包含特定分销商的信息。 + 自由字段。例如,可以包含有关供应商的特定信息。
    用作装配部件信息中的备注。 From 3ed01f171eb0b0e69a81bcda304a1fc29caac8d0 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Fri, 4 Apr 2025 10:50:47 +0200 Subject: [PATCH 39/83] =?UTF-8?q?Assembly=20Konfiguration=20in=20BOM=20aus?= =?UTF-8?q?blenden,=20wenn=20bisher=20keine=20Zuordnung=20zu=20mindestens?= =?UTF-8?q?=20einem=20Eintrag=20stattgefunden=20hat=20(Ber=C3=BCcksichtigu?= =?UTF-8?q?ng=20Rechtekonfiguration)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Twig/AssemblyTwigExtension.php | 26 +++++++++++++++++++ .../form/collection_types_layout.html.twig | 15 ++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 src/Twig/AssemblyTwigExtension.php diff --git a/src/Twig/AssemblyTwigExtension.php b/src/Twig/AssemblyTwigExtension.php new file mode 100644 index 000000000..3430f7d1d --- /dev/null +++ b/src/Twig/AssemblyTwigExtension.php @@ -0,0 +1,26 @@ +getAssembly() !== null) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/templates/form/collection_types_layout.html.twig b/templates/form/collection_types_layout.html.twig index 0175aeaf6..552fd5421 100644 --- a/templates/form/collection_types_layout.html.twig +++ b/templates/form/collection_types_layout.html.twig @@ -44,9 +44,18 @@ {{ form_row(form.part) }} {{ form_errors(form.part) }} -
    - {{ form_widget(form.assembly) }} - {{ form_errors(form.assembly) }} + + {% if form.vars.value is not null and form.vars.value.project is not null %} + {% set hasAssembly = false %} + {% if is_granted("@assemblies.read") or has_assembly(form.vars.value.project.bomEntries.toArray) %} +
    + {{ form_widget(form.assembly) }} + {{ form_errors(form.assembly) }} + {% endif %} + {% elseif is_granted("@assemblies.read") %} + {{ form_widget(form.assembly) }} + {{ form_errors(form.assembly) }} + {% endif %} {{ form_widget(form.name) }} From 29398e23b66d4126550a645d99cab3420eb3d20d Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Fri, 11 Apr 2025 12:07:55 +0200 Subject: [PATCH 40/83] PartController -> new Methode Variablennamen korrigieren --- src/Controller/PartController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 9e7e61b03..246dbbb22 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -185,7 +185,7 @@ public function new(Request $requ } elseif ($assembly instanceof Assembly) { //Initialize a new part for a build part from the given assembly //Ensure that the assembly has not already a build part - if ($project->getBuildPart() instanceof Part) { + if ($assembly->getBuildPart() instanceof Part) { $this->addFlash('error', 'part.new_build_part.error.build_part_already_exists'); return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]); } From 762fee5d35e80b1a595710d4dfa89d4c2b7c2c8c Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Fri, 11 Apr 2025 14:00:11 +0200 Subject: [PATCH 41/83] =?UTF-8?q?JSON=20Importer=20mit=20Minimaldaten=20we?= =?UTF-8?q?iterentwickeln.=20Validierung=20mit=20Violations=20einf=C3=BChr?= =?UTF-8?q?en=20und=20beim=20Import-Versuch=20zus=C3=A4tzlich=20mit=20ausg?= =?UTF-8?q?eben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Controller/AssemblyController.php | 28 +- .../ImportExportSystem/BOMImporter.php | 315 ++++++++++++++---- .../ImportExportSystem/ImporterResult.php | 60 ++++ templates/assemblies/import_bom.html.twig | 25 +- translations/validators.cs.xlf | 54 +++ translations/validators.da.xlf | 54 +++ translations/validators.de.xlf | 56 +++- translations/validators.el.xlf | 54 +++ translations/validators.en.xlf | 54 +++ translations/validators.fr.xlf | 56 +++- translations/validators.hr.xlf | 54 +++ translations/validators.it.xlf | 54 +++ translations/validators.ja.xlf | 54 +++ translations/validators.pl.xlf | 54 +++ translations/validators.ru.xlf | 54 +++ translations/validators.zh.xlf | 54 +++ 16 files changed, 983 insertions(+), 97 deletions(-) create mode 100644 src/Services/ImportExportSystem/ImporterResult.php diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php index 9710e9bec..54cc1abb0 100644 --- a/src/Controller/AssemblyController.php +++ b/src/Controller/AssemblyController.php @@ -29,7 +29,6 @@ use App\Form\AssemblySystem\AssemblyAddPartsType; use App\Form\AssemblySystem\AssemblyBuildType; use App\Helpers\Assemblies\AssemblyBuildRequest; -use App\Repository\PartRepository; use App\Services\ImportExportSystem\BOMImporter; use App\Services\AssemblySystem\AssemblyBuildHelper; use Doctrine\Common\Collections\ArrayCollection; @@ -52,14 +51,10 @@ #[Route(path: '/assembly')] class AssemblyController extends AbstractController { - private PartRepository $partRepository; - public function __construct( private readonly DataTableFactory $dataTableFactory, - private readonly EntityManagerInterface $entityManager, private readonly TranslatorInterface $translator, ) { - $this->partRepository = $this->entityManager->getRepository(Part::class); } #[Route(path: '/{id}/info', name: 'assembly_info', requirements: ['id' => '\d+'])] @@ -161,15 +156,14 @@ public function importBOM(Request $request, EntityManagerInterface $entityManage $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - - //Clear existing BOM entries if requested + // Clear existing entries if requested if ($form->get('clear_existing_bom')->getData()) { $assembly->getBomEntries()->clear(); $entityManager->flush(); } try { - $entries = $BOMImporter->importFileIntoAssembly($form->get('file')->getData(), $assembly, [ + $importerResult = $BOMImporter->importFileIntoAssembly($form->get('file')->getData(), $assembly, [ 'type' => $form->get('type')->getData(), ]); @@ -177,24 +171,17 @@ public function importBOM(Request $request, EntityManagerInterface $entityManage $errors = $validator->validateProperty($assembly, 'bom_entries'); //If no validation errors occured, save the changes and redirect to edit page - if (count ($errors) === 0) { - foreach ($entries as $entry) { - if ($entry instanceof AssemblyBOMEntry && $entry->getPart() !== null) { - $part = $entry->getPart(); - if ($part->getID() === null) { - $this->partRepository->save($part); - } - } - } + if (count ($errors) === 0 && $importerResult->getViolations()->count() === 0) { + $entries = $importerResult->getBomEntries(); $this->addFlash('success', t('assembly.bom_import.flash.success', ['%count%' => count($entries)])); $entityManager->flush(); + return $this->redirectToRoute('assembly_edit', ['id' => $assembly->getID()]); } - //When we get here, there were validation errors + //Show validation errors $this->addFlash('error', t('assembly.bom_import.flash.invalid_entries')); - } catch (\UnexpectedValueException|\RuntimeException|SyntaxError $e) { $this->addFlash('error', t('assembly.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()])); } @@ -226,7 +213,8 @@ public function importBOM(Request $request, EntityManagerInterface $entityManage 'assembly' => $assembly, 'jsonTemplate' => $jsonTemplate, 'form' => $form, - 'errors' => $errors ?? null, + 'validationErrors' => $errors ?? null, + 'importerErrors' => isset($importerResult) ? $importerResult->getViolations() : null, ]); } diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 47ba90c90..6688780a4 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -39,8 +39,9 @@ use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\OptionsResolver\OptionsResolver; -use RuntimeException; +use Symfony\Contracts\Translation\TranslatorInterface; use UnexpectedValueException; +use Symfony\Component\Validator\ConstraintViolation; /** * @see \App\Tests\Services\ImportExportSystem\BOMImporterTest @@ -57,16 +58,21 @@ class BOMImporter 5 => 'Supplier and ref', ]; - private readonly PartRepository $partRepository; + private string $jsonRoot = ''; - private readonly ManufacturerRepository $manufacturerRepository; + private PartRepository $partRepository; - private readonly CategoryRepository $categoryRepository; + private ManufacturerRepository $manufacturerRepository; - private readonly DBElementRepository $assemblyBOMEntryRepository; + private CategoryRepository $categoryRepository; + + private DBElementRepository $assemblyBOMEntryRepository; + + private TranslatorInterface $translator; public function __construct( private readonly EntityManagerInterface $entityManager, + private readonly TranslatorInterface $translator, private readonly LoggerInterface $logger, private readonly BOMValidationService $validationService ) { @@ -74,6 +80,7 @@ public function __construct( $this->manufacturerRepository = $entityManager->getRepository(Manufacturer::class); $this->categoryRepository = $entityManager->getRepository(Category::class); $this->assemblyBOMEntryRepository = $entityManager->getRepository(AssemblyBOMEntry::class); + $this->translator = $translator; } protected function configureOptions(OptionsResolver $resolver): OptionsResolver @@ -110,20 +117,21 @@ public function importFileIntoProject(File $file, Project $project, array $optio } /** - * Converts the given file into an array of BOM entries using the given options and save them into the given assembly. + * Converts the given file into an ImporterResult with an array of BOM entries using the given options and save them into the given assembly. * The changes are not saved into the database yet. - * @return AssemblyBOMEntry[] */ - public function importFileIntoAssembly(File $file, Assembly $assembly, array $options): array + public function importFileIntoAssembly(File $file, Assembly $assembly, array $options): ImporterResult { - $bomEntries = $this->fileToBOMEntries($file, $options, AssemblyBOMEntry::class); + $importerResult = $this->fileToImporterResult($file, $options, AssemblyBOMEntry::class); - //Assign the bom_entries to the assembly - foreach ($bomEntries as $bom_entry) { - $assembly->addBomEntry($bom_entry); + if ($importerResult->getViolations()->count() === 0) { + //Assign the bom_entries to the assembly + foreach ($importerResult->getBomEntries() as $bomEntry) { + $assembly->addBomEntry($bomEntry); + } } - return $bomEntries; + return $importerResult; } /** @@ -152,6 +160,14 @@ public function validateBOMData(string $data, array $options): array }; } + /** + * Converts the given file into an ImporterResult with an array of BOM entries using the given options. + */ + public function fileToImporterResult(File $file, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult + { + return $this->stringToImporterResult($file->getContent(), $options, $objectType); + } + /** * Import string data into an array of BOM entries, which are not yet assigned to a project. * @param string $data The data to import @@ -164,6 +180,24 @@ public function stringToBOMEntries(string $data, array $options, string $objectT $resolver = $this->configureOptions($resolver); $options = $resolver->resolve($options); + return match ($options['type']) { + 'kicad_pcbnew' => $this->parseKiCADPCB($data, $options, $objectType)->getBomEntries(), + default => throw new InvalidArgumentException('Invalid import type!'), + }; + } + + /** + * Import string data into an array of BOM entries, which are not yet assigned to a project. + * @param string $data The data to import + * @param array $options An array of options + * @return ProjectBOMEntry[]|AssemblyBOMEntry[] An array of imported entries + */ + public function stringToImporterResult(string $data, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult + { + $resolver = new OptionsResolver(); + $resolver = $this->configureOptions($resolver); + $options = $resolver->resolve($options); + return match ($options['type']) { 'kicad_pcbnew' => $this->parseKiCADPCB($data, $objectType), 'json' => $this->parseJson($data, $options, $objectType), @@ -171,14 +205,14 @@ public function stringToBOMEntries(string $data, array $options, string $objectT }; } - private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntry::class): array + private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntry::class): ImporterResult { + $result = new ImporterResult(); + $csv = Reader::createFromString($data); $csv->setDelimiter(';'); $csv->setHeaderOffset(0); - $bom_entries = []; - foreach ($csv->getRecords() as $offset => $entry) { //Translate the german field names to english $entry = $this->normalizeColumnNames($entry); @@ -208,10 +242,10 @@ private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntr $bom_entry->setComment($entry['Supplier and ref'] ?? ''); $bom_entry->setQuantity((float) ($entry['Quantity'] ?? 1)); - $bom_entries[] = $bom_entry; + $result->addBomEntry($bom_entry); } - return $bom_entries; + return $result; } /** @@ -271,30 +305,47 @@ private function validateKiCADSchematicData(string $data, array $options): array return $this->validationService->validateBOMEntries($mapped_entries, $options); } - private function parseJson(string $data, array $options = [], string $objectType = ProjectBOMEntry::class): array + private function parseJson(string $data, array $options = [], string $objectType = ProjectBOMEntry::class): ImporterResult { - $result = []; + $result = new ImporterResult(); + $this->jsonRoot = 'JSON Import for '.$objectType === ProjectBOMEntry::class ? 'Project' : 'Assembly'; $data = json_decode($data, true); - foreach ($data as $entry) { + foreach ($data as $key => $entry) { // Check quantity if (!isset($entry['quantity'])) { - throw new UnexpectedValueException('quantity missing'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.quantity.required', + "entry[$key].quantity" + )); } - if (!is_float($entry['quantity']) || $entry['quantity'] <= 0) { - throw new UnexpectedValueException('quantity expected as float greater than 0.0'); + + if (isset($entry['quantity']) && (!is_float($entry['quantity']) || $entry['quantity'] <= 0)) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.quantity.float', + "entry[$key].quantity", + $entry['quantity'] + )); } // Check name if (isset($entry['name']) && !is_string($entry['name'])) { - throw new UnexpectedValueException('name of part list entry expected as string'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.string.notEmpty', + "entry[$key].name", + $entry['name'] + )); } // Check if part is assigned with relevant information if (isset($entry['part'])) { if (!is_array($entry['part'])) { - throw new UnexpectedValueException('The property "part" should be an array'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.array', + "entry[$key].part", + $entry['part'] + )); } $partIdValid = isset($entry['part']['id']) && is_int($entry['part']['id']) && $entry['part']['id'] > 0; @@ -303,9 +354,12 @@ private function parseJson(string $data, array $options = [], string $objectType $partIpnValid = isset($entry['part']['ipn']) && is_string($entry['part']['ipn']) && trim($entry['part']['ipn']) !== ''; if (!$partIdValid && !$partNameValid && !$partMpnrValid && !$partIpnValid) { - throw new UnexpectedValueException( - 'The property "part" must have either assigned: "id" as integer greater than 0, "name", "mpnr", or "ipn" as non-empty string' - ); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.subproperties', + "entry[$key].part", + $entry['part'], + ['%propertyString%' => '"id", "name", "mpnr", or "ipn"'] + )); } $part = $partIdValid ? $this->partRepository->findOneBy(['id' => $entry['part']['id']]) : null; @@ -314,28 +368,71 @@ private function parseJson(string $data, array $options = [], string $objectType $part = $part ?? ($partNameValid ? $this->partRepository->findOneBy(['name' => trim($entry['part']['name'])]) : null); if ($part === null) { - $part = new Part(); - $part->setName($entry['part']['name']); + $value = sprintf('part.id: %s, part.mpnr: %s, part.ipn: %s, part.name: %s', + isset($entry['part']['id']) ? '' . $entry['part']['id'] . '' : '-', + isset($entry['part']['mpnr']) ? '' . $entry['part']['mpnr'] . '' : '-', + isset($entry['part']['ipn']) ? '' . $entry['part']['ipn'] . '' : '-', + isset($entry['part']['name']) ? '' . $entry['part']['name'] . '' : '-', + ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.notFoundFor', + "entry[$key].part", + $entry['part'], + ['%value%' => $value] + )); } - if ($partNameValid && $part->getName() !== trim($entry['part']['name'])) { - throw new RuntimeException(sprintf('Part name does not match exact the given name. Given for import: %s, found part: %s', $entry['part']['name'], $part->getName())); + if ($partNameValid && $part !== null && isset($entry['part']['name']) && $part->getName() !== trim($entry['part']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.name", + $entry['part']['name'], + [ + '%importValue%' => '' . $entry['part']['name'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getName() . '' + ] + )); } - if ($partIpnValid && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) { - throw new RuntimeException(sprintf('Part mpnr does not match exact the given mpnr. Given for import: %s, found part: %s', $entry['part']['mpnr'], $part->getManufacturerProductNumber())); + if ($partMpnrValid && $part !== null && isset($entry['part']['mpnr']) && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.mpnr", + $entry['part']['mpnr'], + [ + '%importValue%' => '' . $entry['part']['mpnr'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getManufacturerProductNumber() . '' + ] + )); } - if ($partIpnValid && $part->getIpn() !== trim($entry['part']['ipn'])) { - throw new RuntimeException(sprintf('Part ipn does not match exact the given ipn. Given for import: %s, found part: %s', $entry['part']['ipn'], $part->getIpn())); + if ($partIpnValid && $part !== null && isset($entry['part']['ipn']) && $part->getIpn() !== trim($entry['part']['ipn'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.ipn", + $entry['part']['ipn'], + [ + '%importValue%' => '' . $entry['part']['ipn'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getIpn() . '' + ] + )); } // Part: Description check - if (isset($entry['part']['description']) && !is_null($entry['part']['description'])) { + if (isset($entry['part']['description'])) { if (!is_string($entry['part']['description']) || trim($entry['part']['description']) === '') { - throw new UnexpectedValueException('The property path "part.description" must be a non-empty string if not null'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.string.notEmpty', + 'entry[$key].part.description', + $entry['part']['description'] + )); } } + $partDescription = $entry['part']['description'] ?? ''; // Part: Manufacturer check @@ -343,7 +440,11 @@ private function parseJson(string $data, array $options = [], string $objectType $manufacturerNameValid = false; if (array_key_exists('manufacturer', $entry['part'])) { if (!is_array($entry['part']['manufacturer'])) { - throw new UnexpectedValueException('The property path "part.manufacturer" must be an array'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.array', + 'entry[$key].part.manufacturer', + $entry['part']['manufacturer']) ?? null + ); } $manufacturerIdValid = isset($entry['part']['manufacturer']['id']) && is_int($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] > 0; @@ -351,23 +452,43 @@ private function parseJson(string $data, array $options = [], string $objectType // Stellen sicher, dass mindestens eine Bedingung für manufacturer erfüllt sein muss if (!$manufacturerIdValid && !$manufacturerNameValid) { - throw new UnexpectedValueException( - 'The property "manufacturer" must have either assigned: "id" as integer greater than 0, or "name" as non-empty string' - ); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties', + "entry[$key].part.manufacturer", + $entry['part']['manufacturer'], + )); } } $manufacturer = $manufacturerIdValid ? $this->manufacturerRepository->findOneBy(['id' => $entry['part']['manufacturer']['id']]) : null; $manufacturer = $manufacturer ?? ($manufacturerNameValid ? $this->manufacturerRepository->findOneBy(['name' => trim($entry['part']['manufacturer']['name'])]) : null); - if ($manufacturer === null) { - throw new RuntimeException( - 'Manufacturer not found' + if (($manufacturerIdValid || $manufacturerNameValid) && $manufacturer === null) { + $value = sprintf( + 'manufacturer.id: %s, manufacturer.name: %s', + isset($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] !== null ? '' . $entry['part']['manufacturer']['id'] . '' : '-', + isset($entry['part']['manufacturer']['name']) && $entry['part']['manufacturer']['name'] !== null ? '' . $entry['part']['manufacturer']['name'] . '' : '-' ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.notFoundFor', + "entry[$key].part.manufacturer", + $entry['part']['manufacturer'], + ['%value%' => $value] + )); } - if ($manufacturerNameValid && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) { - throw new RuntimeException(sprintf('Manufacturer name does not match exact the given name. Given for import: %s, found manufacturer: %s', $entry['manufacturer']['name'], $manufacturer->getName())); + if ($manufacturerNameValid && $manufacturer !== null && isset($entry['part']['manufacturer']['name']) && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.manufacturer.name", + $entry['part']['manufacturer']['name'], + [ + '%importValue%' => '' . $entry['part']['manufacturer']['name'] . '', + '%foundId%' => $manufacturer->getID(), + '%foundValue%' => '' . $manufacturer->getName() . '' + ] + )); } // Part: Category check @@ -375,49 +496,82 @@ private function parseJson(string $data, array $options = [], string $objectType $categoryNameValid = false; if (array_key_exists('category', $entry['part'])) { if (!is_array($entry['part']['category'])) { - throw new UnexpectedValueException('part.category must be an array'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.array', + 'entry[$key].part.category', + $entry['part']['category']) ?? null + ); } $categoryIdValid = isset($entry['part']['category']['id']) && is_int($entry['part']['category']['id']) && $entry['part']['category']['id'] > 0; $categoryNameValid = isset($entry['part']['category']['name']) && is_string($entry['part']['category']['name']) && trim($entry['part']['category']['name']) !== ''; if (!$categoryIdValid && !$categoryNameValid) { - throw new UnexpectedValueException( - 'The property "category" must have either assigned: "id" as integer greater than 0, or "name" as non-empty string' - ); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties', + "entry[$key].part.category", + $entry['part']['category'] + )); } } $category = $categoryIdValid ? $this->categoryRepository->findOneBy(['id' => $entry['part']['category']['id']]) : null; $category = $category ?? ($categoryNameValid ? $this->categoryRepository->findOneBy(['name' => trim($entry['part']['category']['name'])]) : null); - if ($category === null) { - throw new RuntimeException( - 'Category not found' + if (($categoryIdValid || $categoryNameValid) && $category === null) { + $value = sprintf( + 'category.id: %s, category.name: %s', + isset($entry['part']['category']['id']) && $entry['part']['category']['id'] !== null ? '' . $entry['part']['category']['id'] . '' : '-', + isset($entry['part']['category']['name']) && $entry['part']['category']['name'] !== null ? '' . $entry['part']['category']['name'] . '' : '-' ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.notFoundFor', + "entry[$key].part.category", + $entry['part']['category'], + ['%value%' => $value] + )); } - if ($categoryNameValid && $category->getName() !== trim($entry['part']['category']['name'])) { - throw new RuntimeException(sprintf('Category name does not match exact the given name. Given for import: %s, found category: %s', $entry['category']['name'], $category->getName())); + if ($categoryNameValid && $category !== null && isset($entry['part']['category']['name']) && $category->getName() !== trim($entry['part']['category']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.category.name", + $entry['part']['category']['name'], + [ + '%importValue%' => '' . $entry['part']['category']['name'] . '', + '%foundId%' => $category->getID(), + '%foundValue%' => '' . $category->getName() . '' + ] + )); } - $part->setDescription($partDescription); - $part->setManufacturer($manufacturer); - $part->setCategory($category); + if ($result->getViolations()->count() > 0) { + continue; + } - if ($partMpnrValid) { - $part->setManufacturerProductNumber($entry['part']['mpnr'] ?? ''); + if ($partDescription !== '') { + //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe die Beschreibung des Bauteils mit übernehmen. + $part->setDescription($partDescription); } - if ($partIpnValid) { - $part->setIpn($entry['part']['ipn'] ?? ''); + + if ($manufacturer !== null && $manufacturer->getID() !== $part->getManufacturerID()) { + //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe des Hersteller des Bauteils mit übernehmen. + $part->setManufacturer($manufacturer); + } + + if ($category !== null && $category->getID() !== $part->getCategoryID()) { + //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe die Kategorie des Bauteils mit übernehmen. + $part->setCategory($category); } if ($objectType === AssemblyBOMEntry::class) { $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['part' => $part]); if ($bomEntry === null) { - $name = isset($entry['name']) && $entry['name'] !== null ? trim($entry['name']) : ''; - $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $name]); + if (isset($entry['name']) && $entry['name'] !== '') { + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $entry['name']]); + } if ($bomEntry === null) { $bomEntry = new AssemblyBOMEntry(); @@ -431,9 +585,22 @@ private function parseJson(string $data, array $options = [], string $objectType $bomEntry->setName($entry['name'] ?? ''); $bomEntry->setPart($part); - } - $result[] = $bomEntry; + $result->addBomEntry($bomEntry); + } else { + //Eintrag ohne Part-Relation in die Bauteilliste aufnehmen + + if ($objectType === AssemblyBOMEntry::class) { + $bomEntry = new AssemblyBOMEntry(); + } else { + $bomEntry = new ProjectBOMEntry(); + } + + $bomEntry->setQuantity($entry['quantity']); + $bomEntry->setName($entry['name'] ?? ''); + + $result->addBomEntry($bomEntry); + } } return $result; @@ -462,6 +629,18 @@ private function normalizeColumnNames(array $entry): array return $out; } + private function buildJsonViolation(string $message, string $propertyPath, mixed $invalidValue = null, array $parameters = []): ConstraintViolation + { + return new ConstraintViolation( + message: $this->translator->trans($message, $parameters, 'validators'), + messageTemplate: $message, + parameters: $parameters, + root: $this->jsonRoot, + propertyPath: $propertyPath, + invalidValue: $invalidValue + ); + } + /** * Parse KiCad schematic BOM with flexible field mapping */ diff --git a/src/Services/ImportExportSystem/ImporterResult.php b/src/Services/ImportExportSystem/ImporterResult.php new file mode 100644 index 000000000..4e289d133 --- /dev/null +++ b/src/Services/ImportExportSystem/ImporterResult.php @@ -0,0 +1,60 @@ +bomEntries = $bomEntries; + $this->violations = new ConstraintViolationList(); + } + + /** + * Fügt einen neuen BOM-Eintrag hinzu. + */ + public function addBomEntry(object $bomEntry): void + { + $this->bomEntries[] = $bomEntry; + } + + /** + * Gibt alle BOM-Einträge zurück. + */ + public function getBomEntries(): array + { + return $this->bomEntries; + } + + /** + * Gibt die Liste der Violation zurück. + */ + public function getViolations(): ConstraintViolationList + { + return $this->violations; + } + + /** + * Fügt eine neue `ConstraintViolation` zur Liste hinzu. + */ + public function addViolation(ConstraintViolation $violation): void + { + $this->violations->add($violation); + } + + /** + * Prüft, ob die Liste der Violationen leer ist. + */ + public function hasViolations(): bool + { + return count($this->violations) > 0; + } +} \ No newline at end of file diff --git a/templates/assemblies/import_bom.html.twig b/templates/assemblies/import_bom.html.twig index dc9430422..04ff328a3 100644 --- a/templates/assemblies/import_bom.html.twig +++ b/templates/assemblies/import_bom.html.twig @@ -3,16 +3,27 @@ {% block title %}{% trans %}assembly.import_bom{% endtrans %}{% endblock %} {% block before_card %} - {% if errors %} + {% if validationErrors or importerErrors %}

    {% trans %}parts.import.errors.title{% endtrans %}

      - {% for violation in errors %} -
    • - {{ violation.propertyPath }}: - {{ violation.message|trans(violation.parameters, 'validators') }} -
    • - {% endfor %} + {% if validationErrors %} + {% for violation in validationErrors %} +
    • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators') }} +
    • + {% endfor %} + {% endif %} + + {% if importerErrors %} + {% for violation in importerErrors %} +
    • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators')|raw }} +
    • + {% endfor %} + {% endif %}
    {% endif %} diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 06354533e..46471d02d 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -389,5 +389,59 @@ Musíte vybrat součást nebo nastavit název pro nesoučást!
    + + + validator.bom_importer.json.quantity.required + Musíte zadat množství > 0! + + + + + validator.bom_importer.json.quantity.float + očekává se jako float větší než 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + očekává se jako neprázdný řetězec + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + očekává se jako neprázdný řetězec nebo null + + + + + validator.bom_importer.json.parameter.array + očekává se jako pole (array) + + + + + validator.bom_importer.json.parameter.subproperties + musí mít alespoň jeden z následujících pod-parametrů: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + nenalezeno pro %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + se přesně neshoduje. Pro import zadáno: %importValue%, nalezeno (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + musí obsahovat jako pod-parametr buď: "id" jako celé číslo větší než 0 nebo "name" jako neprázdný řetězec + + diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 9a9dea4cd..88ec6784b 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -365,5 +365,59 @@ Du skal vælge en del eller sætte et navn for en ikke-del!
    + + + validator.bom_importer.json.quantity.required + Du skal angive en mængde > 0! + + + + + validator.bom_importer.json.quantity.float + forventet som en float større end 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + forventet som en ikke-tom streng + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + forventet som en ikke-tom streng eller null + + + + + validator.bom_importer.json.parameter.array + forventet som en array + + + + + validator.bom_importer.json.parameter.subproperties + skal have mindst én af følgende underparametre: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + ikke fundet for %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + stemmer ikke helt overens. Givet til import: %importValue%, fundet (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + skal indeholde som en underparameter enten: "id" som et heltal større end 0 eller "name" som en ikke-tom streng + + diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 6fde3419a..cb710ac97 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -386,7 +386,61 @@ validator.assembly.bom_entry.name_or_part_needed - Sie müssen ein Bauteil auswählen, oder einen Namen für ein nicht-Bauteil setzen! + Sie müssen ein Bauteil auswählen, oder einen Namen für den Eintrag setzen! + + + + + validator.bom_importer.json.quantity.required + Sie müssen eine Stückzahl > 0 angeben! + + + + + validator.bom_importer.json.quantity.float + wird als float größer als 0.0 erwartet + + + + + validator.bom_importer.json.parameter.string.notEmpty + als nicht leere Zeichenkette erwartet + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + als nicht leere Zeichenkette oder null erwartet + + + + + validator.bom_importer.json.parameter.array + als array erwartet + + + + + validator.bom_importer.json.parameter.subproperties + muss mindestens eines der folgenden Unter-Parameter haben: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + nicht gefunden für %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + stimmt nicht genau überein. Für den Import gegeben: %importValue%, gefunden (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + muss entweder als Unter-Parameter zugewiesen haben: "id" als Ganzzahl größer als 0 oder "name" als nicht leere Zeichenfolge diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index ee27863c0..318ba8928 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -31,5 +31,59 @@ Πρέπει να επιλέξετε ένα εξάρτημα ή να βάλετε ένα όνομα για ένα μη εξάρτημα!
    + + + validator.bom_importer.json.quantity.required + Πρέπει να εισαγάγετε ποσότητα > 0! + + + + + validator.bom_importer.json.quantity.float + αναμένεται ως δεκαδικός αριθμός (float) μεγαλύτερος από 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + αναμένεται ως μη κενή συμβολοσειρά + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + αναμένεται ως μη κενή συμβολοσειρά ή null + + + + + validator.bom_importer.json.parameter.array + αναμένεται ως array + + + + + validator.bom_importer.json.parameter.subproperties + πρέπει να έχει τουλάχιστον μία από τις ακόλουθες υπο-παραμέτρους: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + δεν βρέθηκε για %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + δεν ταιριάζει απόλυτα. Δόθηκε για εισαγωγή: %importValue%, βρέθηκε (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + πρέπει να περιέχει ως υπο-παράμετρο είτε: "id" ως ακέραιο αριθμό μεγαλύτερο από 0 είτε "name" ως μη κενή συμβολοσειρά + + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 86525b6a8..93640fff7 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -389,5 +389,59 @@ __validator.assembly.bom_entry.name_or_part_needed
    + + + validator.bom_importer.json.quantity.required + you must specify a quantity > 0! + + + + + validator.bom_importer.json.quantity.float + expected as float greater than 0.0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + expected as non-empty string + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + als nicht leere Zeichenkette oder null erwartet + + + + + validator.bom_importer.json.parameter.array + expectd as array + + + + + validator.bom_importer.json.parameter.subproperties + must have at least one of the following sub-properties: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + not found for %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + does not match exactly. Given for import: %importValue%, found (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + must have either assigned as sub-property: "id" as an integer greater than 0, or "name" as a non-empty string + + diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index e9bf32597..cde3672f6 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -224,7 +224,61 @@ validator.assembly.bom_entry.name_or_part_needed - Vous devez sélectionner une pièce ou attribuer un nom pour un non-élément ! + Vous devez sélectionner une pièce ou attribuer un nom pour un non-élément! + + + + + validator.bom_importer.json.quantity.required + Vous devez entrer une quantité > 0 ! + + + + + validator.bom_importer.json.quantity.float + attendu comme un nombre décimal (float) supérieur à 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + attendu comme une chaîne de caractères non vide + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + attendu comme une chaîne de caractères non vide ou null + + + + + validator.bom_importer.json.parameter.array + attendu comme un tableau (array) + + + + + validator.bom_importer.json.parameter.subproperties + doit contenir au moins l'un des sous-paramètres suivants : %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + non trouvé pour %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + ne correspond pas exactement. Donné pour l'importation : %importValue%, trouvé (%foundId%) : %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + doit contenir comme sous-paramètre soit : "id" comme entier supérieur à 0 ou "name" comme chaîne de caractères non vide diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 9c9c3960c..988724201 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -383,5 +383,59 @@ Morate odabrati dio ili unijeti naziv za nedio!
    + + + validator.bom_importer.json.quantity.required + Morate unijeti količinu > 0! + + + + + validator.bom_importer.json.quantity.float + očekuje se decimalni broj (float) veći od 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + očekuje se kao neprazan niz znakova + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + očekuje se kao neprazan niz znakova ili null + + + + + validator.bom_importer.json.parameter.array + očekuje se kao niz + + + + + validator.bom_importer.json.parameter.subproperties + mora sadržavati barem jedan od sljedećih pod-parametara: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + nije pronađeno za %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + ne podudara se točno. Uneseno za uvoz: %importValue%, pronađeno (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + mora sadržavati kao pod-parametar bilo: "id" kao cijeli broj veći od 0 ili "name" kao neprazan niz znakova + + diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index 2f747bc5c..63ca86d77 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -383,5 +383,59 @@ È necessario selezionare una parte o inserire un nome per un non-parte!
    + + + validator.bom_importer.json.quantity.required + Devi inserire una quantità > 0! + + + + + validator.bom_importer.json.quantity.float + atteso come numero decimale (float) maggiore di 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + atteso come stringa non vuota + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + atteso come stringa non vuota o null + + + + + validator.bom_importer.json.parameter.array + atteso come array + + + + + validator.bom_importer.json.parameter.subproperties + deve avere almeno uno dei seguenti sotto-parametri: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + non trovato per %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + non corrisponde esattamente. Valore dato per l'importazione: %importValue%, trovato (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + deve contenere come sotto-parametro: "id" come intero maggiore di 0 o "name" come stringa non vuota + + diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index 0156ffefc..c0b541173 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -227,5 +227,59 @@ 部品を選択するか、非部品の名前を入力する必要があります!
    + + + validator.bom_importer.json.quantity.required + 数量 > 0 を入力する必要があります! + + + + + validator.bom_importer.json.quantity.float + 0.0 より大きい小数 (float) である必要があります + + + + + validator.bom_importer.json.parameter.string.notEmpty + 空でない文字列が期待されます + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + 空でない文字列または null が期待されます + + + + + validator.bom_importer.json.parameter.array + 配列として期待されます + + + + + validator.bom_importer.json.parameter.subproperties + 以下のサブパラメーターのいずれかを含む必要があります:%propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + %value% に対する項目が見つかりません + + + + + validator.bom_importer.json.parameter.noExactMatch + 完全には一致しません。インポートされた値:%importValue%、見つかった値 (%foundId%):%foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + サブパラメーターとして次のいずれかを含む必要があります:"id" は 0 より大きい整数、または "name" は空でない文字列 + + diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 2cc4aef43..d417757dd 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -383,5 +383,59 @@ Musisz wybrać element lub przypisać nazwę dla elementu niestandardowego!
    + + + validator.bom_importer.json.quantity.required + Musisz wprowadzić ilość > 0! + + + + + validator.bom_importer.json.quantity.float + oczekiwano liczby zmiennoprzecinkowej (float) większej od 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + oczekiwano jako niepusty ciąg znaków + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + oczekiwano jako niepusty ciąg znaków lub null + + + + + validator.bom_importer.json.parameter.array + oczekiwano jako tablicę + + + + + validator.bom_importer.json.parameter.subproperties + musi zawierać co najmniej jeden z następujących podparametrów: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + nie znaleziono dla %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + brak dokładnego dopasowania. Wprowadzone do importu: %importValue%, znalezione (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + musi zawierać jako podparametr: "id" jako liczbę całkowitą większą od 0 lub "name" jako niepusty ciąg znaków + + diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 4049b453e..625aea240 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -383,5 +383,59 @@ Необходимо выбрать деталь или ввести название для недетали!
    + + + validator.bom_importer.json.quantity.required + Необходимо указать количество > 0! + + + + + validator.bom_importer.json.quantity.float + ожидается число с плавающей запятой (float), большее 0,0 + + + + + validator.bom_importer.json.parameter.string.notEmpty + ожидается непустая строка + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + ожидается непустая строка или null + + + + + validator.bom_importer.json.parameter.array + ожидается массив + + + + + validator.bom_importer.json.parameter.subproperties + должен содержать хотя бы один из следующих под-параметров: %propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + не найдено для %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + точное совпадение отсутствует. Указано для импорта: %importValue%, найдено (%foundId%): %foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + должен содержать под-параметр: "id" как целое число больше 0 или "name" как непустая строка + + diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 3eab5c4e3..5093ce989 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -371,5 +371,59 @@ 必须选择零件或为非零件指定名称!
    + + + validator.bom_importer.json.quantity.required + 必须输入数量 > 0! + + + + + validator.bom_importer.json.quantity.float + 应为大于 0.0 的浮点数 (float) + + + + + validator.bom_importer.json.parameter.string.notEmpty + 应为非空字符串 + + + + + validator.bom_importer.json.parameter.string.notEmpty.null + 应为非空字符串或 null + + + + + validator.bom_importer.json.parameter.array + 应为数组 + + + + + validator.bom_importer.json.parameter.subproperties + 必须包含以下子参数之一:%propertyString% + + + + + validator.bom_importer.json.parameter.notFoundFor + 未找到对应值 %value% + + + + + validator.bom_importer.json.parameter.noExactMatch + 未精确匹配。用于导入的值:%importValue%,找到的值 (%foundId%):%foundValue% + + + + + validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + 必须包含子参数:"id" 为大于 0 的整数,或 "name" 为非空字符串 + + From 304156cda065d3f614615ccc92fd330d43be15a1 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 16 Apr 2025 10:21:44 +0200 Subject: [PATCH 42/83] =?UTF-8?q?Anpassungen=20zu=20JSON=20Importer=20vorn?= =?UTF-8?q?ehmen.=20CSV=20Importer=20implementieren.=20=C3=9Cbersetzungsar?= =?UTF-8?q?beiten=20vornehmen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Controller/AssemblyController.php | 1 + .../ImportExportSystem/BOMImporter.php | 720 ++++++++++++------ templates/assemblies/import_bom.html.twig | 42 + translations/messages.cs.xlf | 187 ++++- translations/messages.da.xlf | 217 +++++- translations/messages.de.xlf | 163 +++- translations/messages.el.xlf | 189 ++++- translations/messages.en.xlf | 185 ++++- translations/messages.es.xlf | 193 ++++- translations/messages.fr.xlf | 307 ++++++-- translations/messages.it.xlf | 305 ++++++-- translations/messages.ja.xlf | 313 +++++--- translations/messages.nl.xlf | 317 +++++--- translations/messages.pl.xlf | 323 +++++--- translations/messages.ru.xlf | 343 ++++++--- translations/messages.zh.xlf | 195 ++++- translations/validators.cs.xlf | 48 +- translations/validators.da.xlf | 48 +- translations/validators.de.xlf | 48 +- translations/validators.el.xlf | 48 +- translations/validators.en.xlf | 48 +- translations/validators.fr.xlf | 48 +- translations/validators.hr.xlf | 48 +- translations/validators.it.xlf | 48 +- translations/validators.ja.xlf | 48 +- translations/validators.pl.xlf | 48 +- translations/validators.ru.xlf | 48 +- translations/validators.zh.xlf | 48 +- 28 files changed, 3402 insertions(+), 1174 deletions(-) diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php index 54cc1abb0..9106f6775 100644 --- a/src/Controller/AssemblyController.php +++ b/src/Controller/AssemblyController.php @@ -139,6 +139,7 @@ public function importBOM(Request $request, EntityManagerInterface $entityManage 'required' => true, 'choices' => [ 'assembly.bom_import.type.json' => 'json', + 'assembly.bom_import.type.csv' => 'csv', 'assembly.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew', ] ]); diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 6688780a4..fc73835c3 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -38,6 +38,7 @@ use League\Csv\Reader; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\Translation\TranslatorInterface; use UnexpectedValueException; @@ -49,6 +50,11 @@ class BOMImporter { + private const IMPORT_TYPE_JSON = 'json'; + private const IMPORT_TYPE_CSV = 'csv'; + private const IMPORT_TYPE_KICAD_PCB = 'kicad_pcbnew'; + private const IMPORT_TYPE_KICAD_SCHEMATIC = 'kicad_schematic'; + private const MAP_KICAD_PCB_FIELDS = [ 0 => 'Id', 1 => 'Designator', @@ -66,6 +72,8 @@ class BOMImporter private CategoryRepository $categoryRepository; + private DBElementRepository $projectBOMEntryRepository; + private DBElementRepository $assemblyBOMEntryRepository; private TranslatorInterface $translator; @@ -79,6 +87,7 @@ public function __construct( $this->partRepository = $entityManager->getRepository(Part::class); $this->manufacturerRepository = $entityManager->getRepository(Manufacturer::class); $this->categoryRepository = $entityManager->getRepository(Category::class); + $this->projectBOMEntryRepository = $entityManager->getRepository(Project::class); $this->assemblyBOMEntryRepository = $entityManager->getRepository(AssemblyBOMEntry::class); $this->translator = $translator; } @@ -86,7 +95,7 @@ public function __construct( protected function configureOptions(OptionsResolver $resolver): OptionsResolver { $resolver->setRequired('type'); - $resolver->setAllowedValues('type', ['kicad_pcbnew', 'kicad_schematic', 'json']); + $resolver->setAllowedValues('type', [self::IMPORT_TYPE_KICAD_PCB, self::IMPORT_TYPE_KICAD_SCHEMATIC, self::IMPORT_TYPE_JSON, self::IMPORT_TYPE_CSV]); // For flexible schematic import with field mapping $resolver->setDefined(['field_mapping', 'field_priorities', 'delimiter']); @@ -120,7 +129,7 @@ public function importFileIntoProject(File $file, Project $project, array $optio * Converts the given file into an ImporterResult with an array of BOM entries using the given options and save them into the given assembly. * The changes are not saved into the database yet. */ - public function importFileIntoAssembly(File $file, Assembly $assembly, array $options): ImporterResult + public function importFileIntoAssembly(UploadedFile $file, Assembly $assembly, array $options): ImporterResult { $importerResult = $this->fileToImporterResult($file, $options, AssemblyBOMEntry::class); @@ -163,8 +172,44 @@ public function validateBOMData(string $data, array $options): array /** * Converts the given file into an ImporterResult with an array of BOM entries using the given options. */ - public function fileToImporterResult(File $file, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult + public function fileToImporterResult(UploadedFile $file, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult { + $result = new ImporterResult(); + + //Available file endings depending on the import type + $validExtensions = match ($options['type']) { + self::IMPORT_TYPE_KICAD_PCB => ['kicad_pcb'], + self::IMPORT_TYPE_JSON => ['json'], + self::IMPORT_TYPE_CSV => ['csv'], + default => [], + }; + + //Get the file extension of the uploaded file + $fileExtension = pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION); + + //Check whether the file extension is valid + if ($validExtensions === []) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.invalid_import_type', + 'import.type' + )); + + return $result; + } else if (!in_array(strtolower($fileExtension), $validExtensions, true)) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.invalid_file_extension', + 'file.extension', + $fileExtension, + [ + '%extension%' => $fileExtension, + '%importType%' => $this->translator->trans('assembly.bom_import.type.'.$options['type']), + '%allowedExtensions%' => implode(', ', $validExtensions), + ] + )); + + return $result; + } + return $this->stringToImporterResult($file->getContent(), $options, $objectType); } @@ -181,8 +226,8 @@ public function stringToBOMEntries(string $data, array $options, string $objectT $options = $resolver->resolve($options); return match ($options['type']) { - 'kicad_pcbnew' => $this->parseKiCADPCB($data, $options, $objectType)->getBomEntries(), - default => throw new InvalidArgumentException('Invalid import type!'), + self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $objectType)->getBomEntries(), + default => throw new InvalidArgumentException($this->translator->trans('validator.bom_importer.invalid_import_type', [], 'validators')), }; } @@ -190,7 +235,7 @@ public function stringToBOMEntries(string $data, array $options, string $objectT * Import string data into an array of BOM entries, which are not yet assigned to a project. * @param string $data The data to import * @param array $options An array of options - * @return ProjectBOMEntry[]|AssemblyBOMEntry[] An array of imported entries + * @return ImporterResult An result of imported entries or a violation list */ public function stringToImporterResult(string $data, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult { @@ -198,10 +243,17 @@ public function stringToImporterResult(string $data, array $options, string $obj $resolver = $this->configureOptions($resolver); $options = $resolver->resolve($options); + $defaultImporterResult = new ImporterResult(); + $defaultImporterResult->addViolation($this->buildJsonViolation( + 'validator.bom_importer.invalid_import_type', + 'import.type' + )); + return match ($options['type']) { - 'kicad_pcbnew' => $this->parseKiCADPCB($data, $objectType), - 'json' => $this->parseJson($data, $options, $objectType), - default => throw new InvalidArgumentException('Invalid import type!'), + self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $objectType), + self::IMPORT_TYPE_JSON => $this->parseJson($data, $objectType), + self::IMPORT_TYPE_CSV => $this->parseCsv($data, $objectType), + default => $defaultImporterResult, }; } @@ -238,7 +290,7 @@ private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntr $bom_entry->setName($entry['Designation']); } - $bom_entry->setMountnames($entry['Designator'] ?? ''); + $bom_entry->setMountnames($entry['Designator']); $bom_entry->setComment($entry['Supplier and ref'] ?? ''); $bom_entry->setQuantity((float) ($entry['Quantity'] ?? 1)); @@ -305,6 +357,36 @@ private function validateKiCADSchematicData(string $data, array $options): array return $this->validationService->validateBOMEntries($mapped_entries, $options); } + /** + * Parses the given JSON data into an ImporterResult while validating and transforming entries according to the + * specified options and object type. If violations are encountered during parsing, they are added to the result. + * + * The structure of each entry in the JSON data is validated to ensure that required fields (e.g., quantity, and name) + * are present, and optional composite fields, like `part` and its sub-properties, meet specific criteria. Various + * conditions are checked, including whether the provided values are the correct types, and if relationships (like + * matching parts or manufacturers) are resolved successfully. + * + * Violations are added for: + * - Missing or invalid `quantity` values. + * - Non-string `name` values. + * - Invalid structure or missing sub-properties in `part`. + * - Incorrect or unresolved references to parts and their information, such as `id`, `name`, `manufacturer_product_number` + * (mpnr), `internal_part_number` (ipn), or `description`. + * - Inconsistent or absent manufacturer information. + * + * If a match for a part or manufacturer cannot be resolved, a violation is added alongside an indication of the + * imported value and any partially matched information. Warnings for no exact matches are also added for parts + * using specific identifying properties like name, manufacturer product number, or internal part numbers. + * + * Additional validations include: + * - Checking for empty or invalid descriptions. + * - Ensuring manufacturers, if specified, have valid `name` or `id` values. + * + * @param string $data JSON encoded string containing BOM entries data. + * @param string $objectType The type of entries expected during import (e.g., `ProjectBOMEntry` or `AssemblyBOMEntry`). + * + * @return ImporterResult The result containing parsed data and any violations encountered during the parsing process. + */ private function parseJson(string $data, array $options = [], string $objectType = ProjectBOMEntry::class): ImporterResult { $result = new ImporterResult(); @@ -313,297 +395,434 @@ private function parseJson(string $data, array $options = [], string $objectType $data = json_decode($data, true); foreach ($data as $key => $entry) { - // Check quantity if (!isset($entry['quantity'])) { $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.quantity.required', + 'validator.bom_importer.json_csv.quantity.required', "entry[$key].quantity" )); } if (isset($entry['quantity']) && (!is_float($entry['quantity']) || $entry['quantity'] <= 0)) { $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.quantity.float', + 'validator.bom_importer.json_csv.quantity.float', "entry[$key].quantity", $entry['quantity'] )); } - // Check name if (isset($entry['name']) && !is_string($entry['name'])) { $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.string.notEmpty', + 'validator.bom_importer.json_csv.parameter.string.notEmpty', "entry[$key].name", $entry['name'] )); } - // Check if part is assigned with relevant information if (isset($entry['part'])) { - if (!is_array($entry['part'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.array', - "entry[$key].part", - $entry['part'] - )); - } + $this->processPart($entry, $result, $key, $objectType,self::IMPORT_TYPE_JSON); + } else { + $bomEntry = $this->getOrCreateBomEntry($objectType, $entry['name'] ?? null); + $bomEntry->setQuantity((float) $entry['quantity']); - $partIdValid = isset($entry['part']['id']) && is_int($entry['part']['id']) && $entry['part']['id'] > 0; - $partNameValid = isset($entry['part']['name']) && is_string($entry['part']['name']) && trim($entry['part']['name']) !== ''; - $partMpnrValid = isset($entry['part']['mpnr']) && is_string($entry['part']['mpnr']) && trim($entry['part']['mpnr']) !== ''; - $partIpnValid = isset($entry['part']['ipn']) && is_string($entry['part']['ipn']) && trim($entry['part']['ipn']) !== ''; - - if (!$partIdValid && !$partNameValid && !$partMpnrValid && !$partIpnValid) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.subproperties', - "entry[$key].part", - $entry['part'], - ['%propertyString%' => '"id", "name", "mpnr", or "ipn"'] - )); - } + $result->addBomEntry($bomEntry); + } + } - $part = $partIdValid ? $this->partRepository->findOneBy(['id' => $entry['part']['id']]) : null; - $part = $part ?? ($partMpnrValid ? $this->partRepository->findOneBy(['manufacturer_product_number' => trim($entry['part']['mpnr'])]) : null); - $part = $part ?? ($partIpnValid ? $this->partRepository->findOneBy(['ipn' => trim($entry['part']['ipn'])]) : null); - $part = $part ?? ($partNameValid ? $this->partRepository->findOneBy(['name' => trim($entry['part']['name'])]) : null); - - if ($part === null) { - $value = sprintf('part.id: %s, part.mpnr: %s, part.ipn: %s, part.name: %s', - isset($entry['part']['id']) ? '' . $entry['part']['id'] . '' : '-', - isset($entry['part']['mpnr']) ? '' . $entry['part']['mpnr'] . '' : '-', - isset($entry['part']['ipn']) ? '' . $entry['part']['ipn'] . '' : '-', - isset($entry['part']['name']) ? '' . $entry['part']['name'] . '' : '-', - ); - - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.notFoundFor', - "entry[$key].part", - $entry['part'], - ['%value%' => $value] - )); - } + return $result; + } - if ($partNameValid && $part !== null && isset($entry['part']['name']) && $part->getName() !== trim($entry['part']['name'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.noExactMatch', - "entry[$key].part.name", - $entry['part']['name'], - [ - '%importValue%' => '' . $entry['part']['name'] . '', - '%foundId%' => $part->getID(), - '%foundValue%' => '' . $part->getName() . '' - ] - )); - } - if ($partMpnrValid && $part !== null && isset($entry['part']['mpnr']) && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.noExactMatch', - "entry[$key].part.mpnr", - $entry['part']['mpnr'], - [ - '%importValue%' => '' . $entry['part']['mpnr'] . '', - '%foundId%' => $part->getID(), - '%foundValue%' => '' . $part->getManufacturerProductNumber() . '' - ] - )); - } + /** + * Parses a CSV string and processes its rows into hierarchical data structures, + * performing validations and converting data based on the provided headers. + * Handles potential violations and manages the creation of BOM entries based on the given type. + * + * @param string $csvData The raw CSV data to parse, with rows separated by newlines. + * @param string $objectType The class type to instantiate for BOM entries, defaults to ProjectBOMEntry. + * + * @return ImporterResult Returns an ImporterResult instance containing BOM entries and any validation violations encountered. + */ + function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): ImporterResult + { + $result = new ImporterResult(); + $rows = explode("\n", trim($csvData)); + $headers = str_getcsv(array_shift($rows), ';'); - if ($partIpnValid && $part !== null && isset($entry['part']['ipn']) && $part->getIpn() !== trim($entry['part']['ipn'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.noExactMatch', - "entry[$key].part.ipn", - $entry['part']['ipn'], - [ - '%importValue%' => '' . $entry['part']['ipn'] . '', - '%foundId%' => $part->getID(), - '%foundValue%' => '' . $part->getIpn() . '' - ] - )); - } + foreach ($rows as $key => $row) { + $entry = []; + $values = str_getcsv($row, ';'); - // Part: Description check - if (isset($entry['part']['description'])) { - if (!is_string($entry['part']['description']) || trim($entry['part']['description']) === '') { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.string.notEmpty', - 'entry[$key].part.description', - $entry['part']['description'] - )); - } - } + foreach ($headers as $index => $column) { + //Convert column name into hierarchy + $path = explode('_', $column); + $temp = &$entry; - $partDescription = $entry['part']['description'] ?? ''; - - // Part: Manufacturer check - $manufacturerIdValid = false; - $manufacturerNameValid = false; - if (array_key_exists('manufacturer', $entry['part'])) { - if (!is_array($entry['part']['manufacturer'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.array', - 'entry[$key].part.manufacturer', - $entry['part']['manufacturer']) ?? null - ); + foreach ($path as $step) { + if (!isset($temp[$step])) { + $temp[$step] = []; } - $manufacturerIdValid = isset($entry['part']['manufacturer']['id']) && is_int($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] > 0; - $manufacturerNameValid = isset($entry['part']['manufacturer']['name']) && is_string($entry['part']['manufacturer']['name']) && trim($entry['part']['manufacturer']['name']) !== ''; + $temp = &$temp[$step]; + } - // Stellen sicher, dass mindestens eine Bedingung für manufacturer erfüllt sein muss - if (!$manufacturerIdValid && !$manufacturerNameValid) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties', - "entry[$key].part.manufacturer", - $entry['part']['manufacturer'], - )); + //If there is no value, skip + if (isset($values[$index]) && $values[$index] !== '') { + //Check whether the value is numerical + if (is_numeric($values[$index])) { + //Convert to integer or float + $temp = (strpos($values[$index], '.') !== false) + ? floatval($values[$index]) + : intval($values[$index]); + } else { + //Leave other data types untouched + $temp = $values[$index]; } } + } - $manufacturer = $manufacturerIdValid ? $this->manufacturerRepository->findOneBy(['id' => $entry['part']['manufacturer']['id']]) : null; - $manufacturer = $manufacturer ?? ($manufacturerNameValid ? $this->manufacturerRepository->findOneBy(['name' => trim($entry['part']['manufacturer']['name'])]) : null); - - if (($manufacturerIdValid || $manufacturerNameValid) && $manufacturer === null) { - $value = sprintf( - 'manufacturer.id: %s, manufacturer.name: %s', - isset($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] !== null ? '' . $entry['part']['manufacturer']['id'] . '' : '-', - isset($entry['part']['manufacturer']['name']) && $entry['part']['manufacturer']['name'] !== null ? '' . $entry['part']['manufacturer']['name'] . '' : '-' - ); - - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.notFoundFor', - "entry[$key].part.manufacturer", - $entry['part']['manufacturer'], - ['%value%' => $value] - )); - } + $entry = $this->removeEmptyProperties($entry); - if ($manufacturerNameValid && $manufacturer !== null && isset($entry['part']['manufacturer']['name']) && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.noExactMatch', - "entry[$key].part.manufacturer.name", - $entry['part']['manufacturer']['name'], - [ - '%importValue%' => '' . $entry['part']['manufacturer']['name'] . '', - '%foundId%' => $manufacturer->getID(), - '%foundValue%' => '' . $manufacturer->getName() . '' - ] - )); - } + if (!isset($entry['quantity'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.csv.quantity.required', + "row[$key].quantity" + )); + } - // Part: Category check - $categoryIdValid = false; - $categoryNameValid = false; - if (array_key_exists('category', $entry['part'])) { - if (!is_array($entry['part']['category'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.array', - 'entry[$key].part.category', - $entry['part']['category']) ?? null - ); - } + if (isset($entry['quantity']) && (!is_numeric($entry['quantity']) || $entry['quantity'] <= 0)) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.csv.quantity.float', + "row[$key].quantity", + $entry['quantity'] + )); + } - $categoryIdValid = isset($entry['part']['category']['id']) && is_int($entry['part']['category']['id']) && $entry['part']['category']['id'] > 0; - $categoryNameValid = isset($entry['part']['category']['name']) && is_string($entry['part']['category']['name']) && trim($entry['part']['category']['name']) !== ''; + if (isset($entry['name']) && !is_string($entry['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.csv.parameter.string.notEmpty', + "row[$key].name", + $entry['name'] + )); + } - if (!$categoryIdValid && !$categoryNameValid) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties', - "entry[$key].part.category", - $entry['part']['category'] - )); - } - } + if (isset($entry['part'])) { + $this->processPart($entry, $result, $key, $objectType, self::IMPORT_TYPE_CSV); + } else { + $bomEntry = $this->getOrCreateBomEntry($objectType, $entry['name'] ?? null); + $bomEntry->setQuantity((float) $entry['quantity'] ?? 0); - $category = $categoryIdValid ? $this->categoryRepository->findOneBy(['id' => $entry['part']['category']['id']]) : null; - $category = $category ?? ($categoryNameValid ? $this->categoryRepository->findOneBy(['name' => trim($entry['part']['category']['name'])]) : null); - - if (($categoryIdValid || $categoryNameValid) && $category === null) { - $value = sprintf( - 'category.id: %s, category.name: %s', - isset($entry['part']['category']['id']) && $entry['part']['category']['id'] !== null ? '' . $entry['part']['category']['id'] . '' : '-', - isset($entry['part']['category']['name']) && $entry['part']['category']['name'] !== null ? '' . $entry['part']['category']['name'] . '' : '-' - ); - - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.notFoundFor', - "entry[$key].part.category", - $entry['part']['category'], - ['%value%' => $value] - )); - } + $result->addBomEntry($bomEntry); + } + } - if ($categoryNameValid && $category !== null && isset($entry['part']['category']['name']) && $category->getName() !== trim($entry['part']['category']['name'])) { - $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.json.parameter.noExactMatch', - "entry[$key].part.category.name", - $entry['part']['category']['name'], - [ - '%importValue%' => '' . $entry['part']['category']['name'] . '', - '%foundId%' => $category->getID(), - '%foundValue%' => '' . $category->getName() . '' - ] - )); - } + return $result; + } - if ($result->getViolations()->count() > 0) { - continue; - } + /** + * Processes an individual part entry in the import data. + * + * This method validates the structure and content of the provided part entry and uses the findings + * to identify corresponding objects in the database. The result is recorded, and violations are + * logged if issues or discrepancies exist in the validation or database matching process. + * + * @param array $entry The array representation of the part entry. + * @param ImporterResult $result The result object used for recording validation violations. + * @param int $key The index of the entry in the data array. + * @param string $objectType The type of object being processed. + * @param string $importType The type of import being performed. + * + * @return void + */ + private function processPart(array $entry, ImporterResult $result, int $key, string $objectType, string $importType): void + { + $prefix = $importType === self::IMPORT_TYPE_JSON ? 'entry' : 'row'; + + if (!is_array($entry['part'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.array', + $prefix."[$key].part", + $entry['part'] + )); + } - if ($partDescription !== '') { - //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe die Beschreibung des Bauteils mit übernehmen. - $part->setDescription($partDescription); - } + $partIdValid = isset($entry['part']['id']) && is_int($entry['part']['id']) && $entry['part']['id'] > 0; + $partMpnrValid = isset($entry['part']['mpnr']) && is_string($entry['part']['mpnr']) && trim($entry['part']['mpnr']) !== ''; + $partIpnValid = isset($entry['part']['ipn']) && is_string($entry['part']['ipn']) && trim($entry['part']['ipn']) !== ''; + $partNameValid = isset($entry['part']['name']) && is_string($entry['part']['name']) && trim($entry['part']['name']) !== ''; + + if (!$partIdValid && !$partNameValid && !$partMpnrValid && !$partIpnValid) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.subproperties', + $prefix."[$key].part", + $entry['part'], + ['%propertyString%' => '"id", "name", "mpnr", or "ipn"'] + )); + } - if ($manufacturer !== null && $manufacturer->getID() !== $part->getManufacturerID()) { - //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe des Hersteller des Bauteils mit übernehmen. - $part->setManufacturer($manufacturer); - } + $part = $partIdValid ? $this->partRepository->findOneBy(['id' => $entry['part']['id']]) : null; + $part = $part ?? ($partMpnrValid ? $this->partRepository->findOneBy(['manufacturer_product_number' => trim($entry['part']['mpnr'])]) : null); + $part = $part ?? ($partIpnValid ? $this->partRepository->findOneBy(['ipn' => trim($entry['part']['ipn'])]) : null); + $part = $part ?? ($partNameValid ? $this->partRepository->findOneBy(['name' => trim($entry['part']['name'])]) : null); + + if ($part === null) { + $value = sprintf('part.id: %s, part.mpnr: %s, part.ipn: %s, part.name: %s', + isset($entry['part']['id']) ? '' . $entry['part']['id'] . '' : '-', + isset($entry['part']['mpnr']) ? '' . $entry['part']['mpnr'] . '' : '-', + isset($entry['part']['ipn']) ? '' . $entry['part']['ipn'] . '' : '-', + isset($entry['part']['name']) ? '' . $entry['part']['name'] . '' : '-', + ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.notFoundFor', + $prefix."[$key].part", + $entry['part'], + ['%value%' => $value] + )); + } - if ($category !== null && $category->getID() !== $part->getCategoryID()) { - //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe die Kategorie des Bauteils mit übernehmen. - $part->setCategory($category); - } + if ($partNameValid && $part !== null && isset($entry['part']['name']) && $part->getName() !== trim($entry['part']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.name", + $entry['part']['name'], + [ + '%importValue%' => '' . $entry['part']['name'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getName() . '' + ] + )); + } - if ($objectType === AssemblyBOMEntry::class) { - $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['part' => $part]); + if ($partMpnrValid && $part !== null && isset($entry['part']['mpnr']) && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.mpnr", + $entry['part']['mpnr'], + [ + '%importValue%' => '' . $entry['part']['mpnr'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getManufacturerProductNumber() . '' + ] + )); + } - if ($bomEntry === null) { - if (isset($entry['name']) && $entry['name'] !== '') { - $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $entry['name']]); - } + if ($partIpnValid && $part !== null && isset($entry['part']['ipn']) && $part->getIpn() !== trim($entry['part']['ipn'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.ipn", + $entry['part']['ipn'], + [ + '%importValue%' => '' . $entry['part']['ipn'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getIpn() . '' + ] + )); + } - if ($bomEntry === null) { - $bomEntry = new AssemblyBOMEntry(); - } - } - } else { - $bomEntry = new ProjectBOMEntry(); - } + if (isset($entry['part']['description'])) { + if (!is_string($entry['part']['description']) || trim($entry['part']['description']) === '') { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.string.notEmpty', + 'entry[$key].part.description', + $entry['part']['description'] + )); + } + } - $bomEntry->setQuantity($entry['quantity']); - $bomEntry->setName($entry['name'] ?? ''); + $partDescription = $entry['part']['description'] ?? ''; - $bomEntry->setPart($part); + $manufacturerIdValid = false; + $manufacturerNameValid = false; + if (array_key_exists('manufacturer', $entry['part'])) { + if (!is_array($entry['part']['manufacturer'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.array', + 'entry[$key].part.manufacturer', + $entry['part']['manufacturer']) ?? null + ); + } - $result->addBomEntry($bomEntry); - } else { - //Eintrag ohne Part-Relation in die Bauteilliste aufnehmen + $manufacturerIdValid = isset($entry['part']['manufacturer']['id']) && is_int($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] > 0; + $manufacturerNameValid = isset($entry['part']['manufacturer']['name']) && is_string($entry['part']['manufacturer']['name']) && trim($entry['part']['manufacturer']['name']) !== ''; + + if (!$manufacturerIdValid && !$manufacturerNameValid) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties', + $prefix."[$key].part.manufacturer", + $entry['part']['manufacturer'], + )); + } + } + + $manufacturer = $manufacturerIdValid ? $this->manufacturerRepository->findOneBy(['id' => $entry['part']['manufacturer']['id']]) : null; + $manufacturer = $manufacturer ?? ($manufacturerNameValid ? $this->manufacturerRepository->findOneBy(['name' => trim($entry['part']['manufacturer']['name'])]) : null); + + if (($manufacturerIdValid || $manufacturerNameValid) && $manufacturer === null) { + $value = sprintf( + 'manufacturer.id: %s, manufacturer.name: %s', + isset($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] !== null ? '' . $entry['part']['manufacturer']['id'] . '' : '-', + isset($entry['part']['manufacturer']['name']) && $entry['part']['manufacturer']['name'] !== null ? '' . $entry['part']['manufacturer']['name'] . '' : '-' + ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.notFoundFor', + $prefix."[$key].part.manufacturer", + $entry['part']['manufacturer'], + ['%value%' => $value] + )); + } + + if ($manufacturerNameValid && $manufacturer !== null && isset($entry['part']['manufacturer']['name']) && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.manufacturer.name", + $entry['part']['manufacturer']['name'], + [ + '%importValue%' => '' . $entry['part']['manufacturer']['name'] . '', + '%foundId%' => $manufacturer->getID(), + '%foundValue%' => '' . $manufacturer->getName() . '' + ] + )); + } + + $categoryIdValid = false; + $categoryNameValid = false; + if (array_key_exists('category', $entry['part'])) { + if (!is_array($entry['part']['category'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.array', + 'entry[$key].part.category', + $entry['part']['category']) ?? null + ); + } + + $categoryIdValid = isset($entry['part']['category']['id']) && is_int($entry['part']['category']['id']) && $entry['part']['category']['id'] > 0; + $categoryNameValid = isset($entry['part']['category']['name']) && is_string($entry['part']['category']['name']) && trim($entry['part']['category']['name']) !== ''; + + if (!$categoryIdValid && !$categoryNameValid) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties', + $prefix."[$key].part.category", + $entry['part']['category'] + )); + } + } + + $category = $categoryIdValid ? $this->categoryRepository->findOneBy(['id' => $entry['part']['category']['id']]) : null; + $category = $category ?? ($categoryNameValid ? $this->categoryRepository->findOneBy(['name' => trim($entry['part']['category']['name'])]) : null); + + if (($categoryIdValid || $categoryNameValid) && $category === null) { + $value = sprintf( + 'category.id: %s, category.name: %s', + isset($entry['part']['category']['id']) && $entry['part']['category']['id'] !== null ? '' . $entry['part']['category']['id'] . '' : '-', + isset($entry['part']['category']['name']) && $entry['part']['category']['name'] !== null ? '' . $entry['part']['category']['name'] . '' : '-' + ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.notFoundFor', + $prefix."[$key].part.category", + $entry['part']['category'], + ['%value%' => $value] + )); + } + + if ($categoryNameValid && $category !== null && isset($entry['part']['category']['name']) && $category->getName() !== trim($entry['part']['category']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.category.name", + $entry['part']['category']['name'], + [ + '%importValue%' => '' . $entry['part']['category']['name'] . '', + '%foundId%' => $category->getID(), + '%foundValue%' => '' . $category->getName() . '' + ] + )); + } - if ($objectType === AssemblyBOMEntry::class) { + if ($result->getViolations()->count() > 0) { + return; + } + + if ($partDescription !== '') { + //When updating the associated parts to a assembly, take over the description of the part. + $part->setDescription($partDescription); + } + + if ($manufacturer !== null && $manufacturer->getID() !== $part->getManufacturerID()) { + //When updating the associated parts, take over to a assembly of the manufacturer of the part. + $part->setManufacturer($manufacturer); + } + + if ($category !== null && $category->getID() !== $part->getCategoryID()) { + //When updating the associated parts to a assembly, take over the category of the part. + $part->setCategory($category); + } + + if ($objectType === AssemblyBOMEntry::class) { + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['part' => $part]); + + if ($bomEntry === null) { + if (isset($entry['name']) && $entry['name'] !== '') { + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $entry['name']]); + } + + if ($bomEntry === null) { $bomEntry = new AssemblyBOMEntry(); - } else { - $bomEntry = new ProjectBOMEntry(); } + } + } else { + $bomEntry = new ProjectBOMEntry(); + } - $bomEntry->setQuantity($entry['quantity']); - $bomEntry->setName($entry['name'] ?? ''); + $bomEntry->setQuantity((float) $entry['quantity']); + $bomEntry->setName($entry['name'] ?? ''); - $result->addBomEntry($bomEntry); + $bomEntry->setPart($part); + + $result->addBomEntry($bomEntry); + } + + private function removeEmptyProperties(array $data): array + { + foreach ($data as $key => &$value) { + //Recursive check when the value is an array + if (is_array($value)) { + $value = $this->removeEmptyProperties($value); + + //Remove the array when it is empty after cleaning + if (empty($value)) { + unset($data[$key]); + } + } elseif ($value === null || $value === '') { + //Remove values that are explicitly zero or empty + unset($data[$key]); } } - return $result; + return $data; + } + + private function getOrCreateBomEntry(string $objectType, ?string $name) + { + $bomEntry = null; + + //Check whether there is a name + if (!empty($name)) { + if ($objectType === ProjectBOMEntry::class) { + $bomEntry = $this->projectBOMEntryRepository->findOneBy(['name' => $name]); + } elseif ($objectType === AssemblyBOMEntry::class) { + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $name]); + } + } + + //If no bom enttry was found, a new object create + if ($bomEntry === null) { + $bomEntry = new $objectType(); + } + + $bomEntry->setName($name); + + return $bomEntry; } /** @@ -629,6 +848,21 @@ private function normalizeColumnNames(array $entry): array return $out; } + + /** + * Builds a JSON-based constraint violation. + * + * This method creates a `ConstraintViolation` object that represents a validation error. + * The violation includes a message, property path, invalid value, and other contextual information. + * Translations for the violation message can be applied through the translator service. + * + * @param string $message The translation key for the validation message. + * @param string $propertyPath The property path where the violation occurred. + * @param mixed|null $invalidValue The value that caused the violation (optional). + * @param array $parameters Additional parameters for message placeholders (default is an empty array). + * + * @return ConstraintViolation The created constraint violation object. + */ private function buildJsonViolation(string $message, string $propertyPath, mixed $invalidValue = null, array $parameters = []): ConstraintViolation { return new ConstraintViolation( diff --git a/templates/assemblies/import_bom.html.twig b/templates/assemblies/import_bom.html.twig index 04ff328a3..9e99c5417 100644 --- a/templates/assemblies/import_bom.html.twig +++ b/templates/assemblies/import_bom.html.twig @@ -53,6 +53,34 @@
    +
    +
    +
    + {% trans %}assembly.import_bom.template.header.csv{% endtrans %} +
    +
    + {{ 'assembly.bom_import.template.csv.exptected_columns'|trans }} + +
    quantity;name;part_id;part_mpnr;part_ipn;part_name;part_description;part_manufacturer_id;part_manufacturer_name;part_category_id;part_category_name
    + +
      +
    • quantity
    • +
    • name
    • +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    • part_description
    • +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    • part_category_id
    • +
    • part_category_name
    • +
    + + {{ 'assembly.bom_import.template.csv.table'|trans|raw }} +
    +
    +
    @@ -61,6 +89,20 @@
    {{ 'assembly.bom_import.template.kicad_pcbnew.exptected_columns'|trans }}
    Id;Designator;Package;Quantity;Designation;Supplier and ref
    + +
      +
    • Id
    • +
    • Designator
    • +
    • Package
    • +
    • Quantity
    • +
    • Designation
    • +
    • Supplier and ref
    • +
    • Note
    • +
    • Footprint
    • +
    • Value
    • +
    • Footprint
    • +
    + {{ 'assembly.bom_import.template.kicad_pcbnew.exptected_columns.note'|trans|raw }} {{ 'assembly.bom_import.template.kicad_pcbnew.table'|trans|raw }} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 52edddb41..c1d1b5177 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -13881,6 +13881,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz JSON pro sestavu + + + assembly.bom_import.type.csv + CSV pro sestavu + + assembly.bom_import.type.kicad_pcbnew @@ -13905,6 +13911,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Šablona importu JSON pro sestavu + + + assembly.import_bom.template.header.csv + Importní šablona CSV pro sestavu + + assembly.import_bom.template.header.kicad_pcbnew @@ -13964,24 +13976,26 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz quantity - Povinné + Povinné pole Číslo s plovoucí desetinnou čárkou (Float) - Musí být uvedeno a obsahovat hodnotu s plovoucí desetinnou čárkou (Float) větší než 0,0. + Musí být vyplněno a obsahovat číselnou hodnotu (Float) větší než 0.0. name Volitelné - Řetězec (String) - Pokud je přítomen, musí být neprázdný řetězec. + Řetězec + Pokud je uvedeno, musí být neprázdný text. Název položky ve skupině. part Volitelné Objekt/Array - Pokud je uvedeno, musí to být objekt/array a minimálně jedno pole musí být vyplněno: + Pokud má být přiřazena součástka, musí být objektem/arrayem a alespoň jedno z následujících polí musí být vyplněno:
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • part.name
    @@ -13990,38 +14004,38 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz part.id Volitelné Celé číslo (Integer) - Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID součástky v Part-DB. + Celé číslo > 0. Odpovídá internímu číselnému ID součástky v databázi. - part.name + part.mpnr Volitelné - Řetězec (String) - Neprázdný řetězec, pokud není zadáno part.mpnr nebo part.ipn. + Řetězec + Neprázdný text, pokud není vyplněno part.id, part.ipn ani part.name. - part.mpnr + part.ipn Volitelné - Řetězec (String) - Neprázdný řetězec, pokud není zadáno part.name nebo part.ipn. + Řetězec + Neprázdný text, pokud není vyplněno part.id, part.mpnr ani part.name. - part.ipn + part.name Volitelné - Řetězec (String) - Neprázdný řetězec, pokud není zadáno part.name nebo part.mpnr. + Řetězec + Neprázdný text, pokud není vyplněno part.id, part.mpnr ani part.ipn. part.description Volitelné Řetězec nebo null - Pokud je přítomen, musí být neprázdný řetězec nebo null. + Pokud je uvedeno, musí být neprázdný řetězec nebo null. Přepíše stávající hodnotu v součástce. part.manufacturer Volitelné Objekt/Array - Pokud je přítomen, musí to být objekt/array a minimálně jedno pole musí být vyplněno: + Pokud má být výrobce součástky upraven nebo má být součástka jednoznačně identifikována pomocí hodnoty part.mpnr, musí být objektem/arrayem a alespoň jedno z následujících polí musí být vyplněno:
    • manufacturer.id
    • manufacturer.name
    • @@ -14032,20 +14046,20 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz manufacturer.id Volitelné Celé číslo (Integer) - Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID výrobce. + Celé číslo > 0. Odpovídá internímu číselnému ID výrobce. manufacturer.name Volitelné - Řetězec (String) - Neprázdný řetězec, pokud není uvedeno manufacturer.id. + Řetězec + Neprázdný text, pokud není uveden manufacturer.id. part.category Volitelné Objekt/Array - Pokud je přítomen, musí to být objekt/array a minimálně jedno pole musí být vyplněno: + Pokud má být kategorie součástky upravena, musí být objektem/arrayem a alespoň jedno z následujících polí musí být vyplněno:
      • category.id
      • category.name
      • @@ -14056,13 +14070,138 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz category.id Volitelné Celé číslo (Integer) - Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID kategorie součástky. + Celé číslo > 0. Odpovídá internímu číselnému ID kategorie součástky. category.name Volitelné - Řetězec (String) - Neprázdný řetězec, pokud není uvedeno category.id. + Řetězec + Neprázdný text, pokud není uvedeno category.id. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Možné sloupce: + + + + + assembly.bom_import.template.csv.table + + + + + Sloupec + Podmínka + Datový typ + Popis + + + + + quantity + Povinné pole + Číslo s plovoucí desetinnou čárkou (Float) + Musí být vyplněno a obsahovat číselnou hodnotu (Float) větší než 0.0. + + + name + Volitelné + Řetězec + Název položky ve skupině. + + + Sloupce začínající part_ + + Pokud má být přiřazena součástka, jeden z následujících sloupců musí být uveden a vyplněn: +
          +
        • part_id
        • +
        • part_mpnr
        • +
        • part_ipn
        • +
        • part_name
        • +
        + + + + part_id + Volitelné + Celé číslo (Integer) + Celé číslo > 0. Odpovídá internímu číselnému ID součástky v databázi. + + + part_mpnr + Volitelné + Řetězec + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_ipn nebo part_name. + + + part_ipn + Volitelné + Řetězec + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_mpnr nebo part_name. + + + part_name + Volitelné + Řetězec + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_mpnr nebo part_ipn. + + + part_description + Volitelné + Řetězec + Bude přeneseno do součástky a přepíše aktuální hodnotu, pokud je uveden neprázdný text. + + + Sloupce začínající part_manufacturer_ + + Pokud má být výrobce upraven nebo součástka jednoznačně identifikována pomocí part_mpnr, jeden z následujících sloupců musí být uveden a vyplněn: +
          +
        • part_manufacturer_id
        • +
        • part_manufacturer_name
        • +
        + + + + part_manufacturer_id + Volitelné + Celé číslo (Integer) + Celé číslo > 0. Odpovídá internímu číselnému ID výrobce. + + + part_manufacturer_name + Volitelné + Řetězec + Musí být uvedeno, pokud není vyplněn sloupec part_manufacturer_id. + + + Sloupce začínající part.category_ + + Pokud má být kategorie upravena, jeden z následujících sloupců musí být uveden a vyplněn: +
          +
        • part_category_id
        • +
        • part_category_name
        • +
        + + + + part_category_id + Volitelné + Celé číslo (Integer) + Celé číslo > 0. Odpovídá internímu číselnému ID kategorie součástky. + + + part_category_name + Volitelné + Řetězec + Musí být uvedeno, pokud není vyplněn sloupec part_category_id. diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 2461f8c77..4322fd9f8 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -12598,6 +12598,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver JSON for en samling
        + + + assembly.bom_import.type.csv + CSV til en samling + + assembly.bom_import.type.kicad_pcbnew @@ -12622,6 +12628,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver JSON-importskabelon til en samling + + + assembly.import_bom.template.header.csv + Importskabelon CSV til en samling + + assembly.import_bom.template.header.kicad_pcbnew @@ -12681,64 +12693,66 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver quantity - Påkrævet - Flydende tal (Float) - Skal være til stede og indeholde en flydende værdi (Float), der er større end 0,0. + Påkrævet felt + Flydende punkt-tal (Float) + Skal være udfyldt og indeholde en flydende værdi (Float), der er større end 0.0. name - Valgfri - String - Hvis til stede, skal det være en ikke-tom streng. + Valgfrit + Streng + Hvis det er angivet, skal det være en ikke-tom streng. Navn på posten inden for samlingen. part - Valgfri + Valgfrit Objekt/Array - Hvis angivet, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: + Hvis en del skal tildeles, skal det være et objekt/array, og mindst et af følgende felter skal være udfyldt:
        • part.id
        • +
        • part.mpnr
        • +
        • part.ipn
        • part.name
        part.id - Valgfri + Valgfrit Heltal (Integer) - Heltal (Integer) > 0. Matcher Part-DB's interne numeriske ID for komponenten. - - - part.name - Valgfri - String - Ikke-tom streng, hvis part.mpnr eller part.ipn ikke er givet. + Heltal > 0. Tilsvarer den interne nummer-ID for delen i database. part.mpnr - Valgfri - String - Ikke-tom streng, hvis part.name eller part.ipn ikke er givet. + Valgfrit + Streng + Ikke-tom streng, hvis ingen part.id-, part.ipn- eller part.name-værdi er angivet. part.ipn - Valgfri - String - Ikke-tom streng, hvis part.name eller part.mpnr ikke er givet. + Valgfrit + Streng + Ikke-tom streng, hvis ingen part.id-, part.mpnr- eller part.name-værdi er angivet. + + + part.name + Valgfrit + Streng + Ikke-tom streng, hvis ingen part.id-, part.mpnr- eller part.ipn-værdi er angivet. part.description - Valgfri - String eller null - Hvis til stede, skal det være en ikke-tom streng eller null. + Valgfrit + Streng eller null + Hvis angivet, skal det være en ikke-tom streng eller null. Værdien bliver overskrevet i delen. part.manufacturer - Valgfri + Valgfrit Objekt/Array - Hvis til stede, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: + Hvis producenten af en del skal ændres eller entydigt søges ved hjælp af part.mpnr-værdien, skal det være et objekt/array og mindst et af følgende felter skal være udfyldt:
        • manufacturer.id
        • manufacturer.name
        • @@ -12747,22 +12761,22 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver manufacturer.id - Valgfri + Valgfrit Heltal (Integer) - Heltal (Integer) > 0. Matcher producentens interne numeriske ID. + Heltal > 0. Tilsvarer den interne nummer-ID for producenten. manufacturer.name - Valgfri - String - Ikke-tom streng, hvis manufacturer.id ikke er givet. + Valgfrit + Streng + Ikke-tom streng, hvis ingen manufacturer.id er angivet. part.category - Valgfri + Valgfrit Objekt/Array - Hvis til stede, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: + Hvis en delens kategori skal ændres, skal det være et objekt/array, og mindst et af følgende felter skal være udfyldt:
          • category.id
          • category.name
          • @@ -12771,15 +12785,140 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver category.id - Valgfri + Valgfrit Heltal (Integer) - Heltal (Integer) > 0. Matcher komponentkategoriens interne numeriske ID. + Heltal > 0. Tilsvarer den interne nummer-ID for delens kategori. category.name - Valgfri - String - Ikke-tom streng, hvis category.id ikke er givet. + Valgfrit + Streng + Ikke-tom streng, hvis ingen category.id er angivet. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Mulige kolonner: + + + + + assembly.bom_import.template.csv.table + + + + + Kolonne + Betingelse + Datatype + Beskrivelse + + + + + quantity + Påkrævet felt + Flydende punkt-tal (Float) + Skal være udfyldt og indeholde en flydende værdi (Float), der er større end 0.0. + + + name + Valgfrit + Streng + Navnet på posten inden for samlingen. + + + Kolonner, der starter med part_ + + Hvis en del skal tildeles, skal en af følgende kolonner være angivet og udfyldt: +
              +
            • part_id
            • +
            • part_mpnr
            • +
            • part_ipn
            • +
            • part_name
            • +
            + + + + part_id + Valgfrit + Heltal (Integer) + Heltal > 0. Tilsvarer den interne nummer-ID for delen i databasen. + + + part_mpnr + Valgfrit + Streng + Skal angives, hvis ingen af kolonnerne part_id, part_ipn, eller part_name er udfyldt. + + + part_ipn + Valgfrit + Streng + Skal angives, hvis ingen af kolonnerne part_id, part_mpnr eller part_name er udfyldt. + + + part_name + Valgfrit + Streng + Skal angives, hvis ingen af kolonnerne part_id, part_mpnr eller part_ipn er udfyldt. + + + part_description + Valgfrit + Streng + Vil blive overført og overskrive værdien for delen, hvis en ikke-tom streng er angivet. + + + Kolonner, der starter med part_manufacturer_ + + Hvis producenten for en del skal ændres eller søges entydigt ved hjælp af part_mpnr, skal en af følgende kolonner være angivet og udfyldt: +
              +
            • part_manufacturer_id
            • +
            • part_manufacturer_name
            • +
            + + + + part_manufacturer_id + Valgfrit + Heltal (Integer) + Heltal > 0. Tilsvarer den interne nummer-ID for producenten. + + + part_manufacturer_name + Valgfrit + Streng + Skal angives, hvis ingen part_manufacturer_id er udfyldt. + + + Kolonner, der starter med part.category_ + + Hvis en dels kategori skal ændres, skal en af følgende kolonner være angivet og udfyldt: +
              +
            • part_category_id
            • +
            • part_category_name
            • +
            + + + + part_category_id + Valgfrit + Heltal (Integer) + Heltal > 0. Tilsvarer den interne nummer-ID for delens kategori. + + + part_category_name + Valgfrit + Streng + Skal angives, hvis ingen part_category_id er udfyldt. diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index bc0cac33f..8377facc9 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -12986,7 +12986,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön assembly.edit.bom.import_bom - Bauteile importieren + Importiere Bauteil-Liste @@ -13241,6 +13241,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön JSON für eine Baugruppe
            + + + assembly.bom_import.type.csv + CSV für eine Baugruppe + + assembly.bom_import.type.kicad_pcbnew @@ -13265,6 +13271,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Import-Vorlage JSON für eine Baugruppe + + + assembly.import_bom.template.header.csv + Import-Vorlage CSV für eine Baugruppe + + assembly.import_bom.template.header.kicad_pcbnew @@ -13332,16 +13344,18 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön name Optional String - Falls vorhanden, muss es ein nicht-leerer String sein. + Falls vorhanden, muss es ein nicht-leerer String sein. Name des Eintrags innerhalb der Baugruppe. part Optional Objekt/Array - Falls angegeben, muss es ein Objekt/Array sein und mindestens eines der Felder ausgefüllt sein: + Falls ein Bauteil zugeordnet werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein:
            • part.id
            • +
            • part.mpnr
            • +
            • part.ipn
            • part.name
            @@ -13353,35 +13367,35 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. - part.name + part.mpnr Optional String - Nicht-leerer String, falls keine part.mpnr- bzw. part.ipn-Angabe gegeben ist. + Nicht-leerer String, falls keine part.id-, part-ipn- bzw. part.name-Angabe gegeben ist. - part.mpnr + part.ipn Optional String - Nicht-leerer String, falls keine part.name- bzw. part-ipn-Angabe gegeben ist. + Nicht-leerer String, falls keine part.id-, part.mpnr bzw. part.name-Angabe gegeben ist. - part.ipn + part.name Optional String - Nicht-leerer String, falls keine part.name- bzw. part.mpnr-Angabe gegeben ist. + Nicht-leerer String, falls keine part.id-, part.mpnr- bzw. part.ipn-Angabe gegeben ist. part.description Optional String oder null - Falls vorhanden, muss es ein nicht-leerer String sein oder null. + Falls vorhanden, muss es ein nicht-leerer String sein oder null. Wird in das Bauteil übernommen, d.h. der dortige Wert überschrieben. part.manufacturer Optional Objekt/Array - Falls vorhanden, muss es ein Objekt/Array sein und mindestens eines der Felder ausgefüllt sein: + Falls der Hersteller eines Bauteils mit angepasst werden oder das Bauteil anhand der part.mpnr-Angabe eindeutig gesucht werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein:
            • manufacturer.id
            • manufacturer.name
            • @@ -13405,7 +13419,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Optional Objekt/Array - Falls vorhanden, muss es ein Objekt/Array sein und mindestens eines der Felder ausgefüllt sein: + Falls die Kategorie eine Bauteils mit angepasst werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein:
              • category.id
              • category.name
              • @@ -13430,6 +13444,131 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön + + + assembly.bom_import.template.csv.exptected_columns + Mögliche Spalten: + + + + + assembly.bom_import.template.csv.table + + + + + Spalte + Bedingung + Datentyp + Beschreibung + + + + + quantity + Pflichtfeld + Gleitkommazahl (Float) + Muss gegeben sein und enthält einen Gleitkommawert (Float), der größer als 0.0 ist. + + + name + Optional + String + Name des Eintrags innerhalb der Baugruppe. + + + Spalten beginnend mit part_ + + Falls ein Bauteil zugeordnet werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
                  +
                • part_id
                • +
                • part_mpnr
                • +
                • part_ipn
                • +
                • part_name
                • +
                + + + + part_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. + + + part_mpnr + Optional + String + Anzugeben, falls keine part_id-, part_ipn- bzw. part_name-Spalte ausgefüllt gegeben ist. + + + part_ipn + Optional + String + Anzugeben, falls keine part_id-, part_mpnr- bzw. part_name-Spalte ausgefüllt gegeben ist. + + + part_name + Optional + String + Anzugeben, falls keine part_id-, part_mpnr- bzw. part_ipn-Spalte ausgefüllt gegeben ist. + + + part_description + Optional + String + Wird in das Bauteil übernommen, d.h. der dortige Wert überschrieben sofern ein nicht-leerer String gegeben ist. + + + Spalten beginnend mit part_manufacturer_ + + Falls der Hersteller eines Bauteils mit angepasst werden oder das Bauteil anhand der part_mpnr-Angabe eindeutig gesucht werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
                  +
                • part_manufacturer_id
                • +
                • part_manufacturer_name
                • +
                + + + + part_manufacturer_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID des Herstellers. + + + part_manufacturer_name + Optional + String + Anzugeben, falls keine part_manufacturer_id-Spalte ausgefüllt gegeben ist. + + + Spalten beginnend mit part.category_ + + Falls die Kategorie eines Bauteils mit angepasst werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
                  +
                • part_category_id
                • +
                • part_category_name
                • +
                + + + + part_category_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID der Kategorie des Bauteils. + + + part_category_name + Optional + String + Anzugeben, falls keine part_category_id-Spalte ausgefüllt gegeben ist. + + + + ]]> +
                +
                +
                assembly.bom_import.template.kicad_pcbnew.exptected_columns diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index b54131896..c97f06304 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1925,6 +1925,12 @@ JSON για συναρμολόγηση + + + assembly.bom_import.type.csv + CSV για μια συναρμολόγηση + + assembly.bom_import.type.kicad_pcbnew @@ -1949,6 +1955,12 @@ Πρότυπο εισαγωγής JSON για συναρμολόγηση + + + assembly.import_bom.template.header.csv + Πρότυπο CSV εισαγωγής για μια συναρμολόγηση + + assembly.import_bom.template.header.kicad_pcbnew @@ -2001,7 +2013,7 @@ Πεδίο Προϋπόθεση - Τύπος Δεδομένων + Τύπος δεδομένων Περιγραφή @@ -2010,22 +2022,24 @@ quantity Υποχρεωτικό πεδίο Αριθμός κινητής υποδιαστολής (Float) - Πρέπει να παρέχεται και να περιέχει τιμή κινητής υποδιαστολής (Float) μεγαλύτερη από 0.0. + Πρέπει να είναι συμπληρωμένος και να περιέχει μια αριθμητική τιμή (Float) μεγαλύτερη από 0.0. name Προαιρετικό - Κείμενο (String) - Εάν υπάρχει, πρέπει να είναι μη κενό κείμενο. + Χαρακτηριστική ακολουθία (String) + Αν υπάρχει, πρέπει να είναι μη κενό κείμενο. Το όνομα του είδους μέσα στη συλλογή. part Προαιρετικό Αντικείμενο/Πίνακας - Εάν παρέχεται, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα πεδία του να είναι συμπληρωμένο: + Αν πρόκειται να ανατεθεί ένα εξάρτημα, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα παρακάτω πεδία πρέπει να έχει συμπληρωθεί:
                • part.id
                • +
                • part.mpnr
                • +
                • part.ipn
                • part.name
                @@ -2034,38 +2048,38 @@ part.id Προαιρετικό Ακέραιος αριθμός (Integer) - Ακέραιος (Integer) > 0. Αντιστοιχεί στην εσωτερική αριθμητική ταυτότητα (ID) του εξαρτήματος στη βάση δεδομένων. + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID του εξαρτήματος στη βάση δεδομένων. - part.name + part.mpnr Προαιρετικό - Κείμενο (String) - Μη κενό κείμενο, εάν δεν παρέχονται οι ενδείξεις part.mpnr ή part.ipn. + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το part.id, part.ipn ή part.name. - part.mpnr + part.ipn Προαιρετικό - Κείμενο (String) - Μη κενό κείμενο, εάν δεν παρέχονται οι ενδείξεις part.name ή part.ipn. + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το part.id, part.mpnr ή part.name. - part.ipn + part.name Προαιρετικό - Κείμενο (String) - Μη κενό κείμενο, εάν δεν παρέχονται οι ενδείξεις part.name ή part.mpnr. + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το part.id, part.mpnr ή part.ipn. part.description Προαιρετικό - Κείμενο ή null - Εάν υπάρχει, πρέπει να είναι μη κενό κείμενο, ή null. + Χαρακτηριστική ακολουθία ή null + Αν υπάρχει, πρέπει να είναι μη κενό κείμενο ή null. Υπερισχύει της υπάρχουσας τιμής στο εξάρτημα. part.manufacturer Προαιρετικό Αντικείμενο/Πίνακας - Εάν υπάρχει, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα πεδία του να είναι συμπληρωμένο: + Αν ο κατασκευαστής ενός εξαρτήματος χρειάζεται να αλλάξει ή να αναζητηθεί μονοσήμαντα μέσω της τιμής part.mpnr, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα παρακάτω πεδία να είναι συμπληρωμένα:
                • manufacturer.id
                • manufacturer.name
                • @@ -2076,20 +2090,20 @@ manufacturer.id Προαιρετικό Ακέραιος αριθμός (Integer) - Ακέραιος (Integer) > 0. Αντιστοιχεί στην εσωτερική αριθμητική ταυτότητα (ID) του κατασκευαστή. + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID του κατασκευαστή. manufacturer.name Προαιρετικό - Κείμενο (String) - Μη κενό κείμενο, εάν δεν παρέχεται η ένδειξη manufacturer.id. + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το manufacturer.id. part.category Προαιρετικό Αντικείμενο/Πίνακας - Εάν υπάρχει, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα πεδία του να είναι συμπληρωμένο: + Αν χρειάζεται να τροποποιηθεί η κατηγορία του εξαρτήματος, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα παρακάτω πεδία να είναι συμπληρωμένα:
                  • category.id
                  • category.name
                  • @@ -2100,13 +2114,138 @@ category.id Προαιρετικό Ακέραιος αριθμός (Integer) - Ακέραιος (Integer) > 0. Αντιστοιχεί στην εσωτερική αριθμητική ταυτότητα (ID) της κατηγορίας του εξαρτήματος. + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID της κατηγορίας εξαρτήματος. category.name Προαιρετικό - Κείμενο (String) - Μη κενό κείμενο, εάν δεν παρέχεται η ένδειξη category.id. + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το category.id. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Δυνατές στήλες: + + + + + assembly.bom_import.template.csv.table + + + + + Στήλη + Προϋπόθεση + Τύπος δεδομένων + Περιγραφή + + + + + quantity + Υποχρεωτικό πεδίο + Αριθμός κινητής υποδιαστολής (Float) + Πρέπει να είναι συμπληρωμένος και να περιέχει μια αριθμητική τιμή (Float) μεγαλύτερη από 0.0. + + + name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Το όνομα του είδους μέσα στη συλλογή. + + + Στήλες που ξεκινούν με part_ + + Αν χρειάζεται να αποδοθεί εξάρτημα, πρέπει να συμπληρωθεί μία από τις παρακάτω στήλες: +
                      +
                    • part_id
                    • +
                    • part_mpnr
                    • +
                    • part_ipn
                    • +
                    • part_name
                    • +
                    + + + + part_id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID του εξαρτήματος στη βάση δεδομένων. + + + part_mpnr + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσουν οι part_id, part_ipn ή part_name. + + + part_ipn + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσουν οι part_id, part_mpnr ή part_name. + + + part_name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσουν οι part_id, part_mpnr ή part_ipn. + + + part_description + Προαιρετικό + Χαρακτηριστική ακολουθία + Θα μεταφερθεί και θα αντικαταστήσει την τιμή στο εξάρτημα, αν δοθεί μια μη κενή ακολουθία. + + + Στήλες που ξεκινούν με part_manufacturer_ + + Αν ο κατασκευαστής του εξαρτήματος πρέπει να αλλάξει ή να αναζητηθεί μονοσήμαντα μέσω της part_mpnr, πρέπει να συμπληρωθεί μία από τις παρακάτω στήλες: +
                      +
                    • part_manufacturer_id
                    • +
                    • part_manufacturer_name
                    • +
                    + + + + part_manufacturer_id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID του κατασκευαστή. + + + part_manufacturer_name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσει το πεδίο part_manufacturer_id. + + + Στήλες που ξεκινούν με part.category_ + + Αν η κατηγορία του εξαρτήματος πρέπει να αλλάξει, πρέπει να συμπληρωθεί μία από τις παρακάτω στήλες: +
                      +
                    • part_category_id
                    • +
                    • part_category_name
                    • +
                    + + + + part_category_id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID της κατηγορίας του εξαρτήματος. + + + part_category_name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσει το πεδίο part_category_id. diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 8ac704001..f8d762f2f 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12987,7 +12987,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g assembly.edit.bom.import_bom - Import BOM + Import part list @@ -13191,7 +13191,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g assembly.import_bom - Import BOM for project + Import part list for assembly @@ -13239,7 +13239,13 @@ Please note, that you can not impersonate a disabled user. If you try you will g assembly.bom_import.type.json - JSON for one assembly + JSON for an assembly + + + + + assembly.bom_import.type.csv + CSV for an assembly @@ -13266,6 +13272,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Import template JSON format for one assembly
                    + + + assembly.import_bom.template.header.csv + Import template CSV format for one assembly + + assembly.import_bom.template.header.kicad_pcbnew @@ -13318,31 +13330,33 @@ Please note, that you can not impersonate a disabled user. If you try you will g Field Condition - Data type + Data Type Description quantity - Required - Floating point (Float) - Must be provided and contains a floating-point value (Float) greater than 0.0. + Mandatory + Floating point number (Float) + Must be provided and contains a floating point value (Float), which is greater than 0.0. name Optional String - If present, it must be a non-empty string. + If present, it must be a non-empty string. Name of the entry within the assembly. part Optional Object/Array - If provided, it must be an object/array and at least one of the fields must be filled: + If a part is to be associated, it must be an object/array with at least one of the following fields filled out:
                    • part.id
                    • +
                    • part.mpnr
                    • +
                    • part.ipn
                    • part.name
                    @@ -13351,38 +13365,38 @@ Please note, that you can not impersonate a disabled user. If you try you will g part.id Optional Integer - Integer > 0. Matches the Part-DB internal numeric ID of the component. + Integer > 0. Corresponds to the part database internal numeric ID of the part. - part.name + part.mpnr Optional String - Non-empty string if no part.mpnr or part.ipn is provided. + Non-empty string, if no part.id, part.ipn, or part.name is provided. - part.mpnr + part.ipn Optional String - Non-empty string if no part.name or part.ipn is provided. + Non-empty string, if no part.id, part.mpnr, or part.name is provided. - part.ipn + part.name Optional String - Non-empty string if no part.name or part.mpnr is provided. + Non-empty string, if no part.id, part.mpnr, or part.ipn is provided. part.description Optional String or null - If present, it must be a non-empty string or null. + If provided, it must be a non-empty string or null. It will be transferred to the part, overwriting its current value. part.manufacturer Optional Object/Array - If present, it must be an object/array and at least one of the fields must be filled: + If the manufacturer of a part is to be adjusted or the part is to be uniquely identified using the part.mpnr, it must be an object/array with at least one of the following fields filled out:
                    • manufacturer.id
                    • manufacturer.name
                    • @@ -13393,20 +13407,20 @@ Please note, that you can not impersonate a disabled user. If you try you will g manufacturer.id Optional Integer - Integer > 0. Matches the internal numeric ID of the manufacturer. + Integer > 0. Corresponds to the internal numeric ID of the manufacturer. manufacturer.name Optional String - Non-empty string if no manufacturer.id is provided. + Non-empty string, if no manufacturer.id is provided. part.category Optional Object/Array - If present, it must be an object/array and at least one of the fields must be filled: + If the category of a part is to be adjusted, it must be an object/array with at least one of the following fields filled out:
                      • category.id
                      • category.name
                      • @@ -13417,13 +13431,138 @@ Please note, that you can not impersonate a disabled user. If you try you will g category.id Optional Integer - Integer > 0. Matches the internal numeric ID of the component's category. + Integer > 0. Corresponds to the internal numeric ID of the part category. category.name Optional String - Non-empty string if no category.id is provided. + Non-empty string, if no category.id is provided. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Possible columns: + + + + + assembly.bom_import.template.csv.table + + + + + Column + Condition + Data Type + Description + + + + + quantity + Mandatory + Floating point number (Float) + Must be provided and contains a floating point value (Float), which is greater than 0.0. + + + name + Optional + String + Name of the entry within the assembly. + + + Columns starting with part_ + + If a part is to be associated, one of the following columns must be provided and filled out: +
                          +
                        • part_id
                        • +
                        • part_mpnr
                        • +
                        • part_ipn
                        • +
                        • part_name
                        • +
                        + + + + part_id + Optional + Integer + Integer > 0. Corresponds to the part database internal numeric ID of the part. + + + part_mpnr + Optional + String + Must be provided if no part_id, part_ipn, or part_name column is filled out. + + + part_ipn + Optional + String + Must be provided if no part_id, part_mpnr, or part_name column is filled out. + + + part_name + Optional + String + Must be provided if no part_id, part_mpnr, or part_ipn column is filled out. + + + part_description + Optional + String + Will be transferred to the part, overwriting its current value if a non-empty string is provided. + + + Columns starting with part_manufacturer_ + + If the manufacturer of a part is to be adjusted or the part is to be uniquely identified using the part_mpnr, one of the following columns must be provided and filled out: +
                          +
                        • part_manufacturer_id
                        • +
                        • part_manufacturer_name
                        • +
                        + + + + part_manufacturer_id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the manufacturer. + + + part_manufacturer_name + Optional + String + Must be provided if no part_manufacturer_id column is filled out. + + + Columns starting with part.category_ + + If the category of a part is to be adjusted, one of the following columns must be provided and filled out: +
                          +
                        • part_category_id
                        • +
                        • part_category_name
                        • +
                        + + + + part_category_id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the part category. + + + part_category_name + Optional + String + Must be provided if no part_category_id column is filled out. diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 7315f3760..1ee1188e4 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -12758,6 +12758,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S JSON para un ensamblaje
                        + + + assembly.bom_import.type.csv + CSV para un ensamblaje + + assembly.bom_import.type.kicad_pcbnew @@ -12782,6 +12788,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Plantilla de importación JSON para un ensamblaje + + + assembly.import_bom.template.header.csv + Plantilla de importación CSV para un ensamblaje + + assembly.import_bom.template.header.kicad_pcbnew @@ -12834,31 +12846,33 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Campo Condición - Tipo de dato + Tipo de datos Descripción quantity - Obligatorio - Número decimal (Float) - Debe estar presente y contener un valor decimal (Float) mayor que 0.0. + Campo obligatorio + Número de punto flotante (Float) + Debe completarse y contener un valor flotante (Float) mayor a 0.0. name Opcional - Cadena de texto (String) - Si está presente, debe ser una cadena de texto no vacía. + Cadena + Si se especifica, debe ser una cadena no vacía. El nombre del elemento dentro del conjunto. part Opcional Objeto/Array - Si se proporciona, debe ser un objeto/array y al menos uno de los campos debe estar completado: + Si se debe asignar una pieza, debe ser un objeto/array, y al menos uno de los siguientes campos debe completarse:
                        • part.id
                        • +
                        • part.mpnr
                        • +
                        • part.ipn
                        • part.name
                        @@ -12867,38 +12881,38 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S part.id Opcional Entero (Integer) - Entero (Integer) > 0. Corresponde al ID numérico interno del componente en la base de datos. + Entero > 0. Corresponde al ID interno numérico de la pieza en la base de datos. - part.name + part.mpnr Opcional - Cadena de texto (String) - Cadena de texto no vacía, si no se proporciona part.mpnr o part.ipn. + Cadena + Cadena no vacía si no se rellenan los campos part.id, part.ipn o part.name. - part.mpnr + part.ipn Opcional - Cadena de texto (String) - Cadena de texto no vacía, si no se proporciona part.name o part.ipn. + Cadena + Cadena no vacía si no se rellenan los campos part.id, part.mpnr o part.name. - part.ipn + part.name Opcional - Cadena de texto (String) - Cadena de texto no vacía, si no se proporciona part.name o part.mpnr. + Cadena + Cadena no vacía si no se rellenan los campos part.id, part.mpnr o part.ipn. part.description Opcional - Cadena de texto (String) o null - Si está presente, debe ser una cadena de texto no vacía o null. + Cadena o null + Si se especifica, debe ser una cadena no vacía o null. Este valor sobrescribirá el existente en la pieza. part.manufacturer Opcional Objeto/Array - Si está presente, debe ser un objeto/array y al menos uno de los campos debe estar completado: + Si se desea cambiar el fabricante de la pieza o buscarlo de forma única utilizando el valor part.mpnr, debe ser un objeto/array y al menos uno de los siguientes campos debe completarse:
                        • manufacturer.id
                        • manufacturer.name
                        • @@ -12909,20 +12923,20 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S manufacturer.id Opcional Entero (Integer) - Entero (Integer) > 0. Corresponde al ID numérico interno del fabricante. + Entero > 0. Corresponde al ID interno numérico del fabricante. manufacturer.name Opcional - Cadena de texto (String) - Cadena de texto no vacía, si no se proporciona manufacturer.id. + Cadena + Cadena no vacía si no se proporciona el campo manufacturer.id. part.category Opcional Objeto/Array - Si está presente, debe ser un objeto/array y al menos uno de los campos debe estar completado: + Si se desea cambiar la categoría de la pieza, debe ser un objeto/array y al menos uno de los siguientes campos debe completarse:
                          • category.id
                          • category.name
                          • @@ -12933,13 +12947,138 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S category.id Opcional Entero (Integer) - Entero (Integer) > 0. Corresponde al ID numérico interno de la categoría del componente. + Entero > 0. Corresponde al ID interno numérico de la categoría de la pieza. category.name Opcional - Cadena de texto (String) - Cadena de texto no vacía, si no se proporciona category.id. + Cadena + Cadena no vacía si no se proporciona el campo category.id. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Columnas posibles: + + + + + assembly.bom_import.template.csv.table + + + + + Columna + Condición + Tipo de datos + Descripción + + + + + quantity + Campo obligatorio + Número de punto flotante (Float) + Debe completarse y contener un valor flotante (Float) mayor a 0.0. + + + name + Opcional + Cadena + El nombre del elemento dentro del conjunto. + + + Columnas que comienzan con part_ + + Si se debe asignar una pieza, al menos una de las siguientes columnas debe completarse: +
                              +
                            • part_id
                            • +
                            • part_mpnr
                            • +
                            • part_ipn
                            • +
                            • part_name
                            • +
                            + + + + part_id + Opcional + Entero (Integer) + Entero > 0. Corresponde al ID interno numérico de la pieza en la base de datos. + + + part_mpnr + Opcional + Cadena + Debe completarse si no se han rellenado las columnas part_id, part_ipn o part_name. + + + part_ipn + Opcional + Cadena + Debe completarse si no se han rellenado las columnas part_id, part_mpnr o part_name. + + + part_name + Opcional + Cadena + Debe completarse si no se han rellenado las columnas part_id, part_mpnr o part_ipn. + + + part_description + Opcional + Cadena + Se transferirá y reemplazará el valor de la descripción de la pieza si se proporciona una cadena no vacía. + + + Columnas que comienzan con part_manufacturer_ + + Si el fabricante de la pieza debe cambiarse o buscarse exclusivamente mediante part_mpnr, al menos una de las siguientes columnas debe completarse: +
                              +
                            • part_manufacturer_id
                            • +
                            • part_manufacturer_name
                            • +
                            + + + + part_manufacturer_id + Opcional + Entero (Integer) + Entero > 0. Corresponde al ID interno numérico del fabricante. + + + part_manufacturer_name + Opcional + Cadena + Debe completarse si no se ha proporcionado el campo part_manufacturer_id. + + + Columnas que comienzan con part.category_ + + Si se necesita modificar la categoría de la pieza, al menos una de las siguientes columnas debe completarse: +
                              +
                            • part_category_id
                            • +
                            • part_category_name
                            • +
                            + + + + part_category_id + Opcional + Entero (Integer) + Entero > 0. Corresponde al ID interno numérico de la categoría de la pieza. + + + part_category_name + Opcional + Cadena + Debe completarse si no se ha proporcionado el campo part_category_id. diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 320121909..9cd293219 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -9487,6 +9487,12 @@ exemple de ville
                            JSON pour un assemblage
                            + + + assembly.bom_import.type.csv + CSV pour un assemblage + + assembly.bom_import.type.kicad_pcbnew @@ -9511,6 +9517,12 @@ exemple de ville Modèle d’importation JSON pour un assemblage + + + assembly.import_bom.template.header.csv + Modèle d'importation CSV pour un assemblage + + assembly.import_bom.template.header.kicad_pcbnew @@ -9570,24 +9582,26 @@ exemple de ville quantity - Obligatoire - Nombre décimal (Float) - Doit être présent et contenir une valeur décimale (Float) supérieure à 0,0. + Champ obligatoire + Nombre à virgule flottante (Float) + Doit être rempli et contenir une valeur décimale (Float) supérieure à 0.0. name Optionnel - Chaîne (String) - Si présent, doit être une chaîne non vide. + Chaîne + Si renseigné, doit être une chaîne non vide. Le nom de l'élément dans l'assemblage. part Optionnel Objet/Tableau - Si fourni, doit être un objet/un tableau et au moins un des champs doit être rempli : + Si une pièce doit être assignée, cela doit être un objet/tableau et au moins un des champs suivants doit être renseigné :
                            • part.id
                            • +
                            • part.mpnr
                            • +
                            • part.ipn
                            • part.name
                            @@ -9596,38 +9610,38 @@ exemple de ville part.id Optionnel Entier (Integer) - Entier (Integer) > 0. Correspond à l'ID numérique interne dans Part-DB du composant. + Nombre entier > 0. Correspond à l'ID interne numérique de la pièce dans la base de données. - part.name + part.mpnr Optionnel - Chaîne (String) - Chaîne non vide si part.mpnr ou part.ipn ne sont pas fournis. + Chaîne + Chaîne non vide si part.id, part.ipn ou part.name ne sont pas renseignés. - part.mpnr + part.ipn Optionnel - Chaîne (String) - Chaîne non vide si part.name ou part.ipn ne sont pas fournis. + Chaîne + Chaîne non vide si part.id, part.mpnr ou part.name ne sont pas renseignés. - part.ipn + part.name Optionnel - Chaîne (String) - Chaîne non vide si part.name ou part.mpnr ne sont pas fournis. + Chaîne + Chaîne non vide si part.id, part.mpnr ou part.ipn ne sont pas renseignés. part.description Optionnel Chaîne ou null - Si présent, doit être une chaîne non vide ou null. + Si renseignée, doit être une chaîne non vide ou null. Ce champ remplacera la valeur existante de la pièce. part.manufacturer Optionnel Objet/Tableau - Si présent, doit être un objet/un tableau et au moins un des champs doit être rempli : + Si le fabricant de la pièce doit être modifié ou recherché de manière unique à l'aide de la valeur part.mpnr, cela doit être un objet/tableau et au moins un des champs suivants doit être renseigné :
                            • manufacturer.id
                            • manufacturer.name
                            • @@ -9638,20 +9652,20 @@ exemple de ville manufacturer.id Optionnel Entier (Integer) - Entier (Integer) > 0. Correspond à l'ID numérique interne du fabricant. + Nombre entier > 0. Correspond à l'ID interne numérique du fabricant. manufacturer.name Optionnel - Chaîne (String) - Chaîne non vide si manufacturer.id n'est pas fourni. + Chaîne + Chaîne non vide si manufacturer.id n'est pas renseigné. part.category Optionnel Objet/Tableau - Si présent, doit être un objet/un tableau et au moins un des champs doit être rempli : + Si la catégorie de la pièce doit être modifiée, cela doit être un objet/tableau et au moins un des champs suivants doit être renseigné :
                              • category.id
                              • category.name
                              • @@ -9662,13 +9676,138 @@ exemple de ville category.id Optionnel Entier (Integer) - Entier (Integer) > 0. Correspond à l'ID numérique interne de la catégorie du composant. + Nombre entier > 0. Correspond à l'ID interne numérique de la catégorie de la pièce. category.name Optionnel - Chaîne (String) - Chaîne non vide si category.id n'est pas fourni. + Chaîne + Chaîne non vide si category.id n'est pas renseigné. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Colonnes possibles : + + + + + assembly.bom_import.template.csv.table + + + + + Colonne + Condition + Type de données + Description + + + + + quantity + Champ obligatoire + Nombre à virgule flottante (Float) + Doit être rempli et contenir une valeur décimale (Float) supérieure à 0.0. + + + name + Optionnel + Chaîne + Le nom de l'élément dans l'assemblage. + + + Colonnes commençant par part_ + + Si une pièce doit être assignée, au moins une des colonnes suivantes doit être renseignée : +
                                  +
                                • part_id
                                • +
                                • part_mpnr
                                • +
                                • part_ipn
                                • +
                                • part_name
                                • +
                                + + + + part_id + Optionnel + Entier (Integer) + Nombre entier > 0. Correspond à l'ID interne numérique de la pièce dans la base de données. + + + part_mpnr + Optionnel + Chaîne + Doit être renseignée si les colonnes part_id, part_ipn ou part_name ne sont pas remplies. + + + part_ipn + Optionnel + Chaîne + Doit être renseignée si les colonnes part_id, part_mpnr ou part_name ne sont pas remplies. + + + part_name + Optionnel + Chaîne + Doit être renseignée si les colonnes part_id, part_mpnr ou part_ipn ne sont pas remplies. + + + part_description + Optionnel + Chaîne + Sera transférée et remplacera la valeur existante de la description si une chaîne non vide est fournie. + + + Colonnes commençant par part_manufacturer_ + + Si le fabricant de la pièce doit être modifié ou recherché uniquement via part_mpnr, au moins une des colonnes suivantes doit être renseignée : +
                                  +
                                • part_manufacturer_id
                                • +
                                • part_manufacturer_name
                                • +
                                + + + + part_manufacturer_id + Optionnel + Entier (Integer) + Nombre entier > 0. Correspond à l'ID interne numérique du fabricant. + + + part_manufacturer_name + Optionnel + Chaîne + Doit être renseignée si part_manufacturer_id n'est pas fourni. + + + Colonnes commençant par part.category_ + + Si la catégorie de la pièce doit être modifiée, au moins une des colonnes suivantes doit être renseignée : +
                                  +
                                • part_category_id
                                • +
                                • part_category_name
                                • +
                                + + + + part_category_id + Optionnel + Entier (Integer) + Nombre entier > 0. Correspond à l'ID interne numérique de la catégorie de la pièce. + + + part_category_name + Optionnel + Chaîne + Doit être renseignée si part_category_id n'est pas fourni. @@ -9679,75 +9818,75 @@ exemple de ville
                                assembly.bom_import.template.kicad_pcbnew.exptected_columns - Colonnes attendues : + Colonnes attendues: assembly.bom_import.template.kicad_pcbnew.exptected_columns.note - Remarque : Aucun mappage n'est effectué avec des composants spécifiques issus de la gestion des catégories.

                                - ]]> + Remarque: Aucun mappage n'est effectué avec des composants spécifiques issus de la gestion des catégories.

                                + ]]>
                                - - assembly.bom_import.template.kicad_pcbnew.table - - - - - Champ - Condition - Type de Données - Description - - - - - Id - Optionnel - Entier - Champ libre. Un numéro d'identification unique pour chaque composant. - - - Designator - Optionnel - Chaîne - Champ libre. Une désignation de référence unique pour le composant sur le PCB, par exemple, "R1" pour la résistance 1.
                                Utilisé comme le nom de l'emplacement de l'entrée composant dans l'assemblage. - - - Package - Optionnel - Chaîne - Champ libre. Le boîtier ou le format du composant, par exemple, "0805" pour les résistances CMS.
                                Non inclus dans l'entrée composant pour l'assemblage. - - - Quantity - Obligatoire - Entier - Le nombre de composants identiques nécessaires pour créer une instance de l'assemblage.
                                Utilisé comme la quantité dans l'entrée composant de l'assemblage. - - - Designation - Obligatoire - Chaîne - La description ou la fonction du composant, par exemple, valeur de résistance "10kΩ" ou valeur de condensateur "100nF".
                                Utilisé comme le nom dans l'entrée composant pour l'assemblage. - - - Supplier and ref - Optionnel - Chaîne - Champ libre. Peut contenir par exemple des informations spécifiques sur le fournisseur.
                                Utilisé comme une note dans l'entrée composant pour l'assemblage. - - - - ]]> -
                                -
                                + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Champ + Condition + Type de Données + Description + + + + + Id + Optionnel + Entier + Champ libre. Un numéro d'identification unique pour chaque composant. + + + Designator + Optionnel + Chaîne + Champ libre. Une désignation de référence unique pour le composant sur le PCB, par exemple, "R1" pour la résistance 1.
                                Utilisé comme le nom de l'emplacement de l'entrée composant dans l'assemblage. + + + Package + Optionnel + Chaîne + Champ libre. Le boîtier ou le format du composant, par exemple, "0805" pour les résistances CMS.
                                Non inclus dans l'entrée composant pour l'assemblage. + + + Quantity + Obligatoire + Entier + Le nombre de composants identiques nécessaires pour créer une instance de l'assemblage.
                                Utilisé comme la quantité dans l'entrée composant de l'assemblage. + + + Designation + Obligatoire + Chaîne + La description ou la fonction du composant, par exemple, valeur de résistance "10kΩ" ou valeur de condensateur "100nF".
                                Utilisé comme le nom dans l'entrée composant pour l'assemblage. + + + Supplier and ref + Optionnel + Chaîne + Champ libre. Peut contenir par exemple des informations spécifiques sur le fournisseur.
                                Utilisé comme une note dans l'entrée composant pour l'assemblage. + + + + ]]> +
                                +
                                diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index f6e3c9446..8e39c0313 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -12736,6 +12736,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a JSON per un gruppo + + + assembly.bom_import.type.csv + CSV per un'assemblaggio + + assembly.bom_import.type.kicad_pcbnew @@ -12760,6 +12766,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Template di importazione JSON per un gruppo + + + assembly.import_bom.template.header.csv + Modello di importazione CSV per un assemblaggio + + assembly.import_bom.template.header.kicad_pcbnew @@ -12812,31 +12824,33 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Campo Condizione - Tipo di dato + Tipo di dati Descrizione quantity - Obbligatorio + Campo obbligatorio Numero decimale (Float) - Deve essere presente e contenere un valore decimale (Float) maggiore di 0,0. + Deve essere compilato e contenere un valore decimale (Float) maggiore di 0.0. name Opzionale - Stringa (String) - Se presente, deve essere una stringa non vuota. + Stringa + Se specificato, deve essere una stringa non vuota. Il nome del componente all'interno dell'assemblaggio. part Opzionale Oggetto/Array - Se fornito, deve essere un oggetto/un array e almeno uno dei campi deve essere compilato: + Se è necessario assegnare una parte, deve essere un Oggetto/Array e almeno uno dei seguenti campi deve essere compilato:
                                • part.id
                                • +
                                • part.mpnr
                                • +
                                • part.ipn
                                • part.name
                                @@ -12844,39 +12858,39 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a part.id Opzionale - Intero (Integer) - Intero (Integer) > 0. Corrisponde all'ID numerico interno di Part-DB per il componente. + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico del componente nel database. - part.name + part.mpnr Opzionale - Stringa (String) - Stringa non vuota se part.mpnr o part.ipn non sono forniti. + Stringa + Stringa non vuota se i campi part.id, part.ipn o part.name non sono compilati. - part.mpnr + part.ipn Opzionale - Stringa (String) - Stringa non vuota se part.name o part.ipn non sono forniti. + Stringa + Stringa non vuota se i campi part.id, part.mpnr o part.name non sono compilati. - part.ipn + part.name Opzionale - Stringa (String) - Stringa non vuota se part.name o part.mpnr non sono forniti. + Stringa + Stringa non vuota se i campi part.id, part.mpnr o part.ipn non sono compilati. part.description Opzionale Stringa o null - Se presente, deve essere una stringa non vuota o null. + Se specificato, deve essere una stringa non vuota o null. Questo valore sovrascriverà quello esistente nella parte. part.manufacturer Opzionale Oggetto/Array - Se presente, deve essere un oggetto/un array e almeno uno dei campi deve essere compilato: + Se il produttore della parte deve essere cambiato o ricercato esclusivamente utilizzando il valore part.mpnr, deve essere un Oggetto/Array e almeno uno dei seguenti campi deve essere compilato:
                                • manufacturer.id
                                • manufacturer.name
                                • @@ -12886,21 +12900,21 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a manufacturer.id Opzionale - Intero (Integer) - Intero (Integer) > 0. Corrisponde all'ID numerico interno del produttore. + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico del produttore. manufacturer.name Opzionale - Stringa (String) - Stringa non vuota se manufacturer.id non è fornito. + Stringa + Stringa non vuota se il campo manufacturer.id non è compilato. part.category Opzionale Oggetto/Array - Se presente, deve essere un oggetto/un array e almeno uno dei campi deve essere compilato: + Se è necessario modificare la categoria della parte, deve essere un Oggetto/Array e almeno uno dei seguenti campi deve essere compilato:
                                  • category.id
                                  • category.name
                                  • @@ -12910,14 +12924,139 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a category.id Opzionale - Intero (Integer) - Intero (Integer) > 0. Corrisponde all'ID numerico interno della categoria del componente. + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico della categoria della parte. category.name Opzionale - Stringa (String) - Stringa non vuota se category.id non è fornito. + Stringa + Stringa non vuota se il campo category.id non è compilato. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Colonne possibili: + + + + + assembly.bom_import.template.csv.table + + + + + Colonna + Condizione + Tipo di dati + Descrizione + + + + + quantity + Campo obbligatorio + Numero decimale (Float) + Deve essere compilato e contenere un valore decimale (Float) maggiore di 0.0. + + + name + Opzionale + Stringa + Il nome dell'elemento all'interno dell'assemblaggio. + + + Colonne che iniziano con part_ + + Se è necessario assegnare una parte, almeno una delle colonne seguenti deve essere compilata: +
                                      +
                                    • part_id
                                    • +
                                    • part_mpnr
                                    • +
                                    • part_ipn
                                    • +
                                    • part_name
                                    • +
                                    + + + + part_id + Opzionale + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico del componente nel database. + + + part_mpnr + Opzionale + Stringa + Deve essere compilato se le colonne part_id, part_ipn o part_name non sono compilate. + + + part_ipn + Opzionale + Stringa + Deve essere compilato se le colonne part_id, part_mpnr o part_name non sono compilate. + + + part_name + Opzionale + Stringa + Deve essere compilato se le colonne part_id, part_mpnr o part_ipn non sono compilate. + + + part_description + Opzionale + Stringa + Sarà trasferita e sostituirà il valore esistente della descrizione se viene fornita una stringa non vuota. + + + Colonne che iniziano con part_manufacturer_ + + Se il produttore del componente deve essere modificato o ricercato esclusivamente tramite part_mpnr, almeno una delle seguenti colonne deve essere compilata: +
                                      +
                                    • part_manufacturer_id
                                    • +
                                    • part_manufacturer_name
                                    • +
                                    + + + + part_manufacturer_id + Opzionale + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico del produttore. + + + part_manufacturer_name + Opzionale + Stringa + Deve essere compilata se il campo part_manufacturer_id non è fornito. + + + Colonne che iniziano con part_category_ + + Se è necessario modificare la categoria della parte, almeno una delle seguenti colonne deve essere compilata: +
                                      +
                                    • part_category_id
                                    • +
                                    • part_category_name
                                    • +
                                    + + + + part_category_id + Opzionale + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico della categoria del componente. + + + part_category_name + Opzionale + Stringa + Deve essere compilata se il campo part_category_id non è fornito. @@ -12942,61 +13081,61 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a
                                    - - assembly.bom_import.template.kicad_pcbnew.table - - - - - Campo - Condizione - Tipo di Dati - Descrizione - - - - - Id - Opzionale - Intero - Campo libero. Un numero identificativo univoco per ogni componente. - - - Designator - Opzionale - Stringa - Campo libero. Un riferimento univoco al componente su PCB, ad esempio "R1" per il resistore 1.
                                    Utilizzato come nome della posizione nella voce componenti all'interno dell'assemblaggio. - - - Package - Opzionale - Stringa - Campo libero. L'involucro o il fattore di forma del componente, ad esempio "0805" per i resistori SMD.
                                    Non incluso nelle informazioni del componente nell'assemblaggio. - - - Quantity - Obbligatorio - Intero - Il numero di componenti identici richiesti per creare un'istanza dell'assemblaggio.
                                    Utilizzato come quantità nella voce componenti dell'assemblaggio. - - - Designation - Obbligatorio - Stringa - Descrizione o funzione del componente, ad esempio valore resistore "10kΩ" o valore condensatore "100nF".
                                    Utilizzato come nome nella voce componenti dell'assemblaggio. - - - Supplier and ref - Opzionale - Stringa - Campo libero. Può contenere, ad esempio, informazioni specifiche del fornitore.
                                    Utilizzato come nota nelle informazioni del componente nell'assemblaggio. - - - - ]]> -
                                    -
                                    + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Campo + Condizione + Tipo di Dati + Descrizione + + + + + Id + Opzionale + Intero + Campo libero. Un numero identificativo univoco per ogni componente. + + + Designator + Opzionale + Stringa + Campo libero. Un riferimento univoco al componente su PCB, ad esempio "R1" per il resistore 1.
                                    Utilizzato come nome della posizione nella voce componenti all'interno dell'assemblaggio. + + + Package + Opzionale + Stringa + Campo libero. L'involucro o il fattore di forma del componente, ad esempio "0805" per i resistori SMD.
                                    Non incluso nelle informazioni del componente nell'assemblaggio. + + + Quantity + Obbligatorio + Intero + Il numero di componenti identici richiesti per creare un'istanza dell'assemblaggio.
                                    Utilizzato come quantità nella voce componenti dell'assemblaggio. + + + Designation + Obbligatorio + Stringa + Descrizione o funzione del componente, ad esempio valore resistore "10kΩ" o valore condensatore "100nF".
                                    Utilizzato come nome nella voce componenti dell'assemblaggio. + + + Supplier and ref + Opzionale + Stringa + Campo libero. Può contenere, ad esempio, informazioni specifiche del fornitore.
                                    Utilizzato come nota nelle informazioni del componente nell'assemblaggio. + + + + ]]> +
                                    +
                                    diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index a8f5f9151..fb64a0f04 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -9188,6 +9188,12 @@ Exampletown アセンブリ用 JSON + + + assembly.bom_import.type.csv + アセンブリ用のCSV + + assembly.bom_import.type.kicad_pcbnew @@ -9212,6 +9218,12 @@ Exampletown アセンブリ用 JSON テンプレート + + + assembly.import_bom.template.header.csv + アセンブリ用のCSVインポートテンプレート + + assembly.import_bom.template.header.kicad_pcbnew @@ -9271,24 +9283,26 @@ Exampletown quantity - 必須 + 必須項目 浮動小数点数 (Float) - 指定され、0.0より大きい浮動小数点値 (Float) を含む必要があります。 + 入力必須で、0.0よりも大きい浮動小数点数 (Float) を含む必要があります。 name 任意 - 文字列 (String) - 指定されている場合、空でない文字列でなければなりません。 + 文字列 + 指定されている場合、空でない文字列でなければなりません。アセンブリ内のアイテムの名前。 part 任意 オブジェクト/配列 - 指定された場合、オブジェクト/配列であり、以下のフィールドのうち少なくとも1つが入力されている必要があります: + 部品を割り当てる必要がある場合、これはオブジェクト/配列であり、次のフィールドのいずれかを少なくとも1つ入力する必要があります:
                                    • part.id
                                    • +
                                    • part.mpnr
                                    • +
                                    • part.ipn
                                    • part.name
                                    @@ -9297,38 +9311,38 @@ Exampletown part.id 任意 整数 (Integer) - 整数 (Integer) > 0。部品の Part-DB 内部数値 ID に対応します。 + 0より大きい整数。データベース内の部品の内部数値IDに対応します。 - part.name + part.mpnr 任意 - 文字列 (String) - part.mpnr または part.ipn が指定されていない場合、空でない文字列でなければなりません。 + 文字列 + part.id、part.ipn、または part.name が入力されていない場合、空でない文字列。 - part.mpnr + part.ipn 任意 - 文字列 (String) - part.name または part.ipn が指定されていない場合、空でない文字列でなければなりません。 + 文字列 + part.id、part.mpnr、または part.name が入力されていない場合、空でない文字列。 - part.ipn + part.name 任意 - 文字列 (String) - part.name または part.mpnr が指定されていない場合、空でない文字列でなければなりません。 + 文字列 + part.id、part.mpnr、または part.ipn が入力されていない場合、空でない文字列。 part.description 任意 文字列または null - 指定されている場合、空でない文字列または null でなければなりません。 + 指定されている場合、空でない文字列または null である必要があります。この値は部品の既存の値を上書きします。 part.manufacturer 任意 オブジェクト/配列 - 指定されている場合、オブジェクト/配列であり、以下のフィールドのうち少なくとも1つが入力されている必要があります: + 部品のメーカーを変更する場合、または part.mpnr の値を利用して一意に検索する場合、これはオブジェクト/配列であり、次のフィールドのいずれかを少なくとも1つ入力する必要があります:
                                    • manufacturer.id
                                    • manufacturer.name
                                    • @@ -9339,20 +9353,20 @@ Exampletown manufacturer.id 任意 整数 (Integer) - 整数 (Integer) > 0。製造元の内部数値 ID に対応します。 + 0より大きい整数。メーカーの内部数値IDに対応します。 manufacturer.name 任意 - 文字列 (String) - manufacturer.id が指定されていない場合、空でない文字列でなければなりません。 + 文字列 + manufacturer.id が提供されていない場合、空でない文字列。 part.category 任意 オブジェクト/配列 - 指定されている場合、オブジェクト/配列であり、以下のフィールドのうち少なくとも1つが入力されている必要があります: + 部品のカテゴリーを変更する場合、これはオブジェクト/配列であり、次のフィールドのいずれかを少なくとも1つ入力する必要があります:
                                      • category.id
                                      • category.name
                                      • @@ -9363,13 +9377,138 @@ Exampletown category.id 任意 整数 (Integer) - 整数 (Integer) > 0。コンポーネントカテゴリの内部数値 ID に対応します。 + 0より大きい整数。部品のカテゴリーに対応する内部数値ID。 category.name 任意 - 文字列 (String) - category.id が指定されていない場合、空でない文字列でなければなりません。 + 文字列 + category.id が提供されていない場合、空でない文字列。 + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + 可能なカラム: + + + + + assembly.bom_import.template.csv.table + + + + + カラム + 条件 + データ型 + 説明 + + + + + quantity + 必須項目 + 浮動小数点数 (Float) + 入力必須で、0.0よりも大きい浮動小数点数 (Float) を含む必要があります。 + + + name + 任意 + 文字列 + アセンブリ内のアイテムの名前。 + + + part_ で始まるカラム + + 部品を割り当てる必要がある場合、次のカラムのいずれかが少なくとも1つ入力されなければなりません: +
                                          +
                                        • part_id
                                        • +
                                        • part_mpnr
                                        • +
                                        • part_ipn
                                        • +
                                        • part_name
                                        • +
                                        + + + + part_id + 任意 + 整数 (Integer) + 0より大きい整数。データベース内の部品の内部数値ID。 + + + part_mpnr + 任意 + 文字列 + part_id、part_ipn、または part_name が入力されていない場合に入力される必要があります。 + + + part_ipn + 任意 + 文字列 + part_id、part_mpnr、または part_name が入力されていない場合に入力される必要があります。 + + + part_name + 任意 + 文字列 + part_id、part_mpnr、または part_ipn が入力されていない場合に入力される必要があります。 + + + part_description + 任意 + 文字列 + 指定されている場合、部品の説明既存の値を上書きする非空の文字列。 + + + part_manufacturer_ で始まるカラム + + 部品の製造元を変更する場合、または part_mpnr を利用して一意に検索する場合、次のカラムのいずれかを少なくとも1つ入力する必要があります: +
                                          +
                                        • part_manufacturer_id
                                        • +
                                        • part_manufacturer_name
                                        • +
                                        + + + + part_manufacturer_id + 任意 + 整数 (Integer) + 0より大きい整数。製造元の内部数値ID。 + + + part_manufacturer_name + 任意 + 文字列 + part_manufacturer_id が入力されていない場合、入力される必要があります。 + + + part_category_ で始まるカラム + + 部品のカテゴリーを変更する場合、次のカラムのいずれかを少なくとも1つ入力する必要があります: +
                                          +
                                        • part_category_id
                                        • +
                                        • part_category_name
                                        • +
                                        + + + + part_category_id + 任意 + 整数 (Integer) + 0より大きい整数。部品のカテゴリーに対応する内部数値ID。 + + + part_category_name + 任意 + 文字列 + part_category_id が提供されていない場合、入力される必要があります。 @@ -9384,71 +9523,71 @@ Exampletown
                                        - - assembly.bom_import.template.kicad_pcbnew.exptected_columns.note - - 注意: カテゴリ管理から特定のコンポーネントへのマッピングは行われません。

                                        - ]]> -
                                        -
                                        + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + 注意: カテゴリ管理から特定のコンポーネントへのマッピングは行われません。

                                        + ]]> +
                                        +
                                        - - assembly.bom_import.template.kicad_pcbnew.table - - - - - フィールド - 条件 - データ型 - 説明 - - - - - Id - 任意 - 整数 - 自由形式のフィールド。各コンポーネントのユニークな識別番号。 - - - Designator - 任意 - 文字列 - 自由形式のフィールド。PCB上のコンポーネントごとの一意のリファレンス識別子。例: 抵抗 "R1"。
                                        アセンブリ内の部品エントリの配置名として使用。 - - - Package - 任意 - 文字列 - 自由形式のフィールド。コンポーネントのパッケージまたはフォームファクタ。例: 表面実装抵抗器 "0805"。
                                        アセンブリ内の部品エントリ情報には含まれません。 - - - Quantity - 必須 - 整数 - アセンブリの一つのインスタンスを作るために必要な同一部品の数。
                                        アセンブリの部品情報で数量として使用。 - - - Designation - 必須 - 文字列 - コンポーネントの説明や機能。例: 抵抗の値 "10kΩ" やコンデンサの値 "100nF"。
                                        アセンブリの部品情報で名称として使用。 - - - Supplier and ref - 任意 - 文字列 - 自由形式フィールド。例: 供給元に関する特定の情報を含む場合がある。
                                        アセンブリの部品情報の注記として使用。 - - - - ]]> -
                                        -
                                        + + assembly.bom_import.template.kicad_pcbnew.table + + + + + フィールド + 条件 + データ型 + 説明 + + + + + Id + 任意 + 整数 + 自由形式のフィールド。各コンポーネントのユニークな識別番号。 + + + Designator + 任意 + 文字列 + 自由形式のフィールド。PCB上のコンポーネントごとの一意のリファレンス識別子。例: 抵抗 "R1"。
                                        アセンブリ内の部品エントリの配置名として使用。 + + + Package + 任意 + 文字列 + 自由形式のフィールド。コンポーネントのパッケージまたはフォームファクタ。例: 表面実装抵抗器 "0805"。
                                        アセンブリ内の部品エントリ情報には含まれません。 + + + Quantity + 必須 + 整数 + アセンブリの一つのインスタンスを作るために必要な同一部品の数。
                                        アセンブリの部品情報で数量として使用。 + + + Designation + 必須 + 文字列 + コンポーネントの説明や機能。例: 抵抗の値 "10kΩ" やコンデンサの値 "100nF"。
                                        アセンブリの部品情報で名称として使用。 + + + Supplier and ref + 任意 + 文字列 + 自由形式フィールド。例: 供給元に関する特定の情報を含む場合がある。
                                        アセンブリの部品情報の注記として使用。 + + + + ]]> +
                                        +
                                        diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index f98e9c784..7cca59753 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1150,6 +1150,12 @@ JSON voor assemblage + + + assembly.bom_import.type.csv + CSV voor een assemblage + + assembly.bom_import.type.kicad_pcbnew @@ -1174,6 +1180,12 @@ JSON-sjabloon voor assemblage + + + assembly.import_bom.template.header.csv + CSV-importsjabloon voor een assemblage + + assembly.import_bom.template.header.kicad_pcbnew @@ -1235,22 +1247,24 @@ quantity Verplicht veld Kommagetal (Float) - Moet opgegeven zijn en bevat een kommagetal (Float) dat groter is dan 0,0. + Moet worden ingevuld en een kommagetal (Float) bevatten dat groter is dan 0,0. name Optioneel - String - Indien aanwezig, moet het een niet-lege string zijn. + Tekst + Wanneer ingevuld, moet het een niet-lege tekst zijn. De naam van het item binnen de assemblage. part Optioneel Object/Array - Indien opgegeven, moet het een object/array zijn en ten minste één van de velden ingevuld zijn: + Als een onderdeel moet worden toegewezen, moet dit een object/array zijn en moet ten minste één van de volgende velden worden ingevuld:
                                        • part.id
                                        • +
                                        • part.mpnr
                                        • +
                                        • part.ipn
                                        • part.name
                                        @@ -1258,39 +1272,39 @@ part.id Optioneel - Geheel getal (Integer) - Geheel getal (Integer) > 0. Komt overeen met de interne numerieke ID van het onderdeel in de Part-DB. + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van het onderdeel in de database. - part.name + part.mpnr Optioneel - String - Niet-lege string, indien geen part.mpnr- of part.ipn-vermelding is gegeven. + Tekst + Niet-lege tekst, wanneer part.id, part.ipn of part.name niet zijn ingevuld. - part.mpnr + part.ipn Optioneel - String - Niet-lege string, indien geen part.name- of part.ipn-vermelding is gegeven. + Tekst + Niet-lege tekst wanneer part.id, part.mpnr of part.name niet zijn ingevuld. - part.ipn + part.name Optioneel - String - Niet-lege string, indien geen part.name- of part.mpnr-vermelding is gegeven. + Tekst + Niet-lege tekst, wanneer part.id, part.mpnr of part.ipn niet zijn ingevuld. part.description Optioneel - String of null - Indien aanwezig, moet het een niet-lege string zijn of null. + Tekst of null + Indien ingevuld, moet het een niet-lege tekst of null zijn. Deze waarde vervangt de bestaande waarde in het onderdeel. part.manufacturer Optioneel Object/Array - Indien aanwezig, moet het een object/array zijn en ten minste één van de velden ingevuld zijn: + Als de fabrikant van het onderdeel moet worden gewijzigd of uniek moet worden opgezocht met de waarde part.mpnr, moet dit een object/array zijn en moet ten minste één van de volgende velden worden ingevuld:
                                        • manufacturer.id
                                        • manufacturer.name
                                        • @@ -1300,21 +1314,21 @@ manufacturer.id Optioneel - Geheel getal (Integer) - Geheel getal (Integer) > 0. Komt overeen met de interne numerieke ID van de fabrikant. + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van de fabrikant. manufacturer.name Optioneel - String - Niet-lege string, indien geen manufacturer.id-vermelding is gegeven. + Tekst + Niet-lege tekst als manufacturer.id niet is ingevuld. part.category Optioneel Object/Array - Indien aanwezig, moet het een object/array zijn en ten minste één van de velden ingevuld zijn: + Als de categorie van het onderdeel moet worden gewijzigd, moet dit een object/array zijn en moet ten minste één van de volgende velden worden ingevuld:
                                          • category.id
                                          • category.name
                                          • @@ -1324,14 +1338,139 @@ category.id Optioneel - Geheel getal (Integer) - Geheel getal (Integer) > 0. Komt overeen met de interne numerieke ID van de categorie van het onderdeel. + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van de categorie van het onderdeel. category.name Optioneel - String - Niet-lege string, indien geen category.id-vermelding is gegeven. + Tekst + Niet-lege tekst als category.id niet is ingevuld. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Mogelijke kolommen: + + + + + assembly.bom_import.template.csv.table + + + + + Kolom + Voorwaarde + Gegevenstype + Beschrijving + + + + + quantity + Verplicht veld + Kommagetal (Float) + Moet worden ingevuld en moet een kommagetal (Float) bevatten dat groter is dan 0,0. + + + name + Optioneel + Tekst + De naam van het item binnen de assemblage. + + + Kolommen die beginnen met part_ + + Als een onderdeel moet worden toegewezen, moet ten minste één van de volgende kolommen worden ingevuld: +
                                              +
                                            • part_id
                                            • +
                                            • part_mpnr
                                            • +
                                            • part_ipn
                                            • +
                                            • part_name
                                            • +
                                            + + + + part_id + Optioneel + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van het onderdeel in de database. + + + part_mpnr + Optioneel + Tekst + Moet worden ingevuld als part_id, part_ipn of part_name niet zijn ingevuld. + + + part_ipn + Optioneel + Tekst + Moet worden ingevuld als part_id, part_mpnr of part_name niet zijn ingevuld. + + + part_name + Optioneel + Tekst + Moet worden ingevuld als part_id, part_mpnr of part_ipn niet zijn ingevuld. + + + part_description + Optioneel + Tekst + Wordt overgenomen en vervangt de bestaande waarde van de beschrijving als er een niet-lege tekst wordt opgegeven. + + + Kolommen die beginnen met part_manufacturer_ + + Als de fabrikant van een onderdeel moet worden gewijzigd of uniek moet worden gezocht via part_mpnr, moet ten minste één van de volgende kolommen worden ingevuld: +
                                              +
                                            • part_manufacturer_id
                                            • +
                                            • part_manufacturer_name
                                            • +
                                            + + + + part_manufacturer_id + Optioneel + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van de fabrikant. + + + part_manufacturer_name + Optioneel + Tekst + Moet worden ingevuld als part_manufacturer_id niet wordt opgegeven. + + + Kolommen die beginnen met part_category_ + + Als de categorie van een onderdeel moet worden gewijzigd, moet ten minste één van de volgende kolommen worden ingevuld: +
                                              +
                                            • part_category_id
                                            • +
                                            • part_category_name
                                            • +
                                            + + + + part_category_id + Optioneel + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van de categorie van het onderdeel. + + + part_category_name + Optioneel + Tekst + Moet worden ingevuld als part_category_id niet wordt opgegeven. @@ -1346,71 +1485,71 @@
                                            - - assembly.bom_import.template.kicad_pcbnew.exptected_columns.note - - Opmerking: Er wordt geen mapping uitgevoerd met specifieke componenten uit de categoriebeheer.

                                            - ]]> -
                                            + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Opmerking: Er wordt geen mapping uitgevoerd met specifieke componenten uit de categoriebeheer.

                                            + ]]> +
                                            - - assembly.bom_import.template.kicad_pcbnew.table - - - - - Veld - Voorwaarde - Gegevenstype - Beschrijving - - - - - Id - Optioneel - Integer - Vrij veld. Een unieke identificatienummer voor elk onderdeel. - - - Designator - Optioneel - String - Vrij veld. Een unieke referentienaam voor het onderdeel op de PCB, bijvoorbeeld "R1" voor weerstand 1.
                                            Wordt gebruikt als positioneringsnaam in de onderdelenlijst van de assemblage. - - - Package - Optioneel - String - Vrij veld. De behuizing of vormfactor van het onderdeel, bijvoorbeeld "0805" voor SMD-weerstanden.
                                            Wordt niet opgenomen in de onderdelenlijst binnen de assemblage. - - - Quantity - Vereist - Integer - Het aantal identieke onderdelen dat nodig is om een assemblage-instantie te creëren.
                                            Wordt gebruikt als hoeveelheid in de onderdelenlijst binnen de assemblage. - - - Designation - Vereist - String - De beschrijving of functie van het onderdeel, zoals weerstandwaarde "10kΩ" of condensatorwaarde "100nF".
                                            Wordt gebruikt als de naam in de onderdelenlijst binnen de assemblage. - - - Supplier and ref - Optioneel - String - Vrij veld. Kan bijvoorbeeld specifieke informatie over leveranciers bevatten.
                                            Wordt gebruikt als notitie in de onderdelenlijst binnen de assemblage. - - - - ]]> -
                                            -
                                            + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Veld + Voorwaarde + Gegevenstype + Beschrijving + + + + + Id + Optioneel + Integer + Vrij veld. Een unieke identificatienummer voor elk onderdeel. + + + Designator + Optioneel + String + Vrij veld. Een unieke referentienaam voor het onderdeel op de PCB, bijvoorbeeld "R1" voor weerstand 1.
                                            Wordt gebruikt als positioneringsnaam in de onderdelenlijst van de assemblage. + + + Package + Optioneel + String + Vrij veld. De behuizing of vormfactor van het onderdeel, bijvoorbeeld "0805" voor SMD-weerstanden.
                                            Wordt niet opgenomen in de onderdelenlijst binnen de assemblage. + + + Quantity + Vereist + Integer + Het aantal identieke onderdelen dat nodig is om een assemblage-instantie te creëren.
                                            Wordt gebruikt als hoeveelheid in de onderdelenlijst binnen de assemblage. + + + Designation + Vereist + String + De beschrijving of functie van het onderdeel, zoals weerstandwaarde "10kΩ" of condensatorwaarde "100nF".
                                            Wordt gebruikt als de naam in de onderdelenlijst binnen de assemblage. + + + Supplier and ref + Optioneel + String + Vrij veld. Kan bijvoorbeeld specifieke informatie over leveranciers bevatten.
                                            Wordt gebruikt als notitie in de onderdelenlijst binnen de assemblage. + + + + ]]> +
                                            +
                                            diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index d1a095072..86894d998 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -12613,6 +12613,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli JSON dla zespołu + + + assembly.bom_import.type.csv + CSV dla zestawienia + + assembly.bom_import.type.kicad_pcbnew @@ -12637,6 +12643,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Szablon importu JSON dla zespołu + + + assembly.import_bom.template.header.csv + Szablon importu CSV dla zespołu + + assembly.import_bom.template.header.kicad_pcbnew @@ -12696,24 +12708,26 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli quantity - Wymagane - Typ zmiennoprzecinkowy (Float) - Musi być obecne i zawierać wartość zmiennoprzecinkową (Float) większą niż 0,0. + Pole obowiązkowe + Liczba zmiennoprzecinkowa (Float) + Musi być wypełnione i zawierać liczbę zmiennoprzecinkową (Float) większą niż 0,0. name Opcjonalne - Ciąg znaków (String) - Jeśli obecne, musi być niepustym ciągiem znaków. + Tekst + Jeśli określono, musi to być niepusty tekst. Nazwa elementu w ramach montażu. part Opcjonalne Obiekt/Tablica - Jeśli podane, musi być obiektem/tablicą i co najmniej jedno z poniższych pól musi być wypełnione: + Jeśli konieczne jest przypisanie części, musi to być Obiekt/Tablica, a przynajmniej jedno z poniższych pól powinno być wypełnione:
                                            • part.id
                                            • +
                                            • part.mpnr
                                            • +
                                            • part.ipn
                                            • part.name
                                            @@ -12721,39 +12735,39 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli part.id Opcjonalne - Liczba całkowita (Integer) - Liczba całkowita (Integer) > 0. Odpowiada wewnętrznemu numerowi ID komponentu w Part-DB. + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID części w bazie danych. - part.name + part.mpnr Opcjonalne - Ciąg znaków (String) - Niepusty ciąg znaków, jeśli part.mpnr ani part.ipn nie są podane. + Tekst + Niepusty tekst, jeśli pola part.id, part.ipn lub part.name nie są wypełnione. - part.mpnr + part.ipn Opcjonalne - Ciąg znaków (String) - Niepusty ciąg znaków, jeśli part.name ani part.ipn nie są podane. + Tekst + Niepusty tekst, jeśli pola part.id, part.mpnr lub part.name nie są wypełnione. - part.ipn + part.name Opcjonalne - Ciąg znaków (String) - Niepusty ciąg znaków, jeśli part.name ani part.mpnr nie są podane. + Tekst + Niepusty tekst, jeśli pola part.id, part.mpnr lub part.ipn nie są wypełnione. part.description Opcjonalne - Ciąg znaków lub null - Jeśli obecne, musi być niepustym ciągiem znaków lub null. + Tekst lub null + Jeśli określono, musi to być niepusty tekst lub null. Ta wartość nadpisze istniejącą wartość w części. part.manufacturer Opcjonalne Obiekt/Tablica - Jeśli obecne, musi być obiektem/tablicą i co najmniej jedno z poniższych pól musi być wypełnione: + Jeśli producent części ma zostać zmieniony lub wyszukany unikalnie za pomocą wartości part.mpnr, musi to być Obiekt/Tablica i przynajmniej jedno z poniższych pól powinno być wypełnione:
                                            • manufacturer.id
                                            • manufacturer.name
                                            • @@ -12763,21 +12777,21 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli manufacturer.id Opcjonalne - Liczba całkowita (Integer) - Liczba całkowita (Integer) > 0. Odpowiada wewnętrznemu identyfikatorowi numerowemu producenta. + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID producenta. manufacturer.name Opcjonalne - Ciąg znaków (String) - Niepusty ciąg znaków, jeśli manufacturer.id nie jest podane. + Tekst + Niepusty tekst, jeśli pole manufacturer.id nie jest wypełnione. part.category Opcjonalne Obiekt/Tablica - Jeśli obecne, musi być obiektem/tablicą i co najmniej jedno z poniższych pól musi być wypełnione: + Jeśli konieczna jest zmiana kategorii części, musi to być Obiekt/Tablica i przynajmniej jedno z poniższych pól powinno być wypełnione:
                                              • category.id
                                              • category.name
                                              • @@ -12787,14 +12801,139 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli category.id Opcjonalne - Liczba całkowita (Integer) - Liczba całkowita (Integer) > 0. Odpowiada wewnętrznemu numerowi ID kategorii komponentu. + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID kategorii części. category.name Opcjonalne - Ciąg znaków (String) - Niepusty ciąg znaków, jeśli category.id nie jest podane. + Tekst + Niepusty tekst, jeśli pole category.id nie jest wypełnione. + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + Możliwe kolumny: + + + + + assembly.bom_import.template.csv.table + + + + + Kolumna + Warunek + Typ danych + Opis + + + + + quantity + Pole obowiązkowe + Liczba zmiennoprzecinkowa (Float) + Musi być wypełnione i zawierać liczbę zmiennoprzecinkową (Float) większą niż 0,0. + + + name + Opcjonalne + Tekst + Nazwa elementu w ramach montażu. + + + Kolumny zaczynające się od part_ + + Jeśli konieczne jest przypisanie części, przynajmniej jedna z poniższych kolumn powinna być wypełniona: +
                                                  +
                                                • part_id
                                                • +
                                                • part_mpnr
                                                • +
                                                • part_ipn
                                                • +
                                                • part_name
                                                • +
                                                + + + + part_id + Opcjonalne + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID części w bazie danych. + + + part_mpnr + Opcjonalne + Tekst + Musi być wypełnione, gdy kolumny part_id, part_ipn lub part_name nie są wypełnione. + + + part_ipn + Opcjonalne + Tekst + Musi być wypełnione, gdy kolumny part_id, part_mpnr lub part_name nie są wypełnione. + + + part_name + Opcjonalne + Tekst + Musi być wypełnione, gdy kolumny part_id, part_mpnr lub part_ipn nie są wypełnione. + + + part_description + Opcjonalne + Tekst + Zostanie przeniesione i zastąpi istniejącą wartość opisu, jeśli określono niepusty tekst. + + + Kolumny zaczynające się od part_manufacturer_ + + Jeśli producent części musi zostać zmieniony lub wyszukany unikalnie za pomocą part_mpnr, przynajmniej jedna z poniższych kolumn powinna być wypełniona: +
                                                  +
                                                • part_manufacturer_id
                                                • +
                                                • part_manufacturer_name
                                                • +
                                                + + + + part_manufacturer_id + Opcjonalne + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID producenta. + + + part_manufacturer_name + Opcjonalne + Tekst + Musi być wypełnione, jeśli part_manufacturer_id nie jest określony. + + + Kolumny zaczynające się od part_category_ + + Jeśli konieczna jest zmiana kategorii części, przynajmniej jedna z poniższych kolumn powinna być wypełniona: +
                                                  +
                                                • part_category_id
                                                • +
                                                • part_category_name
                                                • +
                                                + + + + part_category_id + Opcjonalne + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID kategorii części. + + + part_category_name + Opcjonalne + Tekst + Musi być wypełnione, jeśli part_category_id nie jest określone. @@ -12809,71 +12948,71 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli
                                                - - assembly.bom_import.template.kicad_pcbnew.exptected_columns.note - - Uwaga: Nie wykonano mapowania z określonymi komponentami z zarządzania kategoriami.

                                                - ]]> -
                                                -
                                                + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Uwaga: Nie wykonano mapowania z określonymi komponentami z zarządzania kategoriami.

                                                + ]]> +
                                                +
                                                - - assembly.bom_import.template.kicad_pcbnew.table - - - - - Pole - Warunek - Typ Danych - Opis - - - - - Id - Opcjonalne - Liczba całkowita - Pole dowolne. Unikalny numer identyfikacyjny dla każdego komponentu. - - - Designator - Opcjonalne - Tekst - Pole dowolne. Jednoznaczny znacznik referencyjny komponentu na PCB, np. "R1" dla rezystora 1.
                                                Używane jako nazwa pozycji w pozycji komponentu w montażu. - - - Package - Opcjonalne - Tekst - Pole dowolne. Obudowa lub forma komponentu, np. "0805" dla rezystorów SMD.
                                                Niewykorzystywane w pozycji komponentu w montażu. - - - Quantity - Wymagane - Liczba całkowita - Liczba identycznych komponentów potrzebna do utworzenia jednej instancji montażu.
                                                Używane jako ilość w pozycji komponentu w montażu. - - - Designation - Wymagane - Tekst - Opis lub funkcja komponentu, np. wartość rezystora "10kΩ" lub wartość kondensatora "100nF".
                                                Używane jako nazwa w pozycji komponentu w montażu. - - - Supplier and ref - Opcjonalne - Tekst - Pole dowolne. Może zawierać np. specyficzne informacje o dostawcy.
                                                Używane jako notatka w pozycji komponentu w montażu. - - - - ]]> -
                                                -
                                                + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Pole + Warunek + Typ Danych + Opis + + + + + Id + Opcjonalne + Liczba całkowita + Pole dowolne. Unikalny numer identyfikacyjny dla każdego komponentu. + + + Designator + Opcjonalne + Tekst + Pole dowolne. Jednoznaczny znacznik referencyjny komponentu na PCB, np. "R1" dla rezystora 1.
                                                Używane jako nazwa pozycji w pozycji komponentu w montażu. + + + Package + Opcjonalne + Tekst + Pole dowolne. Obudowa lub forma komponentu, np. "0805" dla rezystorów SMD.
                                                Niewykorzystywane w pozycji komponentu w montażu. + + + Quantity + Wymagane + Liczba całkowita + Liczba identycznych komponentów potrzebna do utworzenia jednej instancji montażu.
                                                Używane jako ilość w pozycji komponentu w montażu. + + + Designation + Wymagane + Tekst + Opis lub funkcja komponentu, np. wartość rezystora "10kΩ" lub wartość kondensatora "100nF".
                                                Używane jako nazwa w pozycji komponentu w montażu. + + + Supplier and ref + Opcjonalne + Tekst + Pole dowolne. Może zawierać np. specyficzne informacje o dostawcy.
                                                Używane jako notatka w pozycji komponentu w montażu. + + + + ]]> +
                                                +
                                                diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 429a82eca..ac04ed290 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -12713,6 +12713,12 @@ JSON для сборки + + + assembly.bom_import.type.csv + CSV для сборки + + assembly.bom_import.type.kicad_pcbnew @@ -12737,6 +12743,12 @@ Шаблон импорта JSON для сборки + + + assembly.import_bom.template.header.csv + Шаблон импорта CSV для сборки + + assembly.import_bom.template.header.kicad_pcbnew @@ -12796,64 +12808,66 @@ quantity - Обязательное - Дробное число (Float) - Поле должно быть заполнено и содержать дробное значение (Float), большее 0,0. + Обязательное поле + Число с плавающей точкой (Float) + Должно быть заполнено и содержать число с плавающей точкой (Float) больше 0,0. name - Опциональное - Строка (String) - Если присутствует, должно быть непустой строкой. + Необязательное + Строка + Если указано, должно быть непустой строкой. Имя элемента внутри сборки. part - Опциональное + Необязательное Объект/Массив - Если указано, должно быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: + Если необходимо назначить деталь, это должен быть объект/массив, и должно быть заполнено хотя бы одно из следующих полей:
                                                • part.id
                                                • +
                                                • part.mpnr
                                                • +
                                                • part.ipn
                                                • part.name
                                                part.id - Опциональное - Целое число (Integer) - Целое число (Integer) > 0. Соответствует внутреннему цифровому идентификатору компонента в Part-DB. - - - part.name - Опциональное - Строка (String) - Непустая строка, если part.mpnr или part.ipn не указаны. + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому идентификатору детали в базе данных. part.mpnr - Опциональное - Строка (String) - Непустая строка, если part.name или part.ipn не указаны. + Необязательное + Строка + Непустая строка, если part.id, part.ipn или part.name не указаны. part.ipn - Опциональное - Строка (String) - Непустая строка, если part.name или part.mpnr не указаны. + Необязательное + Строка + Непустая строка, если part.id, part.mpnr или part.name не указаны. + + + part.name + Необязательное + Строка + Непустая строка, если part.id, part.mpnr или part.ipn не указаны. part.description - Опциональное + Необязательное Строка или null - Если присутствует, должно быть непустой строкой или null. + Если указано, должно быть непустой строкой или null. Это значение перезаписывает существующее значение в детали. part.manufacturer - Опциональное + Необязательное Объект/Массив - Если присутствует, должно быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: + Если необходимо изменить производителя детали или уникально найти по значению part.mpnr, это должен быть объект/массив, и должно быть заполнено хотя бы одно из следующих полей:
                                                • manufacturer.id
                                                • manufacturer.name
                                                • @@ -12862,22 +12876,22 @@ manufacturer.id - Опциональное - Целое число (Integer) - Целое число (Integer) > 0. Соответствует внутреннему идентификатору производителя. + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому идентификатору производителя. manufacturer.name - Опциональное - Строка (String) + Необязательное + Строка Непустая строка, если manufacturer.id не указано. part.category - Опциональное + Необязательное Объект/Массив - Если присутствует, должно быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: + Если необходимо изменить категорию детали, это должен быть объект/массив, и должно быть заполнено хотя бы одно из следующих полей:
                                                  • category.id
                                                  • category.name
                                                  • @@ -12886,14 +12900,14 @@ category.id - Опциональное - Целое число (Integer) - Целое число (Integer) > 0. Соответствует внутреннему цифровому идентификатору категории компонента. + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому идентификатору категории детали. category.name - Опциональное - Строка (String) + Необязательное + Строка Непустая строка, если category.id не указано. @@ -12902,6 +12916,131 @@ + + + assembly.bom_import.template.csv.exptected_columns + Возможные столбцы: + + + + + assembly.bom_import.template.csv.table + + + + + Столбец + Условие + Тип данных + Описание + + + + + quantity + Обязательное поле + Число с плавающей точкой (Float) + Должно быть заполнено и содержать число с плавающей точкой (Float), больше 0,0. + + + name + Необязательное + Строка + Название элемента в рамках сборки. + + + Столбцы, начинающиеся с part_ + + Если необходимо назначить деталь, то хотя бы один из следующих столбцов должен быть заполнен: +
                                                      +
                                                    • part_id
                                                    • +
                                                    • part_mpnr
                                                    • +
                                                    • part_ipn
                                                    • +
                                                    • part_name
                                                    • +
                                                    + + + + part_id + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому ID детали в базе данных. + + + part_mpnr + Необязательное + Строка + Должно быть заполнено, если part_id, part_ipn или part_name не указаны. + + + part_ipn + Необязательное + Строка + Должно быть заполнено, если part_id, part_mpnr или part_name не указаны. + + + part_name + Необязательное + Строка + Должно быть заполнено, если part_id, part_mpnr или part_ipn не указаны. + + + part_description + Необязательное + Строка + Если указано, заменяет существующее значение описания деталя не пустой строкой. + + + Столбцы, начинающиеся с part_manufacturer_ + + Если необходимо указать производителя детали или найти деталь уникально по part_mpnr, должно быть заполнено хотя бы одно из следующих полей: +
                                                      +
                                                    • part_manufacturer_id
                                                    • +
                                                    • part_manufacturer_name
                                                    • +
                                                    + + + + part_manufacturer_id + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому ID производителя. + + + part_manufacturer_name + Необязательное + Строка + Должно быть заполнено, если part_manufacturer_id не указано. + + + Столбцы, начинающиеся с part_category_ + + Если необходимо изменить категорию детали, должно быть заполнено хотя бы одно из следующих полей: +
                                                      +
                                                    • part_category_id
                                                    • +
                                                    • part_category_name
                                                    • +
                                                    + + + + part_category_id + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому ID категории детали. + + + part_category_name + Необязательное + Строка + Должно быть заполнено, если part_category_id не указано. + + + + ]]> +
                                                    +
                                                    +
                                                    assembly.bom_import.template.kicad_pcbnew.exptected_columns @@ -12909,71 +13048,71 @@ - - assembly.bom_import.template.kicad_pcbnew.exptected_columns.note - - Примечание: Сопоставление с конкретными компонентами из управления категориями не выполняется.

                                                    - ]]> -
                                                    -
                                                    + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Примечание: Сопоставление с конкретными компонентами из управления категориями не выполняется.

                                                    + ]]> +
                                                    +
                                                    - - assembly.bom_import.template.kicad_pcbnew.table - - - - - Поле - Условие - Тип данных - Описание - - - - - Id - Опционально - Целое - Свободное поле. Уникальный идентификационный номер для каждого компонента. - - - Package - Designator - Строка - Свободное поле. Уникальная ссылочная метка компонента на печатной плате, например, "R1" для резистора 1.
                                                    Используется как наименование позиции в компоненте сборки. - - - Package - Опционально - Строка - Свободное поле. Тип корпуса или форм-фактор компонента, например, "0805" для SMD-резисторов.
                                                    Не включается в информацию о компоненте сборки. - - - Quantity - Обязательно - Целое - Количество одинаковых компонентов, необходимых для создания одной версии сборки.
                                                    Используется как количество в информации о компоненте сборки. - - - Designation - Обязательно - Строка - Описание или функция компонента, например, значение резистора "10kΩ" или значение конденсатора "100nF".
                                                    Используется как наименование в информации о компоненте сборки. - - - Supplier and ref - Опционально - Строка - Свободное поле. Может содержать, например, информацию о конкретных поставщиках.
                                                    Используется как примечание в информации о компоненте сборки. - - - - ]]> -
                                                    -
                                                    + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Поле + Условие + Тип данных + Описание + + + + + Id + Опционально + Целое + Свободное поле. Уникальный идентификационный номер для каждого компонента. + + + Package + Designator + Строка + Свободное поле. Уникальная ссылочная метка компонента на печатной плате, например, "R1" для резистора 1.
                                                    Используется как наименование позиции в компоненте сборки. + + + Package + Опционально + Строка + Свободное поле. Тип корпуса или форм-фактор компонента, например, "0805" для SMD-резисторов.
                                                    Не включается в информацию о компоненте сборки. + + + Quantity + Обязательно + Целое + Количество одинаковых компонентов, необходимых для создания одной версии сборки.
                                                    Используется как количество в информации о компоненте сборки. + + + Designation + Обязательно + Строка + Описание или функция компонента, например, значение резистора "10kΩ" или значение конденсатора "100nF".
                                                    Используется как наименование в информации о компоненте сборки. + + + Supplier and ref + Опционально + Строка + Свободное поле. Может содержать, например, информацию о конкретных поставщиках.
                                                    Используется как примечание в информации о компоненте сборки. + + + + ]]> +
                                                    +
                                                    diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 7e90daa28..426566225 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -12598,6 +12598,12 @@ Element 3 JSON 文件(组件) + + + assembly.bom_import.type.csv + 装配的CSV + + assembly.bom_import.type.kicad_pcbnew @@ -12622,6 +12628,12 @@ Element 3 装配 JSON 导入模板 + + + assembly.import_bom.template.header.csv + 用于装配的CSV导入模板 + + assembly.import_bom.template.header.kicad_pcbnew @@ -12681,24 +12693,26 @@ Element 3 quantity - 必填 + 必填字段 浮点数 (Float) - 必须存在,并包含大于 0.0 的浮点值 (Float)。 + 必须填写且包含大于 0.0 的浮点数 (Float)。 name 可选 - 字符串 (String) - 如果存在,必须是非空字符串。 + 文本 + 如果填写,必须是非空文本。表示装配中的项目名称。 part 可选 对象/数组 - 如果提供,则必须是对象/数组,并且以下字段中至少有一个被填写: + 如果需要分配零件,它必须是一个对象/数组,并且至少需要填写以下字段之一:
                                                    • part.id
                                                    • +
                                                    • part.mpnr
                                                    • +
                                                    • part.ipn
                                                    • part.name
                                                    @@ -12706,39 +12720,39 @@ Element 3 part.id 可选 - 整数 (Integer) - 整数 (Integer) > 0。表示组件在 Part-DB 中的内部数字 ID。 + 整数 + 一个大于 0 的整数。对应数据库中零件的内部数字ID。 - part.name + part.mpnr 可选 - 字符串 (String) - 如果未提供 part.mpnr 或 part.ipn,则必须是非空字符串。 + 文本 + 如果 part.id、part.ipn 或 part.name 未填写,则必须是非空文本。 - part.mpnr + part.ipn 可选 - 字符串 (String) - 如果未提供 part.name 或 part.ipn,则必须是非空字符串。 + 文本 + 如果 part.id、part.mpnr 或 part.name 未填写,则必须是非空文本。 - part.ipn + part.name 可选 - 字符串 (String) - 如果未提供 part.name 或 part.mpnr,则必须是非空字符串。 + 文本 + 如果 part.id、part.mpnr 或 part.ipn 未填写,则必须是非空文本。 part.description 可选 - 字符串或 null - 如果存在,必须是非空字符串或 null。 + 文本或 null + 如果填写,必须是非空文本或 null。该值将替换零件中的现有值。 part.manufacturer 可选 对象/数组 - 如果存在,则必须是对象/数组,并且以下字段中至少有一个被填写: + 如果需要更改零件制造商或通过值 part.mpnr 唯一查找零件,它必须是一个对象/数组,并且至少需要填写以下字段之一:
                                                    • manufacturer.id
                                                    • manufacturer.name
                                                    • @@ -12748,21 +12762,21 @@ Element 3 manufacturer.id 可选 - 整数 (Integer) - 整数 (Integer) > 0。表示制造商的内部数字 ID。 + 整数 + 一个大于 0 的整数。对应制造商的内部数字 ID。 manufacturer.name 可选 - 字符串 (String) - 如果未提供 manufacturer.id,则必须是非空字符串。 + 文本 + 如果 manufacturer.id 未填写,则必须是非空文本。 part.category 可选 对象/数组 - 如果存在,则必须是对象/数组,并且以下字段中至少有一个被填写: + 如果需要更改零件的类别,它必须是一个对象/数组,并且至少需要填写以下字段之一:
                                                      • category.id
                                                      • category.name
                                                      • @@ -12772,14 +12786,139 @@ Element 3 category.id 可选 - 整数 (Integer) - 整数 (Integer) > 0。表示组件类别的内部数字 ID。 + 整数 + 一个大于 0 的整数。对应零件类别的内部数字 ID。 category.name 可选 - 字符串 (String) - 如果未提供 category.id,则必须是非空字符串。 + 文本 + 如果 category.id 未填写,则必须是非空文本。 + + + + ]]> + + + + + + assembly.bom_import.template.csv.exptected_columns + 可用列: + + + + + assembly.bom_import.template.csv.table + + + + + 列名 + 条件 + 数据类型 + 描述 + + + + + quantity + 必填字段 + 浮点数 (Float) + 必须填写且包含大于 0.0 的浮点数 (Float)。 + + + name + 可选 + 文本 + 装配中的项目名称。 + + + 以 part_ 开头的列 + + 如果需要分配零件,则至少需要填写以下列之一: +
                                                          +
                                                        • part_id
                                                        • +
                                                        • part_mpnr
                                                        • +
                                                        • part_ipn
                                                        • +
                                                        • part_name
                                                        • +
                                                        + + + + part_id + 可选 + 整数 + 一个大于 0 的整数。对应数据库中零件的内部数字 ID。 + + + part_mpnr + 可选 + 文本 + 如果 part_id、part_ipn 或 part_name 未填写,则必须是非空文本。 + + + part_ipn + 可选 + 文本 + 如果 part_id、part_mpnr 或 part_name 未填写,则必须是非空文本。 + + + part_name + 可选 + 文本 + 如果 part_id、part_mpnr 或 part_ipn 未填写,则必须是非空文本。 + + + part_description + 可选 + 文本 + 如果指定,将取代现有描述值,且必须为非空文本。 + + + 以 part_manufacturer_ 开头的列 + + 如果需要更改零件的制造商或通过 part_mpnr 唯一查找,至少需要填写以下列之一: +
                                                          +
                                                        • part_manufacturer_id
                                                        • +
                                                        • part_manufacturer_name
                                                        • +
                                                        + + + + part_manufacturer_id + 可选 + 整数 + 一个大于 0 的整数。对应制造商的内部数字 ID。 + + + part_manufacturer_name + 可选 + 文本 + 如果 part_manufacturer_id 未填写,则必须是非空文本。 + + + 以 part_category_ 开头的列 + + 如果需要更改零件的类别,则至少需要填写以下列之一: +
                                                          +
                                                        • part_category_id
                                                        • +
                                                        • part_category_name
                                                        • +
                                                        + + + + part_category_id + 可选 + 整数 + 一个大于 0 的整数。对应零件类别的内部数字 ID。 + + + part_category_name + 可选 + 文本 + 如果 part_category_id 未填写,则必须是非空文本。 diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 46471d02d..699b5d2f3 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -371,6 +371,18 @@ Neplatný kód. Zkontrolujte, zda je vaše ověřovací aplikace správně nastavena a zda je čas správně nastaven jak na serveru, tak na ověřovacím zařízení.
                                                        + + + validator.bom_importer.invalid_import_type + Neplatný typ importu! + + + + + validator.bom_importer.invalid_file_extension + Neplatná přípona souboru "%extension%" pro typ importu "%importType%". Povolené přípony souborů: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -389,57 +401,57 @@ Musíte vybrat součást nebo nastavit název pro nesoučást! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Musíte zadat množství > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float očekává se jako float větší než 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty očekává se jako neprázdný řetězec - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null očekává se jako neprázdný řetězec nebo null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array očekává se jako pole (array) - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties musí mít alespoň jeden z následujících pod-parametrů: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor nenalezeno pro %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch se přesně neshoduje. Pro import zadáno: %importValue%, nalezeno (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties musí obsahovat jako pod-parametr buď: "id" jako celé číslo větší než 0 nebo "name" jako neprázdný řetězec diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 88ec6784b..056871bbf 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -347,6 +347,18 @@ Denne leverandørstregkodeværdi er allerede brugt til en anden beholdning. Stregkoden skal være unik! + + + validator.bom_importer.invalid_import_type + Ugyldig importtype! + + + + + validator.bom_importer.invalid_file_extension + Ugyldig filtypenavn "%extension%" for importtypen "%importType%". Tilladte filtypenavne: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -365,57 +377,57 @@ Du skal vælge en del eller sætte et navn for en ikke-del! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Du skal angive en mængde > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float forventet som en float større end 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty forventet som en ikke-tom streng - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null forventet som en ikke-tom streng eller null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array forventet som en array - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties skal have mindst én af følgende underparametre: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor ikke fundet for %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch stemmer ikke helt overens. Givet til import: %importValue%, fundet (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties skal indeholde som en underparameter enten: "id" som et heltal større end 0 eller "name" som en ikke-tom streng diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index cb710ac97..8771a0e65 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -371,6 +371,18 @@ Ungültiger Code. Überprüfen Sie, dass die Authenticator App korrekt eingerichtet ist und dass der Server und das Gerät beide die korrekte Uhrzeit eingestellt haben. + + + validator.bom_importer.invalid_import_type + Ungültiger Importtyp! + + + + + validator.bom_importer.invalid_file_extension + Ungültige Dateierweiterung "%extension%" für den Importtyp "%importType%". Erlaubte Dateierweiterungen: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -389,57 +401,57 @@ Sie müssen ein Bauteil auswählen, oder einen Namen für den Eintrag setzen! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Sie müssen eine Stückzahl > 0 angeben! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float wird als float größer als 0.0 erwartet - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty als nicht leere Zeichenkette erwartet - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null als nicht leere Zeichenkette oder null erwartet - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array als array erwartet - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties muss mindestens eines der folgenden Unter-Parameter haben: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor nicht gefunden für %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch stimmt nicht genau überein. Für den Import gegeben: %importValue%, gefunden (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties muss entweder als Unter-Parameter zugewiesen haben: "id" als Ganzzahl größer als 0 oder "name" als nicht leere Zeichenfolge diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index 318ba8928..bb78c7994 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -13,6 +13,18 @@ Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! + + + validator.bom_importer.invalid_import_type + Μη έγκυρος τύπος εισαγωγής! + + + + + validator.bom_importer.invalid_file_extension + Μη έγκυρη επέκταση αρχείου "%extension%" για τον τύπο εισαγωγής "%importType%". Επιτρεπόμενες επεκτάσεις αρχείων: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -31,57 +43,57 @@ Πρέπει να επιλέξετε ένα εξάρτημα ή να βάλετε ένα όνομα για ένα μη εξάρτημα! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Πρέπει να εισαγάγετε ποσότητα > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float αναμένεται ως δεκαδικός αριθμός (float) μεγαλύτερος από 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty αναμένεται ως μη κενή συμβολοσειρά - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null αναμένεται ως μη κενή συμβολοσειρά ή null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array αναμένεται ως array - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties πρέπει να έχει τουλάχιστον μία από τις ακόλουθες υπο-παραμέτρους: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor δεν βρέθηκε για %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch δεν ταιριάζει απόλυτα. Δόθηκε για εισαγωγή: %importValue%, βρέθηκε (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties πρέπει να περιέχει ως υπο-παράμετρο είτε: "id" ως ακέραιο αριθμό μεγαλύτερο από 0 είτε "name" ως μη κενή συμβολοσειρά diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 93640fff7..4c53ed187 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -371,6 +371,18 @@ Invalid code. Check that your authenticator app is set up correctly and that both the server and authentication device has the time set correctly. + + + validator.bom_importer.invalid_import_type + Invalid import type! + + + + + validator.bom_importer.invalid_file_extension + Invalid file extension "%extension%" for import type %importType%". Allowed file extensions: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -389,57 +401,57 @@ __validator.assembly.bom_entry.name_or_part_needed - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required you must specify a quantity > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float expected as float greater than 0.0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty expected as non-empty string - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null als nicht leere Zeichenkette oder null erwartet - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array expectd as array - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties must have at least one of the following sub-properties: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor not found for %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch does not match exactly. Given for import: %importValue%, found (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties must have either assigned as sub-property: "id" as an integer greater than 0, or "name" as a non-empty string diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index cde3672f6..957a47916 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -209,6 +209,18 @@ Seule une pièce ou un assemblage peut être sélectionné. Veuillez ajuster votre sélection! + + + validator.bom_importer.invalid_import_type + Type d'importation invalide ! + + + + + validator.bom_importer.invalid_file_extension + Extension de fichier "%extension%" invalide pour le type d'importation "%importType%". Extensions de fichier autorisées : %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -227,57 +239,57 @@ Vous devez sélectionner une pièce ou attribuer un nom pour un non-élément! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Vous devez entrer une quantité > 0 ! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float attendu comme un nombre décimal (float) supérieur à 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty attendu comme une chaîne de caractères non vide - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null attendu comme une chaîne de caractères non vide ou null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array attendu comme un tableau (array) - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties doit contenir au moins l'un des sous-paramètres suivants : %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor non trouvé pour %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch ne correspond pas exactement. Donné pour l'importation : %importValue%, trouvé (%foundId%) : %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties doit contenir comme sous-paramètre soit : "id" comme entier supérieur à 0 ou "name" comme chaîne de caractères non vide diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 988724201..639dff8bc 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -365,6 +365,18 @@ Neispravan kod. Provjerite je li vaša aplikacija za autentifikaciju ispravno postavljena i jesu li poslužitelj i uređaj za autentifikaciju ispravno postavili vrijeme. + + + validator.bom_importer.invalid_import_type + Nevažeći tip uvoza! + + + + + validator.bom_importer.invalid_file_extension + Nevažeća ekstenzija datoteke "%extension%" za tip uvoza "%importType%". Dopuštene ekstenzije datoteka: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -383,57 +395,57 @@ Morate odabrati dio ili unijeti naziv za nedio! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Morate unijeti količinu > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float očekuje se decimalni broj (float) veći od 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty očekuje se kao neprazan niz znakova - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null očekuje se kao neprazan niz znakova ili null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array očekuje se kao niz - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties mora sadržavati barem jedan od sljedećih pod-parametara: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor nije pronađeno za %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch ne podudara se točno. Uneseno za uvoz: %importValue%, pronađeno (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties mora sadržavati kao pod-parametar bilo: "id" kao cijeli broj veći od 0 ili "name" kao neprazan niz znakova diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index 63ca86d77..a1b9b2f09 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -365,6 +365,18 @@ Codice non valido. Controlla che la tua app di autenticazione sia impostata correttamente e che sia il server che il dispositivo di autenticazione abbiano l'ora impostata correttamente. + + + validator.bom_importer.invalid_import_type + Tipo di importazione non valido! + + + + + validator.bom_importer.invalid_file_extension + Estensione del file "%extension%" non valida per il tipo di importazione "%importType%". Estensioni consentite: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -383,57 +395,57 @@ È necessario selezionare una parte o inserire un nome per un non-parte! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Devi inserire una quantità > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float atteso come numero decimale (float) maggiore di 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty atteso come stringa non vuota - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null atteso come stringa non vuota o null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array atteso come array - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties deve avere almeno uno dei seguenti sotto-parametri: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor non trovato per %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch non corrisponde esattamente. Valore dato per l'importazione: %importValue%, trovato (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties deve contenere come sotto-parametro: "id" come intero maggiore di 0 o "name" come stringa non vuota diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index c0b541173..4a36a79ac 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -209,6 +209,18 @@ 部品またはアセンブリのみ選択可能です。選択内容を調整してください! + + + validator.bom_importer.invalid_import_type + 無効なインポートタイプです! + + + + + validator.bom_importer.invalid_file_extension + インポートタイプ "%importType%" に対して無効なファイル拡張子 "%extension%"。許可されているファイル拡張子: %allowedExtensions%。 + + assembly.bom_entry.part_already_in_bom @@ -227,57 +239,57 @@ 部品を選択するか、非部品の名前を入力する必要があります! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required 数量 > 0 を入力する必要があります! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float 0.0 より大きい小数 (float) である必要があります - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty 空でない文字列が期待されます - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null 空でない文字列または null が期待されます - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array 配列として期待されます - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties 以下のサブパラメーターのいずれかを含む必要があります:%propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor %value% に対する項目が見つかりません - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch 完全には一致しません。インポートされた値:%importValue%、見つかった値 (%foundId%):%foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties サブパラメーターとして次のいずれかを含む必要があります:"id" は 0 より大きい整数、または "name" は空でない文字列 diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index d417757dd..e80dd23bb 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -365,6 +365,18 @@ Nieprawidłowy kod. Sprawdź, czy aplikacja uwierzytelniająca jest poprawnie skonfigurowana i czy zarówno serwer, jak i urządzenie uwierzytelniające mają poprawnie ustawiony czas. + + + validator.bom_importer.invalid_import_type + Nieprawidłowy typ importu! + + + + + validator.bom_importer.invalid_file_extension + Nieprawidłowe rozszerzenie pliku "%extension%" dla typu importu "%importType%". Dozwolone rozszerzenia plików: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -383,57 +395,57 @@ Musisz wybrać element lub przypisać nazwę dla elementu niestandardowego! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Musisz wprowadzić ilość > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float oczekiwano liczby zmiennoprzecinkowej (float) większej od 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty oczekiwano jako niepusty ciąg znaków - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null oczekiwano jako niepusty ciąg znaków lub null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array oczekiwano jako tablicę - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties musi zawierać co najmniej jeden z następujących podparametrów: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor nie znaleziono dla %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch brak dokładnego dopasowania. Wprowadzone do importu: %importValue%, znalezione (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties musi zawierać jako podparametr: "id" jako liczbę całkowitą większą od 0 lub "name" jako niepusty ciąg znaków diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 625aea240..48f0737eb 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -365,6 +365,18 @@ Неверный код. Проверьте, что приложение аутентификации настроено правильно и что на сервере и устройстве аутентификации установлено правильное время. + + + validator.bom_importer.invalid_import_type + Недопустимый тип импорта! + + + + + validator.bom_importer.invalid_file_extension + Недопустимое расширение файла "%extension%" для типа импорта "%importType%". Допустимые расширения файлов: %allowedExtensions%. + + assembly.bom_entry.part_already_in_bom @@ -383,57 +395,57 @@ Необходимо выбрать деталь или ввести название для недетали! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required Необходимо указать количество > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float ожидается число с плавающей запятой (float), большее 0,0 - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty ожидается непустая строка - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null ожидается непустая строка или null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array ожидается массив - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties должен содержать хотя бы один из следующих под-параметров: %propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor не найдено для %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch точное совпадение отсутствует. Указано для импорта: %importValue%, найдено (%foundId%): %foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties должен содержать под-параметр: "id" как целое число больше 0 или "name" как непустая строка diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 5093ce989..dea45ccc9 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -353,6 +353,18 @@ 由于技术限制,在32位系统中无法选择2038年1月19日之后的日期! + + + validator.bom_importer.invalid_import_type + 无效的导入类型! + + + + + validator.bom_importer.invalid_file_extension + 导入类型“%importType%”的文件扩展名“%extension%”无效。允许的扩展名: %allowedExtensions%。 + + assembly.bom_entry.part_already_in_bom @@ -371,57 +383,57 @@ 必须选择零件或为非零件指定名称! - + - validator.bom_importer.json.quantity.required + validator.bom_importer.json_csv.quantity.required 必须输入数量 > 0! - + - validator.bom_importer.json.quantity.float + validator.bom_importer.json_csv.quantity.float 应为大于 0.0 的浮点数 (float) - + - validator.bom_importer.json.parameter.string.notEmpty + validator.bom_importer.json_csv.parameter.string.notEmpty 应为非空字符串 - + - validator.bom_importer.json.parameter.string.notEmpty.null + validator.bom_importer.json_csv.parameter.string.notEmpty.null 应为非空字符串或 null - + - validator.bom_importer.json.parameter.array + validator.bom_importer.json_csv.parameter.array 应为数组 - + - validator.bom_importer.json.parameter.subproperties + validator.bom_importer.json_csv.parameter.subproperties 必须包含以下子参数之一:%propertyString% - + - validator.bom_importer.json.parameter.notFoundFor + validator.bom_importer.json_csv.parameter.notFoundFor 未找到对应值 %value% - + - validator.bom_importer.json.parameter.noExactMatch + validator.bom_importer.json_csv.parameter.noExactMatch 未精确匹配。用于导入的值:%importValue%,找到的值 (%foundId%):%foundValue% - + - validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties 必须包含子参数:"id" 为大于 0 的整数,或 "name" 为非空字符串 From 7970f04fa12a23e0083da0d4e1f345401a769e6a Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 22 Apr 2025 16:31:22 +0200 Subject: [PATCH 43/83] =?UTF-8?q?Umstellung=20Migrationen=20bzgl.=20Multi-?= =?UTF-8?q?Plattform-Support.=20Zun=C3=A4chst=20MySQL,=20SQLite=20Statemen?= =?UTF-8?q?ts=20integrieren.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/Version20250304081039.php | 89 ++++++- migrations/Version20250304154507.php | 377 ++++++++++++++++++++++++++- migrations/Version20250310160354.php | 167 +++++++++++- 3 files changed, 624 insertions(+), 9 deletions(-) diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php index 4e521ade9..789cf3281 100644 --- a/migrations/Version20250304081039.php +++ b/migrations/Version20250304081039.php @@ -4,12 +4,18 @@ namespace DoctrineMigrations; +use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; -final class Version20250304081039 extends AbstractMigration +final class Version20250304081039 extends AbstractMultiPlatformMigration { - public function up(Schema $schema): void + public function getDescription(): string + { + return 'Add assemblies and assembly BOM entries'; + } + + public function mySQLUp(Schema $schema): void { $this->addSql('CREATE TABLE assemblies (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, order_quantity INT NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts TINYINT(1) NOT NULL, description LONGTEXT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, INDEX IDX_5F3832C0727ACA70 (parent_id), INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); $this->addSql('CREATE TABLE assembly_bom_entries (id INT AUTO_INCREMENT NOT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames LONGTEXT NOT NULL, name VARCHAR(255) DEFAULT NULL, comment LONGTEXT NOT NULL, price NUMERIC(11, 5) DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_assembly INT DEFAULT NULL, id_part INT DEFAULT NULL, price_currency_id INT DEFAULT NULL, INDEX IDX_8C74887E2F180363 (id_assembly), INDEX IDX_8C74887EC22F6CC4 (id_part), INDEX IDX_8C74887E3FFDCD60 (price_currency_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); @@ -20,7 +26,7 @@ public function up(Schema $schema): void $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id)'); } - public function down(Schema $schema): void + public function mySQLDown(Schema $schema): void { $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0727ACA70'); $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0EA7100A1'); @@ -30,4 +36,81 @@ public function down(Schema $schema): void $this->addSql('DROP TABLE assemblies'); $this->addSql('DROP TABLE assembly_bom_entries'); } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE assemblies ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + order_quantity INTEGER NOT NULL, + order_only_missing_parts BOOLEAN NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + status VARCHAR(64) DEFAULT NULL, + description CLOB NOT NULL, + alternative_names CLOB DEFAULT NULL, + CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_5F3832C0727ACA70 ON assemblies (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_5F3832C0EA7100A1 ON assemblies (id_preview_attachment) + SQL); + + $this->addSql(<<<'SQL' + CREATE TABLE assembly_bom_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_assembly INTEGER DEFAULT NULL, + id_part INTEGER DEFAULT NULL, + price_currency_id INTEGER DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames CLOB NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment CLOB NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT FK_8C74887E4AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E4AD2039E ON assembly_bom_entries (id_assembly) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887EC22F6CC4 ON assembly_bom_entries (id_part) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E3FFDCD60 ON assembly_bom_entries (price_currency_id) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP TABLE assembly_bom_entries + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assemblies + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + //Not needed + } + + public function postgreSQLDown(Schema $schema): void + { + //Not needed + } } diff --git a/migrations/Version20250304154507.php b/migrations/Version20250304154507.php index 62dcc43c0..37cf2fec3 100644 --- a/migrations/Version20250304154507.php +++ b/migrations/Version20250304154507.php @@ -4,22 +4,393 @@ namespace DoctrineMigrations; +use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; -final class Version20250304154507 extends AbstractMigration +final class Version20250304154507 extends AbstractMultiPlatformMigration { - public function up(Schema $schema): void + public function getDescription(): string + { + return 'Add built_assembly_id to parts table'; + } + + public function mySQLUp(Schema $schema): void { $this->addSql('ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL'); $this->addSql('ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id)'); $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON parts (built_assembly_id)'); } - public function down(Schema $schema): void + public function mySQLDown(Schema $schema): void { $this->addSql('ALTER TABLE `parts` DROP FOREIGN KEY FK_6940A7FECC660B3C'); $this->addSql('DROP INDEX UNIQ_6940A7FECC660B3C ON `parts`'); $this->addSql('ALTER TABLE `parts` DROP built_assembly_id'); } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__parts AS + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM parts + SQL); + $this->addSql('DROP TABLE parts'); + + $this->addSql(<<<'SQL' + CREATE TABLE "parts" + ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + id_category INTEGER NOT NULL, + id_footprint INTEGER DEFAULT NULL, + id_part_unit INTEGER DEFAULT NULL, + id_manufacturer INTEGER DEFAULT NULL, + order_orderdetails_id INTEGER DEFAULT NULL, + built_project_id INTEGER DEFAULT NULL, + built_assembly_id INTEGER DEFAULT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + needs_review BOOLEAN NOT NULL, + tags CLOB NOT NULL, + mass DOUBLE PRECISION DEFAULT NULL, + description CLOB NOT NULL, + comment CLOB NOT NULL, + visible BOOLEAN NOT NULL, + favorite BOOLEAN NOT NULL, + minamount DOUBLE PRECISION NOT NULL, + manufacturer_product_url CLOB NOT NULL, + manufacturer_product_number VARCHAR(255) NOT NULL, + manufacturing_status VARCHAR(255) DEFAULT NULL, + order_quantity INTEGER NOT NULL, + manual_order BOOLEAN NOT NULL, + ipn VARCHAR(100) DEFAULT NULL, + provider_reference_provider_key VARCHAR(255) DEFAULT NULL, + provider_reference_provider_id VARCHAR(255) DEFAULT NULL, + provider_reference_provider_url VARCHAR(255) DEFAULT NULL, + provider_reference_last_updated DATETIME DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_value VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO parts ( + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + ) SELECT * FROM __temp__parts + SQL); + $this->addSql('DROP TABLE __temp__parts'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON "parts" (built_assembly_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_ipn ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_name ON "parts" (name) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__parts AS + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM parts + SQL); + + $this->addSql('DROP TABLE parts'); + + $this->addSql(<<<'SQL' + CREATE TABLE "parts" + ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + id_category INTEGER NOT NULL, + id_footprint INTEGER DEFAULT NULL, + id_part_unit INTEGER DEFAULT NULL, + id_manufacturer INTEGER DEFAULT NULL, + order_orderdetails_id INTEGER DEFAULT NULL, + built_project_id INTEGER DEFAULT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + needs_review BOOLEAN NOT NULL, + tags CLOB NOT NULL, + mass DOUBLE PRECISION DEFAULT NULL, + description CLOB NOT NULL, + comment CLOB NOT NULL, + visible BOOLEAN NOT NULL, + favorite BOOLEAN NOT NULL, + minamount DOUBLE PRECISION NOT NULL, + manufacturer_product_url CLOB NOT NULL, + manufacturer_product_number VARCHAR(255) NOT NULL, + manufacturing_status VARCHAR(255) DEFAULT NULL, + order_quantity INTEGER NOT NULL, + manual_order BOOLEAN NOT NULL, + ipn VARCHAR(100) DEFAULT NULL, + provider_reference_provider_key VARCHAR(255) DEFAULT NULL, + provider_reference_provider_id VARCHAR(255) DEFAULT NULL, + provider_reference_provider_url VARCHAR(255) DEFAULT NULL, + provider_reference_last_updated DATETIME DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_value VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO parts ( + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + ) SELECT * FROM __temp__parts + SQL); + + $this->addSql('DROP TABLE __temp__parts'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_ipn ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_name ON "parts" (name) + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + //Not needed + } + + public function postgreSQLDown(Schema $schema): void + { + //Not needed + } } diff --git a/migrations/Version20250310160354.php b/migrations/Version20250310160354.php index 542fcac28..201e7e85a 100644 --- a/migrations/Version20250310160354.php +++ b/migrations/Version20250310160354.php @@ -4,12 +4,18 @@ namespace DoctrineMigrations; +use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; -final class Version20250310160354 extends AbstractMigration +final class Version20250310160354 extends AbstractMultiPlatformMigration { - public function up(Schema $schema): void + public function getDescription(): string + { + return 'Add assembly_id to project_bom_entries'; + } + + public function mySQLUp(Schema $schema): void { $this->addSql('ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e2f180363 TO IDX_8C74887E4AD2039E'); $this->addSql('ALTER TABLE project_bom_entries ADD id_assembly INT DEFAULT NULL AFTER id_part'); @@ -17,11 +23,166 @@ public function up(Schema $schema): void $this->addSql('CREATE INDEX IDX_1AA2DD314AD2039E ON project_bom_entries (id_assembly)'); } - public function down(Schema $schema): void + public function mySQLDown(Schema $schema): void { $this->addSql('ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e4ad2039e TO IDX_8C74887E2F180363'); $this->addSql('ALTER TABLE project_bom_entries DROP FOREIGN KEY FK_1AA2DD314AD2039E'); $this->addSql('DROP INDEX IDX_1AA2DD314AD2039E ON project_bom_entries'); $this->addSql('ALTER TABLE project_bom_entries DROP id_assembly'); } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__project_bom_entries AS + SELECT + id, + id_device, + id_part, + price_currency_id, + quantity, + mountnames, + name, + comment, + price, + last_modified, + datetime_added + FROM project_bom_entries + SQL); + + $this->addSql('DROP TABLE project_bom_entries'); + + $this->addSql(<<<'SQL' + CREATE TABLE project_bom_entries + ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_device INTEGER DEFAULT NULL, + id_assembly INTEGER DEFAULT NULL, + id_part INTEGER DEFAULT NULL, + price_currency_id INTEGER DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames CLOB NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment CLOB NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT FK_1AA2DD312F180363 FOREIGN KEY (id_device) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_1AA2DD314AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_1AA2DD31C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + + $this->addSql(<<<'SQL' + INSERT INTO project_bom_entries ( + id, + id_device, + id_part, + price_currency_id, + quantity, + mountnames, + name, + comment, + price, + last_modified, + datetime_added + ) SELECT * FROM __temp__project_bom_entries + SQL); + $this->addSql('DROP TABLE __temp__project_bom_entries'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD314AD2039E ON project_bom_entries (id_assembly) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__project_bom_entries AS + SELECT + id, + id_device, + id_part, + price_currency_id, + quantity, + mountnames, + name, + comment, + price, + last_modified, + datetime_added + FROM project_bom_entries + SQL); + + $this->addSql('DROP TABLE project_bom_entries'); + + $this->addSql(<<<'SQL' + CREATE TABLE project_bom_entries + ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_device INTEGER DEFAULT NULL, + id_part INTEGER DEFAULT NULL, + price_currency_id INTEGER DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames CLOB NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment CLOB NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT FK_1AA2DD312F180363 FOREIGN KEY (id_device) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_1AA2DD31C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO project_bom_entries ( + id, + id_device, + id_part, + price_currency_id, + quantity, + mountnames, + name, + comment, + price, + last_modified, + datetime_added + ) SELECT * FROM __temp__project_bom_entries + SQL); + + $this->addSql('DROP TABLE __temp__project_bom_entries'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id) + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + //Not needed + } + + public function postgreSQLDown(Schema $schema): void + { + //Not needed + } } From 3b7890cbb02b1a1a924e1c81a0596a82a62c1110 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 17 Jun 2025 11:28:42 +0200 Subject: [PATCH 44/83] Projekt BOM-Konfiguration um Assemblies bereinigen. Assembly BOM-Konfiguration um Projektauswahl erweitern (APS-3, APS-4) --- .../elements/project_select_controller.js | 70 +++++++ migrations/Version20250304081039.php | 109 +++++++++- migrations/Version20250304154507.php | 20 +- migrations/Version20250310160354.php | 188 ------------------ src/Controller/TypeaheadController.php | 36 ++-- .../AssemblyBomEntriesDataTable.php | 38 ++-- ...eHelper.php => ProjectDataTableHelper.php} | 6 +- src/DataTables/ProjectBomEntriesDataTable.php | 27 +-- src/Entity/AssemblySystem/Assembly.php | 4 +- .../AssemblySystem/AssemblyBOMEntry.php | 26 ++- src/Entity/ProjectSystem/Project.php | 1 - src/Entity/ProjectSystem/ProjectBOMEntry.php | 38 +--- .../AssemblySystem/AssemblyBOMEntryType.php | 13 +- src/Form/AssemblySystem/AssemblyBuildType.php | 19 +- .../ProjectSystem/ProjectAddPartsType.php | 1 - .../ProjectSystem/ProjectBOMEntryType.php | 5 - src/Form/ProjectSystem/ProjectBuildType.php | 23 +-- ...lySelectType.php => ProjectSelectType.php} | 26 +-- src/Helpers/Projects/ProjectBuildRequest.php | 98 +-------- src/Repository/AssemblyRepository.php | 69 ------- src/Repository/Parts/DeviceRepository.php | 18 ++ .../AssemblySystem/AssemblyBuildHelper.php | 36 ++-- ...erator.php => ProjectPreviewGenerator.php} | 26 +-- .../ProjectSystem/ProjectBuildHelper.php | 71 ++----- src/Twig/AssemblyTwigExtension.php | 6 +- .../form/collection_types_layout.html.twig | 18 +- ...collection_types_layout_assembly.html.twig | 18 +- templates/projects/build/_form.html.twig | 57 +----- translations/messages.cs.xlf | 96 ++------- translations/messages.da.xlf | 94 ++------- translations/messages.de.xlf | 84 ++------ translations/messages.el.xlf | 82 ++------ translations/messages.en.xlf | 82 ++------ translations/messages.es.xlf | 82 ++------ translations/messages.fr.xlf | 86 ++------ translations/messages.it.xlf | 86 ++------ translations/messages.ja.xlf | 106 ++++------ translations/messages.nl.xlf | 82 ++------ translations/messages.pl.xlf | 86 ++------ translations/messages.ru.xlf | 86 ++------ translations/messages.zh.xlf | 86 ++------ translations/validators.cs.xlf | 6 + translations/validators.da.xlf | 6 + translations/validators.de.xlf | 6 + translations/validators.el.xlf | 6 + translations/validators.en.xlf | 12 +- translations/validators.fr.xlf | 6 + translations/validators.hr.xlf | 6 + translations/validators.it.xlf | 6 + translations/validators.ja.xlf | 6 + translations/validators.pl.xlf | 6 + translations/validators.ru.xlf | 6 + translations/validators.zh.xlf | 6 + 53 files changed, 740 insertions(+), 1543 deletions(-) create mode 100644 assets/controllers/elements/project_select_controller.js delete mode 100644 migrations/Version20250310160354.php rename src/DataTables/Helpers/{AssemblyDataTableHelper.php => ProjectDataTableHelper.php} (91%) rename src/Form/Type/{AssemblySelectType.php => ProjectSelectType.php} (82%) delete mode 100644 src/Repository/AssemblyRepository.php rename src/Services/Attachments/{AssemblyPreviewGenerator.php => ProjectPreviewGenerator.php} (75%) diff --git a/assets/controllers/elements/project_select_controller.js b/assets/controllers/elements/project_select_controller.js new file mode 100644 index 000000000..98702d419 --- /dev/null +++ b/assets/controllers/elements/project_select_controller.js @@ -0,0 +1,70 @@ +import {Controller} from "@hotwired/stimulus"; + +import "tom-select/dist/css/tom-select.bootstrap5.css"; +import '../../css/components/tom-select_extensions.css'; +import TomSelect from "tom-select"; +import {marked} from "marked"; + +export default class extends Controller { + _tomSelect; + + connect() { + + let settings = { + allowEmptyOption: true, + plugins: ['dropdown_input', 'clear_button'], + searchField: ["name", "description", "category", "footprint"], + valueField: "id", + labelField: "name", + preload: "focus", + render: { + item: (data, escape) => { + return '' + (data.image ? "" : "") + escape(data.name) + ''; + }, + option: (data, escape) => { + if(data.text) { + return '' + escape(data.text) + ''; + } + + let tmp = '
                                                        ' + + "
                                                        " + + (data.image ? "" : "") + + "
                                                        " + + "
                                                        " + + '
                                                        ' + escape(data.name) + '
                                                        ' + + (data.description ? '

                                                        ' + marked.parseInline(data.description) + '

                                                        ' : "") + + (data.category ? '

                                                        ' + escape(data.category) : ""); + + return tmp + '

                                                        ' + + '
                                                        '; + } + } + }; + + + if (this.element.dataset.autocomplete) { + const base_url = this.element.dataset.autocomplete; + settings.valueField = "id"; + settings.load = (query, callback) => { + const url = base_url.replace('__QUERY__', encodeURIComponent(query)); + + fetch(url) + .then(response => response.json()) + .then(json => {callback(json);}) + .catch(() => { + callback() + }); + }; + + + this._tomSelect = new TomSelect(this.element, settings); + //this._tomSelect.clearOptions(); + } + } + + disconnect() { + super.disconnect(); + //Destroy the TomSelect instance + this._tomSelect.destroy(); + } +} \ No newline at end of file diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php index 789cf3281..755ae2360 100644 --- a/migrations/Version20250304081039.php +++ b/migrations/Version20250304081039.php @@ -6,7 +6,6 @@ use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; -use Doctrine\Migrations\AbstractMigration; final class Version20250304081039 extends AbstractMultiPlatformMigration { @@ -17,12 +16,13 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { - $this->addSql('CREATE TABLE assemblies (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, order_quantity INT NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts TINYINT(1) NOT NULL, description LONGTEXT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, INDEX IDX_5F3832C0727ACA70 (parent_id), INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('CREATE TABLE assembly_bom_entries (id INT AUTO_INCREMENT NOT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames LONGTEXT NOT NULL, name VARCHAR(255) DEFAULT NULL, comment LONGTEXT NOT NULL, price NUMERIC(11, 5) DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_assembly INT DEFAULT NULL, id_part INT DEFAULT NULL, price_currency_id INT DEFAULT NULL, INDEX IDX_8C74887E2F180363 (id_assembly), INDEX IDX_8C74887EC22F6CC4 (id_part), INDEX IDX_8C74887E3FFDCD60 (price_currency_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('CREATE TABLE assemblies (id INT AUTO_INCREMENT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, name VARCHAR(255) NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, order_quantity INT NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts TINYINT(1) NOT NULL, description LONGTEXT NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_5F3832C0727ACA70 (parent_id), INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('CREATE TABLE assembly_bom_entries (id INT AUTO_INCREMENT NOT NULL, id_assembly INT DEFAULT NULL, id_part INT DEFAULT NULL, id_project INT DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames LONGTEXT NOT NULL, name VARCHAR(255) DEFAULT NULL, comment LONGTEXT NOT NULL, price NUMERIC(11, 5) DEFAULT NULL, price_currency_id INT DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_8C74887E2F180363 (id_assembly), INDEX IDX_8C74887EC22F6CC4 (id_part), INDEX IDX_8C74887EF12E799E (id_project), INDEX IDX_8C74887E3FFDCD60 (price_currency_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); $this->addSql('ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id)'); $this->addSql('ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES `attachments` (id) ON DELETE SET NULL'); $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E2F180363 FOREIGN KEY (id_assembly) REFERENCES assemblies (id)'); $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES `parts` (id)'); + $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES `projects` (id)'); $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id)'); } @@ -32,6 +32,7 @@ public function mySQLDown(Schema $schema): void $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0EA7100A1'); $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E2F180363'); $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EC22F6CC4'); + $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EF12E799E'); $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E3FFDCD60'); $this->addSql('DROP TABLE assemblies'); $this->addSql('DROP TABLE assembly_bom_entries'); @@ -70,6 +71,7 @@ public function sqLiteUp(Schema $schema): void id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_assembly INTEGER DEFAULT NULL, id_part INTEGER DEFAULT NULL, + id_project INTEGER DEFAULT NULL, price_currency_id INTEGER DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames CLOB NOT NULL, @@ -80,6 +82,7 @@ public function sqLiteUp(Schema $schema): void datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_8C74887E4AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE ) SQL); @@ -89,6 +92,9 @@ public function sqLiteUp(Schema $schema): void $this->addSql(<<<'SQL' CREATE INDEX IDX_8C74887EC22F6CC4 ON assembly_bom_entries (id_part) SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887EF12E799E ON assembly_bom_entries (id_project) + SQL); $this->addSql(<<<'SQL' CREATE INDEX IDX_8C74887E3FFDCD60 ON assembly_bom_entries (price_currency_id) SQL); @@ -106,11 +112,104 @@ public function sqLiteDown(Schema $schema): void public function postgreSQLUp(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + CREATE TABLE assemblies ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + comment TEXT NOT NULL, + not_selectable BOOLEAN NOT NULL, + alternative_names TEXT DEFAULT NULL, + order_quantity INT NOT NULL, + status VARCHAR(64) DEFAULT NULL, + order_only_missing_parts BOOLEAN NOT NULL, + description TEXT NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, + PRIMARY KEY(id) + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_5F3832C0727ACA70 ON assemblies (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_5F3832C0EA7100A1 ON assemblies (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE assembly_bom_entries ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + id_assembly INT DEFAULT NULL, + id_part INT DEFAULT NULL, + id_project INT DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames TEXT NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment TEXT NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + price_currency_id INT DEFAULT NULL, + last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY(id) + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E4AD2039E ON assembly_bom_entries (id_assembly) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887EC22F6CC4 ON assembly_bom_entries (id_part) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887EF12E799E ON assembly_bom_entries (id_project) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E3FFDCD60 ON assembly_bom_entries (price_currency_id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E4AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES "projects" (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); } public function postgreSQLDown(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP CONSTRAINT FK_5F3832C0727ACA70 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP CONSTRAINT FK_5F3832C0EA7100A1 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887E4AD2039E + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887EC22F6CC4 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887EF12E799E + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887E3FFDCD60 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assemblies + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assembly_bom_entries + SQL); } } diff --git a/migrations/Version20250304154507.php b/migrations/Version20250304154507.php index 37cf2fec3..904a3b658 100644 --- a/migrations/Version20250304154507.php +++ b/migrations/Version20250304154507.php @@ -386,11 +386,27 @@ public function sqLiteDown(Schema $schema): void public function postgreSQLUp(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON parts (built_assembly_id) + SQL); } public function postgreSQLDown(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FECC660B3C + SQL); + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_6940A7FECC660B3C + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE "parts" DROP built_assembly_id + SQL); } } diff --git a/migrations/Version20250310160354.php b/migrations/Version20250310160354.php deleted file mode 100644 index 201e7e85a..000000000 --- a/migrations/Version20250310160354.php +++ /dev/null @@ -1,188 +0,0 @@ -addSql('ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e2f180363 TO IDX_8C74887E4AD2039E'); - $this->addSql('ALTER TABLE project_bom_entries ADD id_assembly INT DEFAULT NULL AFTER id_part'); - $this->addSql('ALTER TABLE project_bom_entries ADD CONSTRAINT FK_1AA2DD314AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id)'); - $this->addSql('CREATE INDEX IDX_1AA2DD314AD2039E ON project_bom_entries (id_assembly)'); - } - - public function mySQLDown(Schema $schema): void - { - $this->addSql('ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e4ad2039e TO IDX_8C74887E2F180363'); - $this->addSql('ALTER TABLE project_bom_entries DROP FOREIGN KEY FK_1AA2DD314AD2039E'); - $this->addSql('DROP INDEX IDX_1AA2DD314AD2039E ON project_bom_entries'); - $this->addSql('ALTER TABLE project_bom_entries DROP id_assembly'); - } - - public function sqLiteUp(Schema $schema): void - { - $this->addSql(<<<'SQL' - CREATE TEMPORARY TABLE __temp__project_bom_entries AS - SELECT - id, - id_device, - id_part, - price_currency_id, - quantity, - mountnames, - name, - comment, - price, - last_modified, - datetime_added - FROM project_bom_entries - SQL); - - $this->addSql('DROP TABLE project_bom_entries'); - - $this->addSql(<<<'SQL' - CREATE TABLE project_bom_entries - ( - id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - id_device INTEGER DEFAULT NULL, - id_assembly INTEGER DEFAULT NULL, - id_part INTEGER DEFAULT NULL, - price_currency_id INTEGER DEFAULT NULL, - quantity DOUBLE PRECISION NOT NULL, - mountnames CLOB NOT NULL, - name VARCHAR(255) DEFAULT NULL, - comment CLOB NOT NULL, - price NUMERIC(11, 5) DEFAULT NULL, - last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - CONSTRAINT FK_1AA2DD312F180363 FOREIGN KEY (id_device) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_1AA2DD314AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_1AA2DD31C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE - ) - SQL); - - - $this->addSql(<<<'SQL' - INSERT INTO project_bom_entries ( - id, - id_device, - id_part, - price_currency_id, - quantity, - mountnames, - name, - comment, - price, - last_modified, - datetime_added - ) SELECT * FROM __temp__project_bom_entries - SQL); - $this->addSql('DROP TABLE __temp__project_bom_entries'); - - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device) - SQL); - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part) - SQL); - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD314AD2039E ON project_bom_entries (id_assembly) - SQL); - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id) - SQL); - } - - public function sqLiteDown(Schema $schema): void - { - $this->addSql(<<<'SQL' - CREATE TEMPORARY TABLE __temp__project_bom_entries AS - SELECT - id, - id_device, - id_part, - price_currency_id, - quantity, - mountnames, - name, - comment, - price, - last_modified, - datetime_added - FROM project_bom_entries - SQL); - - $this->addSql('DROP TABLE project_bom_entries'); - - $this->addSql(<<<'SQL' - CREATE TABLE project_bom_entries - ( - id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - id_device INTEGER DEFAULT NULL, - id_part INTEGER DEFAULT NULL, - price_currency_id INTEGER DEFAULT NULL, - quantity DOUBLE PRECISION NOT NULL, - mountnames CLOB NOT NULL, - name VARCHAR(255) DEFAULT NULL, - comment CLOB NOT NULL, - price NUMERIC(11, 5) DEFAULT NULL, - last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - CONSTRAINT FK_1AA2DD312F180363 FOREIGN KEY (id_device) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_1AA2DD31C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE - ) - SQL); - - $this->addSql(<<<'SQL' - INSERT INTO project_bom_entries ( - id, - id_device, - id_part, - price_currency_id, - quantity, - mountnames, - name, - comment, - price, - last_modified, - datetime_added - ) SELECT * FROM __temp__project_bom_entries - SQL); - - $this->addSql('DROP TABLE __temp__project_bom_entries'); - - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device) - SQL); - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part) - SQL); - $this->addSql(<<<'SQL' - CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id) - SQL); - } - - public function postgreSQLUp(Schema $schema): void - { - //Not needed - } - - public function postgreSQLDown(Schema $schema): void - { - //Not needed - } -} diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 09792951a..4335492e8 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -22,9 +22,9 @@ namespace App\Controller; -use App\Entity\AssemblySystem\Assembly; use App\Entity\Parameters\AbstractParameter; -use App\Services\Attachments\AssemblyPreviewGenerator; +use App\Entity\ProjectSystem\Project; +use App\Services\Attachments\ProjectPreviewGenerator; use Symfony\Component\HttpFoundation\Response; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Category; @@ -151,29 +151,29 @@ public function parts( 'footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '', 'description' => mb_strimwidth($part->getDescription(), 0, 127, '...'), 'image' => $preview_url, - ]; + ]; } return new JsonResponse($data); } - #[Route(path: '/assemblies/search/{query}', name: 'typeahead_assemblies')] - public function assemblies( - EntityManagerInterface $entityManager, - AssemblyPreviewGenerator $assemblyPreviewGenerator, - AttachmentURLGenerator $attachmentURLGenerator, - string $query = "" + #[Route(path: '/projects/search/{query}', name: 'typeahead_projects')] + public function projects( + EntityManagerInterface $entityManager, + ProjectPreviewGenerator $projectPreviewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" ): JsonResponse { - $this->denyAccessUnlessGranted('@assemblies.read'); + $this->denyAccessUnlessGranted('@projects.read'); $result = []; - $assemblyRepository = $entityManager->getRepository(Assembly::class); + $projectRepository = $entityManager->getRepository(Project::class); - $assemblies = $assemblyRepository->autocompleteSearch($query, 100); + $projects = $projectRepository->autocompleteSearch($query, 100); - foreach ($assemblies as $assembly) { - $preview_attachment = $assemblyPreviewGenerator->getTablePreviewAttachment($assembly); + foreach ($projects as $project) { + $preview_attachment = $projectPreviewGenerator->getTablePreviewAttachment($project); if($preview_attachment instanceof Attachment) { $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); @@ -181,13 +181,13 @@ public function assemblies( $preview_url = ''; } - /** @var Assembly $assembly */ + /** @var Project $project */ $result[] = [ - 'id' => $assembly->getID(), - 'name' => $this->translator->trans('typeahead.parts.assembly.name', ['%name%' => $assembly->getName()]), + 'id' => $project->getID(), + 'name' => $project->getName(), 'category' => '', 'footprint' => '', - 'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'), + 'description' => mb_strimwidth($project->getDescription(), 0, 127, '...'), 'image' => $preview_url, ]; } diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php index a953179a9..b31c72bc0 100644 --- a/src/DataTables/AssemblyBomEntriesDataTable.php +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -25,11 +25,13 @@ use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Helpers\ProjectDataTableHelper; use App\DataTables\Helpers\ColumnSortHelper; use App\DataTables\Helpers\PartDataTableHelper; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\ProjectSystem\Project; use App\Services\EntityURLGenerator; use App\Services\Formatters\AmountFormatter; use Doctrine\ORM\QueryBuilder; @@ -43,12 +45,13 @@ class AssemblyBomEntriesDataTable implements DataTableTypeInterface { public function __construct( - protected TranslatorInterface $translator, - protected PartDataTableHelper $partDataTableHelper, - protected EntityURLGenerator $entityURLGenerator, - protected AmountFormatter $amountFormatter, - private string $visible_columns, - private ColumnSortHelper $csh + protected TranslatorInterface $translator, + protected PartDataTableHelper $partDataTableHelper, + protected ProjectDataTableHelper $projectDataTableHelper, + protected EntityURLGenerator $entityURLGenerator, + protected AmountFormatter $amountFormatter, + private string $visible_columns, + private ColumnSortHelper $csh ){ } @@ -86,18 +89,29 @@ public function configure(DataTable $dataTable, array $options): void 'label' => $this->translator->trans('part.table.name'), 'orderField' => 'NATSORT(part.name)', 'render' => function ($value, AssemblyBOMEntry $context) { - if(!$context->getPart() instanceof Part) { + if(!$context->getPart() instanceof Part && !$context->getProject() instanceof Project) { return htmlspecialchars((string) $context->getName()); } - //Part exists if we reach this point - - $tmp = $this->partDataTableHelper->renderName($context->getPart()); - if($context->getName() !== null && $context->getName() !== '') { - $tmp .= '
                                                        '.htmlspecialchars($context->getName()).''; + if ($context->getPart() !== null) { + $tmp = $this->partDataTableHelper->renderName($context->getPart()); + $tmp = $this->translator->trans('part.table.name.value.for_part', ['%value%' => $tmp]); + + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
                                                        '.htmlspecialchars($context->getName()).''; + } + } elseif ($context->getProject() !== null) { + $tmp = $this->projectDataTableHelper->renderName($context->getProject()); + $tmp = $this->translator->trans('part.table.name.value.for_project', ['%value%' => $tmp]); + + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
                                                        '.htmlspecialchars($context->getName()).''; + } } + return $tmp; }, + ]) ->add('ipn', TextColumn::class, [ 'label' => $this->translator->trans('part.table.ipn'), diff --git a/src/DataTables/Helpers/AssemblyDataTableHelper.php b/src/DataTables/Helpers/ProjectDataTableHelper.php similarity index 91% rename from src/DataTables/Helpers/AssemblyDataTableHelper.php rename to src/DataTables/Helpers/ProjectDataTableHelper.php index 36f7836b7..0118d5d56 100644 --- a/src/DataTables/Helpers/AssemblyDataTableHelper.php +++ b/src/DataTables/Helpers/ProjectDataTableHelper.php @@ -23,18 +23,18 @@ namespace App\DataTables\Helpers; -use App\Entity\AssemblySystem\Assembly; +use App\Entity\ProjectSystem\Project; use App\Services\EntityURLGenerator; /** * A helper service which contains common code to render columns for assembly related tables */ -class AssemblyDataTableHelper +class ProjectDataTableHelper { public function __construct(private readonly EntityURLGenerator $entityURLGenerator) { } - public function renderName(Assembly $context): string + public function renderName(Project $context): string { $icon = ''; diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index 1c7a09e49..89572a8ab 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -25,9 +25,7 @@ use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; -use App\DataTables\Helpers\AssemblyDataTableHelper; use App\DataTables\Helpers\PartDataTableHelper; -use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\ProjectBOMEntry; @@ -46,7 +44,6 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface public function __construct( protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, - protected AssemblyDataTableHelper $assemblyDataTableHelper, protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter ) { @@ -90,26 +87,16 @@ public function configure(DataTable $dataTable, array $options): void 'label' => $this->translator->trans('part.table.name'), 'orderField' => 'NATSORT(part.name)', 'render' => function ($value, ProjectBOMEntry $context) { - if(!$context->getPart() instanceof Part && !$context->getAssembly() instanceof Assembly) { + if(!$context->getPart() instanceof Part) { return htmlspecialchars((string) $context->getName()); } - if ($context->getPart() !== null) { - $tmp = $this->partDataTableHelper->renderName($context->getPart()); - $tmp = $this->translator->trans('part.table.name.value.for_part', ['%value%' => $tmp]); - - if($context->getName() !== null && $context->getName() !== '') { - $tmp .= '
                                                        '.htmlspecialchars($context->getName()).''; - } - } elseif ($context->getAssembly() !== null) { - $tmp = $this->assemblyDataTableHelper->renderName($context->getAssembly()); - $tmp = $this->translator->trans('part.table.name.value.for_assembly', ['%value%' => $tmp]); - - if($context->getName() !== null && $context->getName() !== '') { - $tmp .= '
                                                        '.htmlspecialchars($context->getName()).''; - } - } + //Part exists if we reach this point + $tmp = $this->partDataTableHelper->renderName($context->getPart()); + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
                                                        '.htmlspecialchars($context->getName()).''; + } return $tmp; }, ]) @@ -121,6 +108,8 @@ public function configure(DataTable $dataTable, array $options): void if($context->getPart() instanceof Part) { return $context->getPart()->getIpn(); } + + return ''; } ]) ->add('description', MarkdownColumn::class, [ diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 17a6868f0..54305a6fa 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -22,7 +22,6 @@ namespace App\Entity\AssemblySystem; -use App\Repository\AssemblyRepository; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; @@ -57,7 +56,7 @@ * * @extends AbstractStructuralDBElement */ -#[ORM\Entity(repositoryClass: AssemblyRepository::class)] +#[ORM\Entity] #[ORM\Table(name: 'assemblies')] #[ApiResource( operations: [ @@ -108,6 +107,7 @@ class Assembly extends AbstractStructuralDBElement #[Groups(['extended', 'full', 'import'])] #[ORM\OneToMany(mappedBy: 'assembly', targetEntity: AssemblyBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])] + #[UniqueObjectCollection(message: 'assembly.bom_entry.project_already_in_bom', fields: ['project'])] #[UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name'])] protected Collection $bom_entries; diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 375fef040..f6ede2194 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -36,7 +36,7 @@ use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Contracts\TimeStampableInterface; -use App\Entity\AssemblySystem\Assembly; +use App\Entity\ProjectSystem\Project; use App\Repository\DBElementRepository; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; @@ -133,6 +133,18 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt #[Groups(['bom_entry:read', 'bom_entry:write', 'full'])] protected ?Part $part = null; + /** + * @var Project|null The associated project + */ + #[Assert\Expression( + '(this.getPart() === null or this.getProject() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))', + message: 'validator.project.bom_entry.only_part_or_assembly_allowed' + )] + #[ORM\ManyToOne(targetEntity: Project::class)] + #[ORM\JoinColumn(name: 'id_project', nullable: true)] + #[Groups(['bom_entry:read', 'bom_entry:write', ])] + protected ?Project $project = null; + /** * @var BigDecimal|null The price of this non-part BOM entry */ @@ -225,6 +237,17 @@ public function setPart(?Part $part): AssemblyBOMEntry return $this; } + public function getProject(): ?Project + { + return $this->project; + } + + public function setProject(?Project $project): AssemblyBOMEntry + { + $this->project = $project; + return $this; + } + /** * Returns the price of this BOM entry, if existing. * Prices are only valid on non-Part BOM entries. @@ -297,6 +320,7 @@ public function getComparableFields(): array return [ 'name' => $this->getName(), 'part' => $this->getPart()?->getID(), + 'project' => $this->getProject()?->getID(), ]; } } diff --git a/src/Entity/ProjectSystem/Project.php b/src/Entity/ProjectSystem/Project.php index 36a96377b..a103d6946 100644 --- a/src/Entity/ProjectSystem/Project.php +++ b/src/Entity/ProjectSystem/Project.php @@ -108,7 +108,6 @@ class Project extends AbstractStructuralDBElement #[Groups(['extended', 'full', 'import'])] #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part'])] - #[UniqueObjectCollection(message: 'project.bom_entry.assembly_already_in_bom', fields: ['assembly'])] #[UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name'])] protected Collection $bom_entries; diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index 692407730..f58e4d5eb 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -35,7 +35,6 @@ use ApiPlatform\OpenApi\Model\Operation; use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; -use App\Entity\AssemblySystem\Assembly; use App\Entity\Contracts\TimeStampableInterface; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; @@ -104,10 +103,7 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte /** * @var string|null An optional name describing this BOM entry (useful for non-part entries) */ - #[Assert\Expression( - 'this.getPart() !== null or this.getAssembly() !== null or (this.getName() !== null and this.getName() != "")', - message: 'validator.project.bom_entry.part_or_assembly_needed' - )] + #[Assert\Expression('this.getPart() !== null or this.getName() !== null', message: 'validator.project.bom_entry.name_or_part_needed')] #[ORM\Column(type: Types::STRING, nullable: true)] #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] protected ?string $name = null; @@ -135,18 +131,6 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte #[Groups(['bom_entry:read', 'bom_entry:write', 'full'])] protected ?Part $part = null; - /** - * @var Assembly|null The associated assembly - */ - #[Assert\Expression( - '(this.getPart() === null or this.getAssembly() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))', - message: 'validator.project.bom_entry.only_part_or_assembly_allowed' - )] - #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'assembly_bom_entries')] - #[ORM\JoinColumn(name: 'id_assembly')] - #[Groups(['bom_entry:read', 'bom_entry:write', ])] - protected ?Assembly $assembly = null; - /** * @var BigDecimal|null The price of this non-part BOM entry */ @@ -239,16 +223,6 @@ public function setPart(?Part $part): ProjectBOMEntry return $this; } - public function getAssembly(): ?Assembly - { - return $this->assembly; - } - - public function setAssembly(?Assembly $assembly): void - { - $this->assembly = $assembly; - } - /** * Returns the price of this BOM entry, if existing. * Prices are only valid on non-Part BOM entries. @@ -286,15 +260,6 @@ public function isPartBomEntry(): bool return $this->part instanceof Part; } - /** - * Checks whether this BOM entry is a assembly associated BOM entry or not. - * @return bool True if this BOM entry is a assembly associated BOM entry, false otherwise. - */ - public function isAssemblyBomEntry(): bool - { - return $this->assembly instanceof Assembly; - } - #[Assert\Callback] public function validate(ExecutionContextInterface $context, $payload): void { @@ -356,7 +321,6 @@ public function getComparableFields(): array return [ 'name' => $this->getName(), 'part' => $this->getPart()?->getID(), - 'assembly' => $this->getAssembly()?->getID(), ]; } } diff --git a/src/Form/AssemblySystem/AssemblyBOMEntryType.php b/src/Form/AssemblySystem/AssemblyBOMEntryType.php index 9addccb34..42d463bd9 100644 --- a/src/Form/AssemblySystem/AssemblyBOMEntryType.php +++ b/src/Form/AssemblySystem/AssemblyBOMEntryType.php @@ -8,6 +8,7 @@ use App\Form\Type\BigDecimalNumberType; use App\Form\Type\CurrencyEntityType; use App\Form\Type\PartSelectType; +use App\Form\Type\ProjectSelectType; use App\Form\Type\RichTextEditorType; use App\Form\Type\SIUnitType; use Symfony\Component\Form\AbstractType; @@ -34,11 +35,13 @@ public function buildForm(FormBuilderInterface $builder, array $options): void }); $builder - ->add('part', PartSelectType::class, [ 'required' => false, ]) - + ->add('project', ProjectSelectType::class, [ + 'label' => 'assembly.bom.project', + 'required' => false, + ]) ->add('name', TextType::class, [ 'label' => 'assembly.bom.name', 'required' => false, @@ -75,10 +78,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'label' => false, 'short' => true, - ]) - - ; - + ] + ); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Form/AssemblySystem/AssemblyBuildType.php b/src/Form/AssemblySystem/AssemblyBuildType.php index 8838706d4..e87acb868 100644 --- a/src/Form/AssemblySystem/AssemblyBuildType.php +++ b/src/Form/AssemblySystem/AssemblyBuildType.php @@ -78,35 +78,34 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, ]); - - //The form is initially empty, we have to define the fields after we know the data + //The form is initially empty, define the fields after we know the data $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { $form = $event->getForm(); - /** @var AssemblyBuildRequest $build_request */ - $build_request = $event->getData(); + /** @var AssemblyBuildRequest $assemblyBuildRequest */ + $assemblyBuildRequest = $event->getData(); $form->add('addBuildsToBuildsPart', CheckboxType::class, [ 'label' => 'assembly.build.add_builds_to_builds_part', 'required' => false, - 'disabled' => !$build_request->getAssembly()->getBuildPart() instanceof Part, + 'disabled' => !$assemblyBuildRequest->getAssembly()->getBuildPart() instanceof Part, ]); - if ($build_request->getAssembly()->getBuildPart() instanceof Part) { + if ($assemblyBuildRequest->getAssembly()->getBuildPart() instanceof Part) { $form->add('buildsPartLot', PartLotSelectType::class, [ 'label' => 'assembly.build.builds_part_lot', 'required' => false, - 'part' => $build_request->getAssembly()->getBuildPart(), + 'part' => $assemblyBuildRequest->getAssembly()->getBuildPart(), 'placeholder' => 'assembly.build.buildsPartLot.new_lot' ]); } - foreach ($build_request->getPartBomEntries() as $bomEntry) { + foreach ($assemblyBuildRequest->getPartBomEntries() as $bomEntry) { //Every part lot has a field to specify the number of parts to take from this lot - foreach ($build_request->getPartLotsForBOMEntry($bomEntry) as $lot) { + foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($bomEntry) as $lot) { $form->add('lot_' . $lot->getID(), SIUnitType::class, [ 'label' => false, 'measurement_unit' => $bomEntry->getPart()->getPartUnit(), - 'max' => min($build_request->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()), + 'max' => min($assemblyBuildRequest->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()), 'disabled' => !$this->security->isGranted('withdraw', $lot), ]); } diff --git a/src/Form/ProjectSystem/ProjectAddPartsType.php b/src/Form/ProjectSystem/ProjectAddPartsType.php index c5dbe99f3..61f72c41d 100644 --- a/src/Form/ProjectSystem/ProjectAddPartsType.php +++ b/src/Form/ProjectSystem/ProjectAddPartsType.php @@ -59,7 +59,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ], 'constraints' => [ new UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part']), - new UniqueObjectCollection(message: 'project.bom_entry.assembly_already_in_bom', fields: ['assembly']), new UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name']), ] ]); diff --git a/src/Form/ProjectSystem/ProjectBOMEntryType.php b/src/Form/ProjectSystem/ProjectBOMEntryType.php index de8eb789c..44850c304 100644 --- a/src/Form/ProjectSystem/ProjectBOMEntryType.php +++ b/src/Form/ProjectSystem/ProjectBOMEntryType.php @@ -5,7 +5,6 @@ namespace App\Form\ProjectSystem; use App\Entity\ProjectSystem\ProjectBOMEntry; -use App\Form\Type\AssemblySelectType; use App\Form\Type\BigDecimalNumberType; use App\Form\Type\CurrencyEntityType; use App\Form\Type\PartSelectType; @@ -39,10 +38,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'project.bom.part', 'required' => false, ]) - ->add('assembly', AssemblySelectType::class, [ - 'label' => 'project.bom.assembly', - 'required' => false, - ]) ->add('name', TextType::class, [ 'label' => 'project.bom.name', 'required' => false, diff --git a/src/Form/ProjectSystem/ProjectBuildType.php b/src/Form/ProjectSystem/ProjectBuildType.php index d0d4e3433..b13dd12f2 100644 --- a/src/Form/ProjectSystem/ProjectBuildType.php +++ b/src/Form/ProjectSystem/ProjectBuildType.php @@ -22,7 +22,6 @@ */ namespace App\Form\ProjectSystem; -use App\Helpers\Assemblies\AssemblyBuildRequest; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; @@ -39,11 +38,10 @@ use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Contracts\Translation\TranslatorInterface; class ProjectBuildType extends AbstractType implements DataMapperInterface { - public function __construct(private readonly Security $security, private readonly TranslatorInterface $translator) + public function __construct(private readonly Security $security) { } @@ -113,25 +111,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]); } } - - foreach ($projectBuildRequest->getAssemblyBomEntries() as $bomEntry) { - $assemblyBuildRequest = new AssemblyBuildRequest($bomEntry->getAssembly(), $projectBuildRequest->getNumberOfBuilds()); - - //Add fields for assembly bom entries - foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) { - foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $lot) { - $form->add('lot_' . $lot->getID(), SIUnitType::class, [ - 'label' => $this->translator->trans('project.build.builds_part_lot_label', [ - '%name%' => $partBomEntry->getPart()->getName(), - '%quantity%' => $partBomEntry->getQuantity() * $projectBuildRequest->getNumberOfBuilds() - ]), - 'measurement_unit' => $partBomEntry->getPart()->getPartUnit(), - 'max' => min($assemblyBuildRequest->getNeededAmountForBOMEntry($partBomEntry), $lot->getAmount()), - 'disabled' => !$this->security->isGranted('withdraw', $lot), - ]); - } - } - } }); } diff --git a/src/Form/Type/AssemblySelectType.php b/src/Form/Type/ProjectSelectType.php similarity index 82% rename from src/Form/Type/AssemblySelectType.php rename to src/Form/Type/ProjectSelectType.php index 10e858f26..18a10c08c 100644 --- a/src/Form/Type/AssemblySelectType.php +++ b/src/Form/Type/ProjectSelectType.php @@ -4,9 +4,9 @@ namespace App\Form\Type; -use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; -use App\Services\Attachments\AssemblyPreviewGenerator; +use App\Entity\ProjectSystem\Project; +use App\Services\Attachments\ProjectPreviewGenerator; use App\Services\Attachments\AttachmentURLGenerator; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bridge\Doctrine\Form\Type\EntityType; @@ -20,9 +20,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -class AssemblySelectType extends AbstractType implements DataMapperInterface +class ProjectSelectType extends AbstractType implements DataMapperInterface { - public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em, private readonly AssemblyPreviewGenerator $previewGenerator, private readonly AttachmentURLGenerator $attachmentURLGenerator) + public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em, private readonly ProjectPreviewGenerator $previewGenerator, private readonly AttachmentURLGenerator $attachmentURLGenerator) { } @@ -69,28 +69,28 @@ public function buildForm(FormBuilderInterface $builder, array $options): void public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'class' => Assembly::class, + 'class' => Project::class, 'choice_label' => 'name', 'compound' => true, 'error_bubbling' => false, ]); - error_log($this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__'])); + error_log($this->urlGenerator->generate('typeahead_projects', ['query' => '__QUERY__'])); $resolver->setDefaults([ 'attr' => [ - 'data-controller' => 'elements--assembly-select', - 'data-autocomplete' => $this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']), + 'data-controller' => 'elements--project-select', + 'data-autocomplete' => $this->urlGenerator->generate('typeahead_projects', ['query' => '__QUERY__']), 'autocomplete' => 'off', ], ]); $resolver->setDefaults([ //Prefill the selected choice with the needed data, so the user can see it without an additional Ajax request - 'choice_attr' => ChoiceList::attr($this, function (?Assembly $assembly) { - if($assembly instanceof Assembly) { + 'choice_attr' => ChoiceList::attr($this, function (?Project $project) { + if($project instanceof Project) { //Determine the picture to show: - $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($assembly); + $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($project); if ($preview_attachment instanceof Attachment) { $preview_url = $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); @@ -99,8 +99,8 @@ public function configureOptions(OptionsResolver $resolver): void } } - return $assembly instanceof Assembly ? [ - 'data-description' => $assembly->getDescription() ? mb_strimwidth($assembly->getDescription(), 0, 127, '...') : '', + return $project instanceof Project ? [ + 'data-description' => $project->getDescription() ? mb_strimwidth($project->getDescription(), 0, 127, '...') : '', 'data-category' => '', 'data-footprint' => '', 'data-image' => $preview_url, diff --git a/src/Helpers/Projects/ProjectBuildRequest.php b/src/Helpers/Projects/ProjectBuildRequest.php index 3254565a5..24bb5eb78 100644 --- a/src/Helpers/Projects/ProjectBuildRequest.php +++ b/src/Helpers/Projects/ProjectBuildRequest.php @@ -22,13 +22,10 @@ */ namespace App\Helpers\Projects; -use App\Entity\AssemblySystem\Assembly; -use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; -use App\Helpers\Assemblies\AssemblyBuildRequest; use App\Validator\Constraints\ProjectSystem\ValidProjectBuildRequest; /** @@ -82,7 +79,7 @@ private function initializeArray(): void //Completely reset the array $this->withdraw_amounts = []; - //Now create an array for each part BOM entry + //Now create an array for each BOM entry foreach ($this->getPartBomEntries() as $bom_entry) { $remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry); foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) { @@ -91,21 +88,6 @@ private function initializeArray(): void $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]); } } - - //Now create an array for each assembly BOM entry - foreach ($this->getAssemblyBomEntries() as $assemblyBomEntry) { - $assemblyBuildRequest = new AssemblyBuildRequest($assemblyBomEntry->getAssembly(), $this->number_of_builds); - - //Add fields for assembly bom entries - foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) { - $remaining_amount = $assemblyBuildRequest->getNeededAmountForBOMEntry($partBomEntry) * $assemblyBomEntry->getQuantity(); - - foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $lot) { - $this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount()); - $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]); - } - } - } } /** @@ -248,77 +230,12 @@ public function getPartLotsForBOMEntry(ProjectBOMEntry $projectBOMEntry): ?array { $this->ensureBOMEntryValid($projectBOMEntry); - if (!$projectBOMEntry->getPart() instanceof Part && !$projectBOMEntry->getAssembly() instanceof Assembly) { + if (!$projectBOMEntry->getPart() instanceof Part) { return null; } //Filter out all lots which have unknown instock - if ($projectBOMEntry->getPart() instanceof Part) { - return $projectBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); - } elseif ($projectBOMEntry->getAssembly() instanceof Assembly) { - $assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds); - - //Add fields for assembly bom entries - $result = []; - foreach ($assemblyBuildRequest->getPartBomEntries() as $assemblyBOMEntry) { - $tmp = $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); - $result = array_merge($result, $tmp); - } - - return $result; - } - - return null; - } - - /** - * Returns all available assembly BOM-entries with no part assigned. - * @return AssemblyBOMEntry[]|null Returns null if no entries found - */ - public function getAssemblyBomEntriesWithoutPart(ProjectBOMEntry $projectBOMEntry): ?array - { - $this->ensureBOMEntryValid($projectBOMEntry); - - if (!$projectBOMEntry->getAssembly() instanceof Assembly) { - return null; - } - - $assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds); - - $result = []; - - foreach ($assemblyBuildRequest->getBomEntries() as $assemblyBOMEntry) { - if ($assemblyBOMEntry->getPart() === null) { - $result[] = $assemblyBOMEntry; - } - } - - return count($result) > 0 ? $result : null; - } - - /** - * Returns all available assembly BOM-entries with no part assigned. - * @return AssemblyBOMEntry[]|null Returns null if no entries found - */ - public function getAssemblyBomEntriesWithPartNoStock(ProjectBOMEntry $projectBOMEntry): ?array - { - $this->ensureBOMEntryValid($projectBOMEntry); - - if (!$projectBOMEntry->getAssembly() instanceof Assembly) { - return null; - } - - $assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds); - - $result = []; - - foreach ($assemblyBuildRequest->getBomEntries() as $assemblyBOMEntry) { - if ($assemblyBOMEntry->getPart() instanceof Part && $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->count() === 0) { - $result[] = $assemblyBOMEntry; - } - } - - return count($result) > 0 ? $result : null; + return $projectBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); } /** @@ -349,15 +266,6 @@ public function getPartBomEntries(): array return $this->project->getBomEntries()->filter(fn(ProjectBOMEntry $entry) => $entry->isPartBomEntry())->toArray(); } - /** - * Returns the all assembly bom entries that have to be built. - * @return ProjectBOMEntry[] - */ - public function getAssemblyBomEntries(): array - { - return $this->project->getBomEntries()->filter(fn(ProjectBOMEntry $entry) => $entry->isAssemblyBomEntry())->toArray(); - } - /** * Returns which project should be build */ diff --git a/src/Repository/AssemblyRepository.php b/src/Repository/AssemblyRepository.php deleted file mode 100644 index 031e6e82b..000000000 --- a/src/Repository/AssemblyRepository.php +++ /dev/null @@ -1,69 +0,0 @@ -. - */ - -declare(strict_types=1); - -/** - * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). - * - * Copyright (C) 2019 - 2022 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 . - */ - -namespace App\Repository; - -use App\Entity\AssemblySystem\Assembly; - -/** - * @template TEntityClass of Assembly - * @extends DBElementRepository - */ -class AssemblyRepository extends StructuralDBElementRepository -{ - /** - * @return Assembly[] - */ - public function autocompleteSearch(string $query, int $max_limits = 50): array - { - $qb = $this->createQueryBuilder('assembly'); - $qb->select('assembly') - ->where('ILIKE(assembly.name, :query) = TRUE') - ->orWhere('ILIKE(assembly.description, :query) = TRUE'); - - $qb->setParameter('query', '%'.$query.'%'); - - $qb->setMaxResults($max_limits); - $qb->orderBy('NATSORT(assembly.name)', 'ASC'); - - return $qb->getQuery()->getResult(); - } -} \ No newline at end of file diff --git a/src/Repository/Parts/DeviceRepository.php b/src/Repository/Parts/DeviceRepository.php index 442c91e58..c714523af 100644 --- a/src/Repository/Parts/DeviceRepository.php +++ b/src/Repository/Parts/DeviceRepository.php @@ -51,4 +51,22 @@ public function getPartsCount(object $element): int //Prevent user from deleting devices, to not accidentally remove filled devices from old versions return 1; } + + /** + * @return Project[] + */ + public function autocompleteSearch(string $query, int $max_limits = 50): array + { + $qb = $this->createQueryBuilder('project'); + $qb->select('project') + ->where('ILIKE(project.name, :query) = TRUE') + ->orWhere('ILIKE(project.description, :query) = TRUE'); + + $qb->setParameter('query', '%'.$query.'%'); + + $qb->setMaxResults($max_limits); + $qb->orderBy('NATSORT(project.name)', 'ASC'); + + return $qb->getQuery()->getResult(); + } } diff --git a/src/Services/AssemblySystem/AssemblyBuildHelper.php b/src/Services/AssemblySystem/AssemblyBuildHelper.php index 8c95a4b66..3fb3221ac 100644 --- a/src/Services/AssemblySystem/AssemblyBuildHelper.php +++ b/src/Services/AssemblySystem/AssemblyBuildHelper.php @@ -27,14 +27,17 @@ use App\Entity\Parts\Part; use App\Helpers\Assemblies\AssemblyBuildRequest; use App\Services\Parts\PartLotWithdrawAddHelper; +use App\Services\ProjectSystem\ProjectBuildHelper; /** * @see \App\Tests\Services\AssemblySystem\AssemblyBuildHelperTest */ class AssemblyBuildHelper { - public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper) - { + public function __construct( + private readonly PartLotWithdrawAddHelper $withdraw_add_helper, + private readonly ProjectBuildHelper $projectBuildHelper + ) { } /** @@ -66,12 +69,16 @@ public function getMaximumBuildableCount(Assembly $assembly): int $maximum_buildable_count = PHP_INT_MAX; foreach ($assembly->getBomEntries() as $bom_entry) { //Skip BOM entries without a part (as we can not determine that) - if (!$bom_entry->isPartBomEntry()) { + if (!$bom_entry->isPartBomEntry() && $bom_entry->getProject() === null) { continue; } - //The maximum buildable count for the whole assembly is the minimum of all BOM entries - $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); + //The maximum buildable count for the whole project is the minimum of all BOM entries + if ($bom_entry->getPart() !== null) { + $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); + } elseif ($bom_entry->getProject() !== null) { + $maximum_buildable_count = min($maximum_buildable_count, $this->projectBuildHelper->getMaximumBuildableCount($bom_entry->getProject())); + } } return $maximum_buildable_count; @@ -97,7 +104,7 @@ public function isBOMEntryBuildable(AssemblyBOMEntry $bom_entry, int $number_of_ } /** - * Returns the assembly BOM entries for which parts are missing in the stock for the given number of builds + * Returns the project BOM entries for which parts are missing in the stock for the given number of builds * @param Assembly $assembly The assembly for which the BOM entries should be checked * @param int $number_of_builds How often should the assembly be build? * @return AssemblyBOMEntry[] @@ -108,24 +115,29 @@ public function getNonBuildableAssemblyBomEntries(Assembly $assembly, int $numbe throw new \InvalidArgumentException('The number of builds must be greater than 0!'); } - $non_buildable_entries = []; + $nonBuildableEntries = []; foreach ($assembly->getBomEntries() as $bomEntry) { $part = $bomEntry->getPart(); //Skip BOM entries without a part (as we can not determine that) - if (!$part instanceof Part) { + if (!$part instanceof Part && $bomEntry->getAssembly() === null) { continue; } - $amount_sum = $part->getAmountSum(); + if ($bomEntry->getPart() !== null) { + $amount_sum = $part->getAmountSum(); - if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { - $non_buildable_entries[] = $bomEntry; + if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { + $nonBuildableEntries[] = $bomEntry; + } + } elseif ($bomEntry->getAssembly() !== null) { + $nonBuildableAssemblyEntries = $this->projectBuildHelper->getNonBuildableProjectBomEntries($bomEntry->getProject(), $number_of_builds); + $nonBuildableEntries = array_merge($nonBuildableEntries, $nonBuildableAssemblyEntries); } } - return $non_buildable_entries; + return $nonBuildableEntries; } /** diff --git a/src/Services/Attachments/AssemblyPreviewGenerator.php b/src/Services/Attachments/ProjectPreviewGenerator.php similarity index 75% rename from src/Services/Attachments/AssemblyPreviewGenerator.php rename to src/Services/Attachments/ProjectPreviewGenerator.php index 9ecbbd070..9929dbd3c 100644 --- a/src/Services/Attachments/AssemblyPreviewGenerator.php +++ b/src/Services/Attachments/ProjectPreviewGenerator.php @@ -22,38 +22,38 @@ namespace App\Services\Attachments; -use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; +use App\Entity\ProjectSystem\Project; -class AssemblyPreviewGenerator +class ProjectPreviewGenerator { public function __construct(protected AttachmentManager $attachmentHelper) { } /** - * Returns a list of attachments that can be used for previewing the assembly ordered by priority. + * Returns a list of attachments that can be used for previewing the project ordered by priority. * - * @param Assembly $assembly the assembly for which the attachments should be determined + * @param Project $project the project for which the attachments should be determined * * @return (Attachment|null)[] * * @psalm-return list */ - public function getPreviewAttachments(Assembly $assembly): array + public function getPreviewAttachments(Project $project): array { $list = []; //Master attachment has top priority - $attachment = $assembly->getMasterPictureAttachment(); + $attachment = $project->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { $list[] = $attachment; } - //Then comes the other images of the assembly - foreach ($assembly->getAttachments() as $attachment) { + //Then comes the other images of the project + foreach ($project->getAttachments() as $attachment) { //Dont show the master attachment twice - if ($this->isAttachmentValidPicture($attachment) && $attachment !== $assembly->getMasterPictureAttachment()) { + if ($this->isAttachmentValidPicture($attachment) && $attachment !== $project->getMasterPictureAttachment()) { $list[] = $attachment; } } @@ -62,14 +62,14 @@ public function getPreviewAttachments(Assembly $assembly): array } /** - * Determines what attachment should be used for previewing a assembly (especially in assembly table). + * Determines what attachment should be used for previewing a project (especially in project table). * The returned attachment is guaranteed to be existing and be a picture. * - * @param Assembly $assembly The assembly for which the attachment should be determined + * @param Project $project The project for which the attachment should be determined */ - public function getTablePreviewAttachment(Assembly $assembly): ?Attachment + public function getTablePreviewAttachment(Project $project): ?Attachment { - $attachment = $assembly->getMasterPictureAttachment(); + $attachment = $project->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { return $attachment; } diff --git a/src/Services/ProjectSystem/ProjectBuildHelper.php b/src/Services/ProjectSystem/ProjectBuildHelper.php index d7ba9e6c9..269c7e4c2 100644 --- a/src/Services/ProjectSystem/ProjectBuildHelper.php +++ b/src/Services/ProjectSystem/ProjectBuildHelper.php @@ -22,13 +22,10 @@ */ namespace App\Services\ProjectSystem; -use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; -use App\Helpers\Assemblies\AssemblyBuildRequest; use App\Helpers\Projects\ProjectBuildRequest; -use App\Services\AssemblySystem\AssemblyBuildHelper; use App\Services\Parts\PartLotWithdrawAddHelper; /** @@ -36,10 +33,8 @@ */ class ProjectBuildHelper { - public function __construct( - private readonly PartLotWithdrawAddHelper $withdrawAddHelper, - private readonly AssemblyBuildHelper $assemblyBuildHelper - ) { + public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper) + { } /** @@ -71,16 +66,12 @@ public function getMaximumBuildableCount(Project $project): int $maximum_buildable_count = PHP_INT_MAX; foreach ($project->getBomEntries() as $bom_entry) { //Skip BOM entries without a part (as we can not determine that) - if (!$bom_entry->isPartBomEntry() && $bom_entry->getAssembly() === null) { + if (!$bom_entry->isPartBomEntry()) { continue; } //The maximum buildable count for the whole project is the minimum of all BOM entries - if ($bom_entry->getPart() !== null) { - $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); - } elseif ($bom_entry->getAssembly() !== null) { - $maximum_buildable_count = min($maximum_buildable_count, $this->assemblyBuildHelper->getMaximumBuildableCount($bom_entry->getAssembly())); - } + $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); } return $maximum_buildable_count; @@ -106,10 +97,10 @@ public function isBOMEntryBuildable(ProjectBOMEntry $bom_entry, int $number_of_b } /** - * Returns the project or assembly BOM entries for which parts are missing in the stock for the given number of builds + * Returns the project BOM entries for which parts are missing in the stock for the given number of builds * @param Project $project The project for which the BOM entries should be checked * @param int $number_of_builds How often should the project be build? - * @return ProjectBOMEntry[]|AssemblyBOMEntry[] + * @return ProjectBOMEntry[] */ public function getNonBuildableProjectBomEntries(Project $project, int $number_of_builds = 1): array { @@ -117,29 +108,24 @@ public function getNonBuildableProjectBomEntries(Project $project, int $number_o throw new \InvalidArgumentException('The number of builds must be greater than 0!'); } - $nonBuildableEntries = []; + $non_buildable_entries = []; foreach ($project->getBomEntries() as $bomEntry) { $part = $bomEntry->getPart(); //Skip BOM entries without a part (as we can not determine that) - if (!$part instanceof Part && $bomEntry->getAssembly() === null) { + if (!$part instanceof Part) { continue; } - if ($bomEntry->getPart() !== null) { - $amount_sum = $part->getAmountSum(); + $amount_sum = $part->getAmountSum(); - if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { - $nonBuildableEntries[] = $bomEntry; - } - } elseif ($bomEntry->getAssembly() !== null) { - $nonBuildableAssemblyEntries = $this->assemblyBuildHelper->getNonBuildableAssemblyBomEntries($bomEntry->getAssembly(), $number_of_builds); - $nonBuildableEntries = array_merge($nonBuildableEntries, $nonBuildableAssemblyEntries); + if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { + $non_buildable_entries[] = $bomEntry; } } - return $nonBuildableEntries; + return $non_buildable_entries; } /** @@ -147,37 +133,22 @@ public function getNonBuildableProjectBomEntries(Project $project, int $number_o * The ProjectBuildRequest has to be validated before!! * You have to flush changes to DB afterward */ - public function doBuild(ProjectBuildRequest $projectBuildRequest): void + public function doBuild(ProjectBuildRequest $buildRequest): void { - $message = $projectBuildRequest->getComment(); - $message .= ' (Project build: '.$projectBuildRequest->getProject()->getName().')'; + $message = $buildRequest->getComment(); + $message .= ' (Project build: '.$buildRequest->getProject()->getName().')'; - foreach ($projectBuildRequest->getPartBomEntries() as $bomEntry) { - foreach ($projectBuildRequest->getPartLotsForBOMEntry($bomEntry) as $partLot) { - $amount = $projectBuildRequest->getLotWithdrawAmount($partLot); + foreach ($buildRequest->getPartBomEntries() as $bom_entry) { + foreach ($buildRequest->getPartLotsForBOMEntry($bom_entry) as $part_lot) { + $amount = $buildRequest->getLotWithdrawAmount($part_lot); if ($amount > 0) { - $this->withdrawAddHelper->withdraw($partLot, $amount, $message); - } - } - } - - foreach ($projectBuildRequest->getAssemblyBomEntries() as $bomEntry) { - $assemblyBuildRequest = new AssemblyBuildRequest($bomEntry->getAssembly(), $projectBuildRequest->getNumberOfBuilds()); - - //Add fields for assembly bom entries - foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) { - foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $partLot) { - //Read amount from build configuration of the projectBuildRequest - $amount = $projectBuildRequest->getLotWithdrawAmount($partLot); - if ($amount > 0) { - $this->withdrawAddHelper->withdraw($partLot, $amount, $message); - } + $this->withdraw_add_helper->withdraw($part_lot, $amount, $message); } } } - if ($projectBuildRequest->getAddBuildsToBuildsPart()) { - $this->withdrawAddHelper->add($projectBuildRequest->getBuildsPartLot(), $projectBuildRequest->getNumberOfBuilds(), $message); + if ($buildRequest->getAddBuildsToBuildsPart()) { + $this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message); } } } diff --git a/src/Twig/AssemblyTwigExtension.php b/src/Twig/AssemblyTwigExtension.php index 3430f7d1d..d43c201e8 100644 --- a/src/Twig/AssemblyTwigExtension.php +++ b/src/Twig/AssemblyTwigExtension.php @@ -10,14 +10,14 @@ class AssemblyTwigExtension extends AbstractExtension public function getFunctions(): array { return [ - new TwigFunction('has_assembly', [$this, 'hasAssembly']), + new TwigFunction('has_project', [$this, 'hasProject']), ]; } - public function hasAssembly(array $bomEntries): bool + public function hasProject(array $bomEntries): bool { foreach ($bomEntries as $entry) { - if ($entry->getAssembly() !== null) { + if ($entry->getProject() !== null) { return true; } } diff --git a/templates/form/collection_types_layout.html.twig b/templates/form/collection_types_layout.html.twig index 552fd5421..96b71bf0f 100644 --- a/templates/form/collection_types_layout.html.twig +++ b/templates/form/collection_types_layout.html.twig @@ -6,7 +6,7 @@ {# expand button #} {% trans %}project.bom.quantity{% endtrans %} - {% trans %}project.bom.partOrAssembly{% endtrans %} + {% trans %}project.bom.part{% endtrans %} {% trans %}project.bom.name{% endtrans %} {# Remove button #} @@ -41,21 +41,9 @@ {{ form_widget(form.quantity) }} {{ form_errors(form.quantity) }} - - {{ form_row(form.part) }} + + {{ form_widget(form.part) }} {{ form_errors(form.part) }} - - {% if form.vars.value is not null and form.vars.value.project is not null %} - {% set hasAssembly = false %} - {% if is_granted("@assemblies.read") or has_assembly(form.vars.value.project.bomEntries.toArray) %} -
                                                        - {{ form_widget(form.assembly) }} - {{ form_errors(form.assembly) }} - {% endif %} - {% elseif is_granted("@assemblies.read") %} - {{ form_widget(form.assembly) }} - {{ form_errors(form.assembly) }} - {% endif %} {{ form_widget(form.name) }} diff --git a/templates/form/collection_types_layout_assembly.html.twig b/templates/form/collection_types_layout_assembly.html.twig index c5acebda0..24964801a 100644 --- a/templates/form/collection_types_layout_assembly.html.twig +++ b/templates/form/collection_types_layout_assembly.html.twig @@ -6,7 +6,7 @@ {# expand button #} {% trans %}assembly.bom.quantity{% endtrans %} - {% trans %}assembly.bom.part{% endtrans %} + {% trans %}assembly.bom.partOrProject{% endtrans %} {% trans %}assembly.bom.name{% endtrans %} {# Remove button #} @@ -41,9 +41,21 @@ {{ form_widget(form.quantity) }} {{ form_errors(form.quantity) }} - - {{ form_widget(form.part) }} + + {{ form_row(form.part) }} {{ form_errors(form.part) }} + + {% if form.vars.value is not null and form.vars.value.assembly is not null %} + {% if is_granted("@projects.read") or has_project(form.vars.value.assembly.bomEntries.toArray) %} +
                                                        + {{ form_widget(form.project) }} + {{ form_errors(form.project) }} + {% endif %} + {% elseif is_granted("@projects.read") %} +
                                                        + {{ form_widget(form.project) }} + {{ form_errors(form.project) }} + {% endif %} {{ form_widget(form.name) }} diff --git a/templates/projects/build/_form.html.twig b/templates/projects/build/_form.html.twig index 340b86700..b25ca81eb 100644 --- a/templates/projects/build/_form.html.twig +++ b/templates/projects/build/_form.html.twig @@ -27,9 +27,7 @@ {% if bom_entry.part %} - {{ 'projects.build.form.part'|trans({'%name%': bom_entry.part.name}) }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} - {% elseif bom_entry.assembly %} - {{ 'projects.build.form.assembly'|trans({'%name%': bom_entry.assembly.name}) }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} + {{ bom_entry.part.name }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} {% else %} {{ bom_entry.name }} {% endif %} @@ -47,29 +45,9 @@ {% set lots = build_request.partLotsForBOMEntry(bom_entry) %} - {% set assemblyBomEntriesWithoutPart = build_request.assemblyBomEntriesWithoutPart(bom_entry) %} - {% set assemblyBomEntriesWithPartNoStock = build_request.assemblyBomEntriesWithPartNoStock(bom_entry) %} {% if lots is not null %} - {% set previousLabel = null %} - {% for lot in lots %} {# @var lot \App\Entity\Parts\PartLot #} - - {% set label = '' %} - {% if form["lot_"~lot.id].vars.label is defined and form["lot_"~lot.id].vars.label is not empty %} - {% set label = form["lot_"~lot.id].vars.label %} - {% endif %} - - {% if label != '' and (previousLabel is null or label != previousLabel) %} -
                                                        - -
                                                        - {% endif %} - - {% set previousLabel = label %} -
                                                        -
                                                        +
                                                        / {{ lot.amount | format_amount(lot.part.partUnit) }} {% trans %}project.builds.stocked{% endtrans %}
                                                        {% endfor %} {% endif %} - {% if assemblyBomEntriesWithoutPart is not null %} - {% for bomEntryWithoutPart in assemblyBomEntriesWithoutPart %} -
                                                        - -
                                                        -
                                                        - / {% trans %}project.builds.no_stock{% endtrans %} -
                                                        -
                                                        - {% endfor %} - {% endif %} - {% if assemblyBomEntriesWithPartNoStock is not null %} - {% for bomEntryWithPartNoStock in assemblyBomEntriesWithPartNoStock %} -
                                                        -
                                                        - -
                                                        -
                                                        - / {% trans %}project.builds.no_stock{% endtrans %} -
                                                        -
                                                        -
                                                        - {% endfor %} - {% endif %} {% endfor %} @@ -126,7 +75,7 @@ {{ form_row(form.comment) }}
                                                        -{{ form_row(form.dontCheckQuantity) }} + {{ form_row(form.dontCheckQuantity) }}
                                                        {{ form_row(form.addBuildsToBuildsPart) }} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index c1d1b5177..c4021c2c5 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -4741,19 +4741,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Název
                                                        - - - project.bom.assembly - Sestava - - - - - project.bom.partOrAssembly - Výběr - - - + Part-DB1\src\DataTables\PartsDataTable.php:178 Part-DB1\src\DataTables\PartsDataTable.php:126 @@ -9798,18 +9786,6 @@ Element 3 Díl - - - project.bom.assembly - Baugruppe - - - - - project.bom.partOrAssembly - Auswahl - - project.bom.add_entry @@ -10200,12 +10176,6 @@ Element 3 k dispozici - - - project.builds.no_stock - není uveden žádný sklad - - project.builds.needed @@ -10278,12 +10248,6 @@ Element 3 Cílový inventář - - - project.build.builds_part_lot_label - %name% (%quantity% požadováno) - - project.builds.number_of_builds @@ -13557,10 +13521,10 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz %value% (Součást) - + - part.table.name.value.for_assembly - %value% (Sestava) + part.table.name.value.for_project + %value% (Projekt) @@ -13791,12 +13755,24 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz potřebné + + + assembly.bom.delete.confirm + Opravdu chcete tuto položku smazat? + + assembly.add_parts_to_assembly Přidat součásti do sestavy + + + assembly.bom.project + Projekt + + assembly.bom.name @@ -13833,9 +13809,9 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Importovat součásti do sestavy - + - assembly.bom.part + assembly.bom.partOrProject Součást @@ -14282,41 +14258,5 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz - - - typeahead.parts.part.name - %name% (součást) - - - - - typeahead.parts.assembly.name - %name% (sestava) - - - - - projects.build.form.part - Součást "%name%" - - - - - projects.build.form.assembly - Sestava "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (potřebné množství: %quantity%) - - - - - projects.build.form.assembly.bom.entry.no.stock - není skladem - - diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 4322fd9f8..24d42ac6a 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -4748,18 +4748,6 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Navn - - - project.bom.assembly - Montering - - - - - project.bom.partOrAssembly - Valg - - Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -9824,18 +9812,6 @@ Element 3 Komponent - - - project.bom.assembly - Baugruppe - - - - - project.bom.partOrAssembly - Auswahl - - project.bom.add_entry @@ -10226,12 +10202,6 @@ Element 3 På lager - - - project.builds.no_stock - intet lager angivet - - project.builds.needed @@ -10304,12 +10274,6 @@ Element 3 Mål mængde - - - project.build.builds_part_lot_label - %name% (%quantity% påkrævet) - - project.builds.number_of_builds @@ -12274,10 +12238,10 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver %value% (Del) - + - part.table.name.value.for_assembly - %value% (Samlingsenhed) + part.table.name.value.for_project + %value% (Projekt) @@ -12508,12 +12472,24 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver nødvendig + + + assembly.bom.delete.confirm + Vil du virkelig slette denne post? + + assembly.add_parts_to_assembly Tilføj dele til samlingen + + + assembly.bom.project + Projekt + + assembly.bom.name @@ -12550,9 +12526,9 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Importer dele til samling - + - assembly.bom.part + assembly.bom.partOrProject Del @@ -12999,41 +12975,5 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver - - - typeahead.parts.part.name - %name% (del) - - - - - typeahead.parts.assembly.name - %name% (samling) - - - - - projects.build.form.part - Del "%name%" - - - - - projects.build.form.assembly - Samling "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% nødvendig) - - - - - projects.build.form.assembly.bom.entry.no.stock - ikke på lager - - diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 8377facc9..c25fd7e77 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -4746,10 +4746,10 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr %value% (Bauteil) - + - part.table.name.value.for_assembly - %value% (Baugruppe) + part.table.name.value.for_project + %value% (Projekt) @@ -9800,18 +9800,6 @@ Element 1 -> Element 1.2 Bauteil - - - project.bom.assembly - Baugruppe - - - - - project.bom.partOrAssembly - Auswahl - - project.bom.add_entry @@ -10202,12 +10190,6 @@ Element 1 -> Element 1.2 vorhanden - - - project.builds.no_stock - kein Lager angegeben - - project.builds.needed @@ -10280,12 +10262,6 @@ Element 1 -> Element 1.2 Ziel-Bestand - - - project.build.builds_part_lot_label - %name% (%quantity% benötigt) - - project.builds.number_of_builds @@ -13151,12 +13127,24 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön benötigt + + + assembly.bom.delete.confirm + Wollen sie diesen Eintrag wirklich löschen? + + assembly.add_parts_to_assembly Bauteile zur Baugruppe hinzufügen + + + assembly.bom.project + Projekt + + assembly.bom.name @@ -13193,10 +13181,10 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Importiere Parts für Baugruppe - + - assembly.bom.part - Bauteil + assembly.bom.partOrProject + Bauteil oder Projekt @@ -13642,42 +13630,6 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön - - - typeahead.parts.part.name - %name% (Bauteil) - - - - - typeahead.parts.assembly.name - %name% (Baugruppe) - - - - - projects.build.form.part - Bauteil "%name%" - - - - - projects.build.form.assembly - Baugruppe "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% benötigt) - - - - - projects.build.form.assembly.bom.entry.no.stock - nicht auf Lager - - part.table.actions.error diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index c97f06304..a6dda3d47 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1541,22 +1541,10 @@ %value% (Μέρος) - + - part.table.name.value.for_assembly - %value% (Συναρμολόγηση) - - - - - project.bom.assembly - Συναρμολόγηση - - - - - project.bom.partOrAssembly - Επιλογή + part.table.name.value.for_project + %value% (Έργο) @@ -1595,18 +1583,6 @@ Αρχειοθετήθηκε - - - project.builds.no_stock - δεν έχει καθοριστεί απόθεμα - - - - - project.build.builds_part_lot_label - %name% (%quantity% απαιτείται) - - assembly.label @@ -1835,12 +1811,24 @@ απαιτούμενο + + + assembly.bom.delete.confirm + Θέλετε πραγματικά να διαγράψετε αυτήν την εγγραφή; + + assembly.add_parts_to_assembly Προσθήκη εξαρτημάτων στη συναρμολόγηση + + + assembly.bom.project + έργο + + assembly.bom.name @@ -1877,9 +1865,9 @@ Εισαγωγή εξαρτημάτων συναρμολόγησης - + - assembly.bom.part + assembly.bom.partOrProject Εξάρτημα @@ -2326,41 +2314,5 @@ - - - typeahead.parts.part.name - %name% (Εξάρτημα) - - - - - typeahead.parts.assembly.name - %name% (Συναρμολόγηση) - - - - - projects.build.form.part - Εξάρτημα "%name%" - - - - - projects.build.form.assembly - Συναρμολόγηση "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% απαιτείται) - - - - - projects.build.form.assembly.bom.entry.no.stock - δεν υπάρχει στο απόθεμα - - diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index f8d762f2f..763b6aff0 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -4747,10 +4747,10 @@ If you have done this incorrectly or if a computer is no longer trusted, you can %value% (Part) - + - part.table.name.value.for_assembly - %value% (Assembly) + part.table.name.value.for_project + %value% (Project) @@ -9801,18 +9801,6 @@ Element 1 -> Element 1.2 Part - - - project.bom.assembly - Assembly - - - - - project.bom.partOrAssembly - Selection - - project.bom.add_entry @@ -10203,12 +10191,6 @@ Element 1 -> Element 1.2 stocked - - - project.builds.no_stock - no stock specified - - project.builds.needed @@ -10281,12 +10263,6 @@ Element 1 -> Element 1.2 Target lot - - - project.build.builds_part_lot_label - %name% (%quantity% needed) - - project.builds.number_of_builds @@ -13152,12 +13128,24 @@ Please note, that you can not impersonate a disabled user. If you try you will g needed + + + assembly.bom.delete.confirm + Do you really want to delete this entry? + + assembly.add_parts_to_assembly Add parts to assembly + + + assembly.bom.project + Project + + assembly.bom.name @@ -13194,9 +13182,9 @@ Please note, that you can not impersonate a disabled user. If you try you will g Import part list for assembly - + - assembly.bom.part + assembly.bom.partOrProject Part @@ -13643,42 +13631,6 @@ Please note, that you can not impersonate a disabled user. If you try you will g - - - typeahead.parts.part.name - %name% (Part) - - - - - typeahead.parts.assembly.name - %name% (Assembly) - - - - - projects.build.form.part - Part "%name%" - - - - - projects.build.form.assembly - Assembly "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% needed) - - - - - projects.build.form.assembly.bom.entry.no.stock - not in stock - - part.table.actions.error diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 1ee1188e4..0ff8df3f2 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -4746,10 +4746,10 @@ Subelementos serán desplazados hacia arriba. %value% (Componente) - + - part.table.name.value.for_assembly - %value% (Ensamblaje) + part.table.name.value.for_project + %value% (Proyecto) @@ -9816,18 +9816,6 @@ Elemento 3 Componente - - - project.bom.assembly - Baugruppe - - - - - project.bom.partOrAssembly - Auswahl - - project.bom.add_entry @@ -10218,12 +10206,6 @@ Elemento 3 Almacenado - - - project.builds.no_stock - no se ha especificado stock - - project.builds.needed @@ -10296,12 +10278,6 @@ Elemento 3 Lote objetivo - - - project.build.builds_part_lot_label - %name% (se requiere %quantity%) - - project.builds.number_of_builds @@ -12668,12 +12644,24 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S necesario + + + assembly.bom.delete.confirm + ¿Realmente desea eliminar esta entrada? + + assembly.add_parts_to_assembly Añadir piezas al ensamblaje + + + assembly.bom.project + Proyecto + + assembly.bom.name @@ -12710,9 +12698,9 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Importar piezas para ensamblaje - + - assembly.bom.part + assembly.bom.partOrProject Pieza @@ -13159,41 +13147,5 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S - - - typeahead.parts.part.name - %name% (Componente) - - - - - typeahead.parts.assembly.name - %name% (Ensamblaje) - - - - - projects.build.form.part - Componente "%name%" - - - - - projects.build.form.assembly - Ensamblaje "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% necesario) - - - - - projects.build.form.assembly.bom.entry.no.stock - sin stock - - diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 9cd293219..234dee6ef 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -4709,11 +4709,11 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia %value% (Componente) - - - part.table.name.value.for_assembly - %value% (Assemblaggio) - + + + part.table.name.value.for_project + %value% (Projet) + @@ -9109,18 +9109,6 @@ exemple de ville Si vous avez des questions à propos de Part-DB , rendez vous sur <a href="%href%" class="link-external" target="_blank">Github</a> - - - project.bom.assembly - Assemblage - - - - - project.bom.partOrAssembly - Sélection - - assembly.edit.status @@ -9157,18 +9145,6 @@ exemple de ville Archivé - - - project.builds.no_stock - aucun stock indiqué - - - - - project.build.builds_part_lot_label - %name% (%quantity% requis) - - assembly.label @@ -9397,12 +9373,24 @@ exemple de ville nécessaire + + + assembly.bom.delete.confirm + Voulez-vous vraiment supprimer cette entrée ? + + assembly.add_parts_to_assembly Ajouter des pièces à l'assemblage + + + assembly.bom.project + Projet + + assembly.bom.name @@ -9439,9 +9427,9 @@ exemple de ville Importer des pièces pour l'assemblage - + - assembly.bom.part + assembly.bom.partOrProject Pièce @@ -9888,41 +9876,5 @@ exemple de ville - - - typeahead.parts.part.name - %name% (pièce) - - - - - typeahead.parts.assembly.name - %name% (assemblage) - - - - - projects.build.form.part - Pièce "%name%" - - - - - projects.build.form.assembly - Assemblage "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% nécessaires) - - - - - projects.build.form.assembly.bom.entry.no.stock - Non disponible en stock - - diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 8e39c0313..57654f44e 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -4748,11 +4748,11 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi %value% (Componente) - - - part.table.name.value.for_assembly - %value% (Assemblaggio) - + + + part.table.name.value.for_project + %value% (Progetto) + @@ -9818,18 +9818,6 @@ Element 3 Componente - - - project.bom.assembly - Assemblaggio - - - - - project.bom.partOrAssembly - Selezione - - project.bom.add_entry @@ -10220,12 +10208,6 @@ Element 3 a magazzino - - - project.builds.no_stock - nessuna scorta specificata - - project.builds.needed @@ -10298,12 +10280,6 @@ Element 3 Lotto target - - - project.build.builds_part_lot_label - %name% (%quantity% richiesti) - - project.builds.number_of_builds @@ -12646,12 +12622,24 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a necessari + + + assembly.bom.delete.confirm + Vuoi davvero eliminare questa voce? + + assembly.add_parts_to_assembly Aggiungi componenti al gruppo + + + assembly.bom.project + Progetto + + assembly.bom.name @@ -12688,9 +12676,9 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Importa componenti per il gruppo - + - assembly.bom.part + assembly.bom.partOrProject Componente @@ -13137,42 +13125,6 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a - - - typeahead.parts.part.name - %name% (componente) - - - - - typeahead.parts.assembly.name - %name% (gruppo) - - - - - projects.build.form.part - Componente "%name%" - - - - - projects.build.form.assembly - Gruppo "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% necessari) - - - - - projects.build.form.assembly.bom.entry.no.stock - Non disponibile in magazzino - - part.table.actions.error diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index fb64a0f04..2ff7a7446 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -4709,11 +4709,11 @@ %value%(部品) - - - part.table.name.value.for_assembly - %value%(アセンブリ) - + + + part.table.name.value.for_project + %value%(プロジェクト) + @@ -8846,18 +8846,6 @@ Exampletown Part-DBについての質問は、<a href="%href%" class="link-external" target="_blank">GitHub</a> にスレッドがあります。 - - - project.bom.assembly - アセンブリ - - - - - project.bom.partOrAssembly - 選択 - - assembly.edit.status @@ -8894,18 +8882,6 @@ Exampletown アーカイブ済み - - - project.builds.no_stock - nessuna scorta specificata - - - - - project.build.builds_part_lot_label - %name% (必要数: %quantity%) - - assembly.label @@ -9134,12 +9110,48 @@ Exampletown 必要数量 + + + assembly.bom.delete.confirm + 本当にこのエントリを削除しますか? + + assembly.add_parts_to_assembly アセンブリに部品を追加 + + + assembly.bom.project + プロジェクト + + + + + assembly.bom.name + 名前 + + + + + assembly.bom.comment + コメント + + + + + assembly.builds.following_bom_entries_miss_instock_n + このアセンブリを%number_of_builds%回作成するための部品が十分に在庫にありません。以下の部品が不足しています: + + + + + assembly.build.help + どの在庫から必要な部品を取り出すか(およびその数量)を選択してください。部品を取り出した場合は、各項目のチェックをオンにするか、最上部のチェックボックスを使って一括でオンにすることができます。 + + assembly.build.required_qty @@ -9589,41 +9601,5 @@ Exampletown - - - typeahead.parts.part.name - %name%(部品) - - - - - typeahead.parts.assembly.name - %name%(アセンブリ) - - - - - projects.build.form.part - 部品「%name%」 - - - - - projects.build.form.assembly - アセンブリ「%name%」 - - - - - projects.build.form.assembly.bom.entry - %name% (必要数量: %quantity%) - - - - - projects.build.form.assembly.bom.entry.no.stock - 在庫なし - - diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 7cca59753..be8728ce5 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -730,22 +730,10 @@ %value% (Onderdeel) - + - part.table.name.value.for_assembly - %value% (Samenstelling) - - - - - project.bom.assembly - Assemblage - - - - - project.bom.partOrAssembly - Selectie + part.table.name.value.for_project + %value% (Project) @@ -784,18 +772,6 @@ Αρχειοθετήθηκε - - - project.builds.no_stock - geen voorraad opgegeven - - - - - project.build.builds_part_lot_label - %name% (%quantity% vereist) - - assembly.label @@ -1024,12 +1000,24 @@ Nodig + + + assembly.bom.delete.confirm + Weet u zeker dat u dit item wilt verwijderen? + + assembly.add_parts_to_assembly Onderdelen toevoegen aan assemblage + + + assembly.bom.project + Project + + assembly.bom.name @@ -1066,9 +1054,9 @@ Importeer onderdelen voor assemblage - + - assembly.bom.part + assembly.bom.partOrProject Onderdeel @@ -1551,41 +1539,5 @@ - - - typeahead.parts.part.name - %name% (Onderdeel) - - - - - typeahead.parts.assembly.name - %name% (Assemblage) - - - - - projects.build.form.part - Onderdelen "%name%" - - - - - projects.build.form.assembly - Assemblage "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (%quantity% benodigd) - - - - - projects.build.form.assembly.bom.entry.no.stock - niet op voorraad - - diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 86894d998..d48e0d930 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -4751,11 +4751,11 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo %value%(部品) - - - part.table.name.value.for_assembly - %value%(アセンブリ) - + + + part.table.name.value.for_project + %value% (Projekt) + @@ -9821,18 +9821,6 @@ Element 3 Komponent - - - project.bom.assembly - Zespół - - - - - project.bom.partOrAssembly - Wybór - - project.bom.add_entry @@ -10223,12 +10211,6 @@ Element 3 dostępny - - - project.builds.no_stock - brak podanego stanu magazynowego - - project.builds.needed @@ -10301,12 +10283,6 @@ Element 3 Partia docelowa - - - project.build.builds_part_lot_label - %name% (%quantity% wymagane) - - project.builds.number_of_builds @@ -12523,12 +12499,24 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli potrzebne + + + assembly.bom.delete.confirm + Czy na pewno chcesz usunąć ten element? + + assembly.add_parts_to_assembly Dodaj części do zespołu + + + assembly.bom.project + Projekt + + assembly.bom.name @@ -12565,9 +12553,9 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Importuj części dla zespołu - + - assembly.bom.part + assembly.bom.partOrProject Część @@ -13014,41 +13002,5 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli - - - typeahead.parts.part.name - %name% (część) - - - - - typeahead.parts.assembly.name - %name% (zespół) - - - - - projects.build.form.part - Część "%name%" - - - - - projects.build.form.assembly - Zespół "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (wymagana ilość: %quantity%) - - - - - projects.build.form.assembly.bom.entry.no.stock - brak na magazynie - - diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index ac04ed290..f27cd8586 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -4757,11 +4757,11 @@ %value% (Часть) - - - part.table.name.value.for_assembly - %value% (Сборка) - + + + part.table.name.value.for_project + %value% (Проект) + @@ -9825,18 +9825,6 @@ Компонент - - - project.bom.assembly - Сборка - - - - - project.bom.partOrAssembly - Выбор - - project.bom.add_entry @@ -10227,12 +10215,6 @@ запасено - - - project.builds.no_stock - склад не указан - - project.builds.needed @@ -10305,12 +10287,6 @@ Целевой лот - - - project.build.builds_part_lot_label - %name% (требуется: %quantity%) - - project.builds.number_of_builds @@ -12623,12 +12599,24 @@ Необходимо + + + assembly.bom.delete.confirm + Вы действительно хотите удалить этот элемент? + + assembly.add_parts_to_assembly Добавить детали в сборку + + + assembly.bom.project + Проект + + assembly.bom.name @@ -12665,9 +12653,9 @@ Импортировать детали для сборки - + - assembly.bom.part + assembly.bom.partOrProject Компонент @@ -13114,41 +13102,5 @@ - - - typeahead.parts.part.name - %name% (Деталь) - - - - - typeahead.parts.assembly.name - %name% (Сборка) - - - - - projects.build.form.part - Компонент "%name%" - - - - - projects.build.form.assembly - Сборка "%name%" - - - - - projects.build.form.assembly.bom.entry - %name% (необходимо: %quantity%) - - - - - projects.build.form.assembly.bom.entry.no.stock - Нет на складе - - diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 426566225..9fd6855a0 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -4755,11 +4755,11 @@ %value%(部件) - - - part.table.name.value.for_assembly - %value%(组件) - + + + part.table.name.value.for_project + %value%(项目) + @@ -9824,18 +9824,6 @@ Element 3 部件 - - - project.bom.assembly - 装配 - - - - - project.bom.partOrAssembly - 选择 - - project.bom.add_entry @@ -10226,12 +10214,6 @@ Element 3 在库 - - - project.builds.no_stock - 未指定库存 - - project.builds.needed @@ -10304,12 +10286,6 @@ Element 3 目标批次 - - - project.build.builds_part_lot_label - %name% (需求数量: %quantity%) - - project.builds.number_of_builds @@ -12508,12 +12484,24 @@ Element 3 需要 + + + assembly.bom.delete.confirm + 您确定要删除此项目吗? + + assembly.add_parts_to_assembly 添加零件到组件 + + + assembly.bom.project + 项目 + + assembly.bom.name @@ -12550,9 +12538,9 @@ Element 3 导入组件的零件 - + - assembly.bom.part + assembly.bom.partOrProject 零件 @@ -12999,41 +12987,5 @@ Element 3 - - - typeahead.parts.part.name - %name%(零件) - - - - - typeahead.parts.assembly.name - %name%(组件) - - - - - projects.build.form.part - 零件“%name%” - - - - - projects.build.form.assembly - 组件“%name%” - - - - - projects.build.form.assembly.bom.entry - %name%(需数量:%quantity%) - - - - - projects.build.form.assembly.bom.entry.no.stock - 库存不足 - - diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 699b5d2f3..ee69c98c5 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -389,6 +389,12 @@ Tato součást již existuje ve skupině! + + + assembly.bom_entry.project_already_in_bom + Tento projekt již v této skupině existuje! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 056871bbf..8494e436e 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -365,6 +365,12 @@ Denne del eksisterer allerede i gruppen! + + + assembly.bom_entry.project_already_in_bom + Dette projekt eksisterer allerede i gruppen! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 8771a0e65..23794c3a4 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -389,6 +389,12 @@ Dieses Bauteil existiert bereits in der Gruppe! + + + assembly.bom_entry.project_already_in_bom + Dieses Projekt existiert bereits in der Gruppe! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index bb78c7994..e04b9dae3 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -31,6 +31,12 @@ Αυτό το εξάρτημα υπάρχει ήδη στην ομάδα! + + + assembly.bom_entry.project_already_in_bom + Αυτό το έργο υπάρχει ήδη στην ομάδα! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 4c53ed187..ef4606ba6 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -386,19 +386,25 @@ assembly.bom_entry.part_already_in_bom - __assembly.bom_entry.part_already_in_bom + This part already exists in the list! + + + + + assembly.bom_entry.project_already_in_bom + This project already exists in the list! assembly.bom_entry.name_already_in_bom - __assembly.bom_entry.name_already_in_bom + There is already a part with this name! validator.assembly.bom_entry.name_or_part_needed - __validator.assembly.bom_entry.name_or_part_needed + You must select a part or set a name for the entry! diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index 957a47916..e603bdaf8 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -227,6 +227,12 @@ Cette pièce existe déjà dans le groupe! + + + assembly.bom_entry.project_already_in_bom + Ce projet existe déjà dans le groupe! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 639dff8bc..4df9c735b 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -383,6 +383,12 @@ Ovaj dio već postoji u grupi! + + + assembly.bom_entry.project_already_in_bom + Ovaj projekt već postoji u grupi! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index a1b9b2f09..cbc331d57 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -383,6 +383,12 @@ Questa parte è già presente nel gruppo! + + + assembly.bom_entry.project_already_in_bom + Questo progetto esiste già nel gruppo! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index 4a36a79ac..070281ccc 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -227,6 +227,12 @@ この部品はすでにグループに存在します! + + + assembly.bom_entry.project_already_in_bom + このプロジェクトは既にグループに存在しています! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index e80dd23bb..1ef74c8eb 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -383,6 +383,12 @@ Ten element już istnieje w grupie! + + + assembly.bom_entry.project_already_in_bom + Ten projekt już znajduje się w grupie! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 48f0737eb..a878cc931 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -383,6 +383,12 @@ Эта деталь уже существует в группе! + + + assembly.bom_entry.project_already_in_bom + Этот проект уже находится в группе! + + assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index dea45ccc9..3ac139f16 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -371,6 +371,12 @@ 此零件已存在于组中! + + + assembly.bom_entry.project_already_in_bom + 该项目已在组中! + + assembly.bom_entry.name_already_in_bom From 7bf83de98761be42d326d738dec6585a95384090 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 17 Jun 2025 14:02:30 +0200 Subject: [PATCH 45/83] SQL-Formatierung in Migration verbessern --- migrations/Version20250304081039.php | 99 +++++++++++++++++++++++----- migrations/Version20250304154507.php | 24 +++++-- 2 files changed, 101 insertions(+), 22 deletions(-) diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php index 755ae2360..c0fc08d96 100644 --- a/migrations/Version20250304081039.php +++ b/migrations/Version20250304081039.php @@ -16,26 +16,93 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { - $this->addSql('CREATE TABLE assemblies (id INT AUTO_INCREMENT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, name VARCHAR(255) NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, order_quantity INT NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts TINYINT(1) NOT NULL, description LONGTEXT NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_5F3832C0727ACA70 (parent_id), INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('CREATE TABLE assembly_bom_entries (id INT AUTO_INCREMENT NOT NULL, id_assembly INT DEFAULT NULL, id_part INT DEFAULT NULL, id_project INT DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames LONGTEXT NOT NULL, name VARCHAR(255) DEFAULT NULL, comment LONGTEXT NOT NULL, price NUMERIC(11, 5) DEFAULT NULL, price_currency_id INT DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_8C74887E2F180363 (id_assembly), INDEX IDX_8C74887EC22F6CC4 (id_part), INDEX IDX_8C74887EF12E799E (id_project), INDEX IDX_8C74887E3FFDCD60 (price_currency_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id)'); - $this->addSql('ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES `attachments` (id) ON DELETE SET NULL'); - $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E2F180363 FOREIGN KEY (id_assembly) REFERENCES assemblies (id)'); - $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES `parts` (id)'); - $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES `projects` (id)'); - $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id)'); + $this->addSql(<<<'SQL' + CREATE TABLE assemblies ( + id INT AUTO_INCREMENT NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, + name VARCHAR(255) NOT NULL, + comment LONGTEXT NOT NULL, + not_selectable TINYINT(1) NOT NULL, + alternative_names LONGTEXT DEFAULT NULL, + order_quantity INT NOT NULL, + status VARCHAR(64) DEFAULT NULL, + order_only_missing_parts TINYINT(1) NOT NULL, + description LONGTEXT NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + INDEX IDX_5F3832C0727ACA70 (parent_id), + INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), + PRIMARY KEY(id) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE assembly_bom_entries ( + id INT AUTO_INCREMENT NOT NULL, + id_assembly INT DEFAULT NULL, + id_part INT DEFAULT NULL, + id_project INT DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames LONGTEXT NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment LONGTEXT NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + price_currency_id INT DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + INDEX IDX_8C74887E2F180363 (id_assembly), + INDEX IDX_8C74887EC22F6CC4 (id_part), + INDEX IDX_8C74887EF12E799E (id_project), + INDEX IDX_8C74887E3FFDCD60 (price_currency_id), + PRIMARY KEY(id) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES `attachments` (id) ON DELETE SET NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E2F180363 FOREIGN KEY (id_assembly) REFERENCES assemblies (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES `parts` (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES `projects` (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) + SQL); } public function mySQLDown(Schema $schema): void { - $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0727ACA70'); - $this->addSql('ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0EA7100A1'); - $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E2F180363'); - $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EC22F6CC4'); - $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EF12E799E'); - $this->addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E3FFDCD60'); - $this->addSql('DROP TABLE assemblies'); - $this->addSql('DROP TABLE assembly_bom_entries'); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0727ACA70 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0EA7100A1 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E2F180363 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EC22F6CC4 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EF12E799E + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E3FFDCD60 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assemblies + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assembly_bom_entries + SQL); } public function sqLiteUp(Schema $schema): void diff --git a/migrations/Version20250304154507.php b/migrations/Version20250304154507.php index 904a3b658..4f7fed2f4 100644 --- a/migrations/Version20250304154507.php +++ b/migrations/Version20250304154507.php @@ -17,16 +17,28 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { - $this->addSql('ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON parts (built_assembly_id)'); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON parts (built_assembly_id) + SQL); } public function mySQLDown(Schema $schema): void { - $this->addSql('ALTER TABLE `parts` DROP FOREIGN KEY FK_6940A7FECC660B3C'); - $this->addSql('DROP INDEX UNIQ_6940A7FECC660B3C ON `parts`'); - $this->addSql('ALTER TABLE `parts` DROP built_assembly_id'); + $this->addSql(<<<'SQL' + ALTER TABLE parts DROP FOREIGN KEY FK_6940A7FECC660B3C + SQL); + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_6940A7FECC660B3C ON parts + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE `parts` DROP built_assembly_id + SQL); } public function sqLiteUp(Schema $schema): void From dd9bf5626296c6dd5395fd678840c4c2ee00e3b1 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 24 Jun 2025 09:42:12 +0200 Subject: [PATCH 46/83] Projekt-Importer um JSON/CSV Importer analog zu Assemblies erweitern --- src/Controller/ProjectController.php | 42 ++- src/Entity/ProjectSystem/ProjectBOMEntry.php | 3 +- .../ImportExportSystem/BOMImporter.php | 44 ++- templates/assemblies/import_bom.html.twig | 1 - templates/projects/import_bom.html.twig | 96 ++++- translations/messages.cs.xlf | 325 +++++++++++++++++ translations/messages.da.xlf | 331 +++++++++++++++++- translations/messages.de.xlf | 331 +++++++++++++++++- translations/messages.el.xlf | 4 +- translations/messages.en.xlf | 325 +++++++++++++++++ translations/messages.es.xlf | 329 ++++++++++++++++- translations/messages.fr.xlf | 12 + translations/messages.it.xlf | 329 ++++++++++++++++- translations/messages.ja.xlf | 4 +- translations/messages.nl.xlf | 4 +- translations/messages.pl.xlf | 329 ++++++++++++++++- translations/messages.ru.xlf | 329 ++++++++++++++++- translations/messages.zh.xlf | 329 ++++++++++++++++- 18 files changed, 3112 insertions(+), 55 deletions(-) diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 2a6d19ee2..7b69ba2b4 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -46,14 +46,16 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Validator\Validator\ValidatorInterface; - +use Symfony\Contracts\Translation\TranslatorInterface; use function Symfony\Component\Translation\t; #[Route(path: '/project')] class ProjectController extends AbstractController { - public function __construct(private readonly DataTableFactory $dataTableFactory) - { + public function __construct( + private readonly DataTableFactory $dataTableFactory, + private readonly TranslatorInterface $translator, + ) { } #[Route(path: '/{id}/info', name: 'project_info', requirements: ['id' => '\d+'])] @@ -147,6 +149,8 @@ public function importBOM( 'label' => 'project.bom_import.type', 'required' => true, 'choices' => [ + 'project.bom_import.type.json' => 'json', + 'project.bom_import.type.csv' => 'csv', 'project.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew', 'project.bom_import.type.kicad_schematic' => 'kicad_schematic', 'project.bom_import.type.generic_csv' => 'generic_csv', @@ -189,17 +193,20 @@ public function importBOM( } // For PCB imports, proceed directly - $entries = $BOMImporter->importFileIntoProject($form->get('file')->getData(), $project, [ - 'type' => $import_type, + $importerResult = $BOMImporter->importFileIntoProject($form->get('file')->getData(), $project, [ + 'type' => $form->get('type')->getData(), ]); // Validate the project entries $errors = $validator->validateProperty($project, 'bom_entries'); - // If no validation errors occurred, save the changes and redirect to edit page - if (count($errors) === 0) { + //If no validation errors occured, save the changes and redirect to edit page + if (count ($errors) === 0 && $importerResult->getViolations()->count() === 0) { + $entries = $importerResult->getBomEntries(); + $this->addFlash('success', t('project.bom_import.flash.success', ['%count%' => count($entries)])); $entityManager->flush(); + return $this->redirectToRoute('project_edit', ['id' => $project->getID()]); } @@ -211,10 +218,29 @@ public function importBOM( } } + $jsonTemplate = [ + [ + "quantity" => 1.0, + "name" => $this->translator->trans('project.bom_import.template.entry.name'), + "part" => [ + "id" => null, + "ipn" => $this->translator->trans('project.bom_import.template.entry.part.ipn'), + "mpnr" => $this->translator->trans('project.bom_import.template.entry.part.mpnr'), + "name" => $this->translator->trans('project.bom_import.template.entry.part.name'), + "manufacturer" => [ + "id" => null, + "name" => $this->translator->trans('project.bom_import.template.entry.part.manufacturer.name') + ], + ] + ] + ]; + return $this->render('projects/import_bom.html.twig', [ 'project' => $project, + 'jsonTemplate' => $jsonTemplate, 'form' => $form, - 'errors' => $errors ?? null, + 'validationErrors' => $errors ?? null, + 'importerErrors' => isset($importerResult) ? $importerResult->getViolations() : null, ]); } diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index f58e4d5eb..b2a3b2e95 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -36,6 +36,7 @@ use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Contracts\TimeStampableInterface; +use App\Repository\DBElementRepository; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; @@ -54,7 +55,7 @@ * The ProjectBOMEntry class represents an entry in a project's BOM. */ #[ORM\HasLifecycleCallbacks] -#[ORM\Entity] +#[ORM\Entity(repositoryClass: DBElementRepository::class)] #[ORM\Table('project_bom_entries')] #[ApiResource( operations: [ diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index fc73835c3..d1c5f939a 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -87,7 +87,7 @@ public function __construct( $this->partRepository = $entityManager->getRepository(Part::class); $this->manufacturerRepository = $entityManager->getRepository(Manufacturer::class); $this->categoryRepository = $entityManager->getRepository(Category::class); - $this->projectBOMEntryRepository = $entityManager->getRepository(Project::class); + $this->projectBOMEntryRepository = $entityManager->getRepository(ProjectBOMEntry::class); $this->assemblyBOMEntryRepository = $entityManager->getRepository(AssemblyBOMEntry::class); $this->translator = $translator; } @@ -111,18 +111,19 @@ protected function configureOptions(OptionsResolver $resolver): OptionsResolver /** * Converts the given file into an array of BOM entries using the given options and save them into the given project. * The changes are not saved into the database yet. - * @return ProjectBOMEntry[] */ - public function importFileIntoProject(File $file, Project $project, array $options): array + public function importFileIntoProject(UploadedFile $file, Project $project, array $options): ImporterResult { - $bom_entries = $this->fileToBOMEntries($file, $options); + $importerResult = $this->fileToImporterResult($file, $options); - //Assign the bom_entries to the project - foreach ($bom_entries as $bom_entry) { - $project->addBomEntry($bom_entry); + if ($importerResult->getViolations()->count() === 0) { + //Assign the bom_entries to the project + foreach ($importerResult->getBomEntries() as $bomEntry) { + $project->addBomEntry($bomEntry); + } } - return $bom_entries; + return $importerResult; } /** @@ -202,7 +203,7 @@ public function fileToImporterResult(UploadedFile $file, array $options, string $fileExtension, [ '%extension%' => $fileExtension, - '%importType%' => $this->translator->trans('assembly.bom_import.type.'.$options['type']), + '%importType%' => $this->translator->trans($objectType === ProjectBOMEntry::class ? 'project.bom_import.type.'.$options['type'] : 'assembly.bom_import.type.'.$options['type']), '%allowedExtensions%' => implode(', ', $validExtensions), ] )); @@ -711,7 +712,7 @@ private function processPart(array $entry, ImporterResult $result, int $key, str $category = $categoryIdValid ? $this->categoryRepository->findOneBy(['id' => $entry['part']['category']['id']]) : null; $category = $category ?? ($categoryNameValid ? $this->categoryRepository->findOneBy(['name' => trim($entry['part']['category']['name'])]) : null); - if (($categoryIdValid || $categoryNameValid) && $category === null) { + if (($categoryIdValid || $categoryNameValid)) { $value = sprintf( 'category.id: %s, category.name: %s', isset($entry['part']['category']['id']) && $entry['part']['category']['id'] !== null ? '' . $entry['part']['category']['id'] . '' : '-', @@ -748,12 +749,12 @@ private function processPart(array $entry, ImporterResult $result, int $key, str $part->setDescription($partDescription); } - if ($manufacturer !== null && $manufacturer->getID() !== $part->getManufacturerID()) { + if ($manufacturer !== null && $manufacturer->getID() !== $part->getManufacturer()->getID()) { //When updating the associated parts, take over to a assembly of the manufacturer of the part. $part->setManufacturer($manufacturer); } - if ($category !== null && $category->getID() !== $part->getCategoryID()) { + if ($category !== null && $category->getID() !== $part->getCategory()->getID()) { //When updating the associated parts to a assembly, take over the category of the part. $part->setCategory($category); } @@ -771,11 +772,26 @@ private function processPart(array $entry, ImporterResult $result, int $key, str } } } else { - $bomEntry = new ProjectBOMEntry(); + $bomEntry = $this->projectBOMEntryRepository->findOneBy(['part' => $part]); + + if ($bomEntry === null) { + if (isset($entry['name']) && $entry['name'] !== '') { + $bomEntry = $this->projectBOMEntryRepository->findOneBy(['name' => $entry['name']]); + } + + if ($bomEntry === null) { + $bomEntry = new ProjectBOMEntry(); + } + } } $bomEntry->setQuantity((float) $entry['quantity']); - $bomEntry->setName($entry['name'] ?? ''); + + if (isset($entry['name'])) { + $bomEntry->setName(trim($entry['name']) === '' ? null : trim ($entry['name'])); + } else { + $bomEntry->setName(null); + } $bomEntry->setPart($part); diff --git a/templates/assemblies/import_bom.html.twig b/templates/assemblies/import_bom.html.twig index 9e99c5417..89f504c2f 100644 --- a/templates/assemblies/import_bom.html.twig +++ b/templates/assemblies/import_bom.html.twig @@ -29,7 +29,6 @@ {% endif %} {% endblock %} - {% block card_title %} {% trans %}assembly.import_bom{% endtrans %}{% if assembly %}: {{ assembly.name }}{% endif %} diff --git a/templates/projects/import_bom.html.twig b/templates/projects/import_bom.html.twig index 0e7f17875..3f9059120 100644 --- a/templates/projects/import_bom.html.twig +++ b/templates/projects/import_bom.html.twig @@ -3,29 +3,107 @@ {% block title %}{% trans %}project.import_bom{% endtrans %}{% endblock %} {% block before_card %} - {% if errors %} + {% if validationErrors or importerErrors %}

                                                        {% trans %}parts.import.errors.title{% endtrans %}

                                                          - {% for violation in errors %} -
                                                        • - {{ violation.propertyPath }}: - {{ violation.message|trans(violation.parameters, 'validators') }} -
                                                        • - {% endfor %} + {% if validationErrors %} + {% for violation in validationErrors %} +
                                                        • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators') }} +
                                                        • + {% endfor %} + {% endif %} + + {% if importerErrors %} + {% for violation in importerErrors %} +
                                                        • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators')|raw }} +
                                                        • + {% endfor %} + {% endif %}
                                                        {% endif %} {% endblock %} - {% block card_title %} {% trans %}project.import_bom{% endtrans %}{% if project %}: {{ project.name }}{% endif %} {% endblock %} {% block card_content %} - {{ form(form) }} +{% endblock %} + +{% block additional_content %} +
                                                        +
                                                        +
                                                        +
                                                        + {% trans %}project.import_bom.template.header.json{% endtrans %} +
                                                        +
                                                        +
                                                        {{ jsonTemplate|json_encode(constant('JSON_PRETTY_PRINT') b-or constant('JSON_UNESCAPED_UNICODE')) }}
                                                        + + {{ 'project.bom_import.template.json.table'|trans|raw }} +
                                                        +
                                                        +
                                                        +
                                                        +
                                                        +
                                                        + {% trans %}project.import_bom.template.header.csv{% endtrans %} +
                                                        +
                                                        + {{ 'project.bom_import.template.csv.exptected_columns'|trans }} +
                                                        quantity;name;part_id;part_mpnr;part_ipn;part_name;part_manufacturer_id;part_manufacturer_name
                                                        + +
                                                          +
                                                        • quantity
                                                        • +
                                                        • name
                                                        • +
                                                        • part_id
                                                        • +
                                                        • part_mpnr
                                                        • +
                                                        • part_ipn
                                                        • +
                                                        • part_name
                                                        • +
                                                        • part_manufacturer_id
                                                        • +
                                                        • part_manufacturer_name
                                                        • +
                                                        + + {{ 'project.bom_import.template.csv.table'|trans|raw }} +
                                                        +
                                                        +
                                                        +
                                                        +
                                                        +
                                                        + {% trans %}project.import_bom.template.header.kicad_pcbnew{% endtrans %} +
                                                        +
                                                        + {{ 'project.bom_import.template.kicad_pcbnew.exptected_columns'|trans }} +
                                                        Id;Designator;Package;Quantity;Designation;Supplier and ref
                                                        + +
                                                          +
                                                        • Id
                                                        • +
                                                        • Designator
                                                        • +
                                                        • Package
                                                        • +
                                                        • Quantity
                                                        • +
                                                        • Designation
                                                        • +
                                                        • Supplier and ref
                                                        • +
                                                        • Note
                                                        • +
                                                        • Footprint
                                                        • +
                                                        • Value
                                                        • +
                                                        • Footprint
                                                        • +
                                                        + + {{ 'project.bom_import.template.kicad_pcbnew.exptected_columns.note'|trans|raw }} + + {{ 'project.bom_import.template.kicad_pcbnew.table'|trans|raw }} +
                                                        +
                                                        +
                                                        +
                                                        {% endblock %} \ No newline at end of file diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index c4021c2c5..1acfef098 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -11046,6 +11046,18 @@ Element 3 Typ
                                                        + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11064,6 +11076,319 @@ Element 3 Výběrem této možnosti odstraníte všechny existující položky BOM v projektu a přepíšete je importovaným souborem BOM! + + + project.import_bom.template.header.json + Šablona importu JSON + + + + + project.import_bom.template.header.csv + Šablona importu CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Šablona importu CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Název komponenty v projektu + + + + + project.bom_import.template.entry.part.mpnr + Jedinečné číslo produktu u výrobce + + + + + project.bom_import.template.entry.part.ipn + Jedinečné IPN součásti + + + + + project.bom_import.template.entry.part.name + Jedinečný název součásti + + + + + project.bom_import.template.entry.part.manufacturer.name + Jedinečný název výrobce + + + + + project.bom_import.template.json.table + + + + + Pole + Podmínka + Datový typ + Popis + + + + + quantity + Povinné + Desetinné číslo (Float) + Musí být zadáno a obsahovat desetinnou hodnotu (Float), která je větší než 0.0. + + + name + Volitelné + Řetězec (String) + Pokud je přítomen, musí být neprázdný řetězec. Název položky v kusovníku. + + + part + Volitelné + Objekt/Array + + Pokud je potřeba přiřadit součástku, musí to být objekt/pole a musí být vyplněno alespoň jedno z následujících polí: +
                                                          +
                                                        • part.id
                                                        • +
                                                        • part.mpnr
                                                        • +
                                                        • part.ipn
                                                        • +
                                                        • part.name
                                                        • +
                                                        + + + + part.id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID součástky v Part-DB. + + + part.mpnr + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno part.id, part.ipn nebo part.name. + + + part.ipn + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno part.id, part.mpnr nebo part.name. + + + part.name + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno part.id, part.mpnr nebo part.ipn. + + + part.manufacturer + Volitelné + Objekt/Array + + Pokud má být upraven výrobce součástky nebo pokud má být součástka nalezena jednoznačně na základě part.mpnr, musí to být objekt/pole a musí být vyplněno alespoň jedno z následujících polí: +
                                                          +
                                                        • manufacturer.id
                                                        • +
                                                        • manufacturer.name
                                                        • +
                                                        + + + + manufacturer.id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID výrobce. + + + manufacturer.name + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno manufacturer.id. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.csv.exptected_columns + Možné sloupce: + + + + + project.bom_import.template.csv.table + + + + + Sloupec + Podmínka + Datový typ + Popis + + + + + quantity + Povinné + Desetinné číslo (Float) + Musí být uvedeno a obsahovat hodnotu desetinného čísla (Float) větší než 0.0. + + + name + Optional + String + Název položky v kusovníku. + + + Sloupce začínající part_ + + Pokud má být přiřazena součástka, musí být uveden a vyplněn alespoň jeden z následujících sloupců: +
                                                          +
                                                        • part_id
                                                        • +
                                                        • part_mpnr
                                                        • +
                                                        • part_ipn
                                                        • +
                                                        • part_name
                                                        • +
                                                        + + + + part_id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID součástky v Part-DB. + + + part_mpnr + Volitelné + Řetězec (String) + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_ipn nebo part_name. + + + part_ipn + Volitelné + Řetězec (String) + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_mpnr nebo part_name. + + + part_name + Volitelné + Řetězec (String) + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_mpnr nebo part_ipn. + + + Sloupce začínající part_manufacturer_ + + Pokud má být upraven výrobce dílu nebo má být díl jednoznačně identifikován podle hodnoty part_mpnr, musí být uveden a vyplněn alespoň jeden z následujících sloupců: +
                                                          +
                                                        • part_manufacturer_id
                                                        • +
                                                        • part_manufacturer_name
                                                        • +
                                                        + + + + part_manufacturer_id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID výrobce. + + + part_manufacturer_name + Volitelné + Řetězec (String) + Musí být uvedeno, pokud není vyplněn sloupec part_manufacturer_id. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Očekávané sloupce: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Poznámka: Nedochází k přiřazení ke konkrétním součástkám ze správy kategorií.

                                                        + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.table + + + + + Pole + Podmínka + Datový typ + Popis + + + + + Id + Volitelné + Celé číslo (Integer) + Volný údaj. Jedinečné identifikační číslo pro každou součástku. + + + Designator + Volitelné + Řetězec (String) + Volný údaj. Jedinečný referenční označovač součástky na desce plošných spojů, např. „R1“ pro odpor 1.
                                                        Je převzat do osazovacího názvu záznamu součástky. + + + Package + Volitelné + Řetězec (String) + Volný údaj. Pouzdro nebo tvar součástky, např. „0805“ pro SMD odpory.
                                                        Pro záznam součástky není převzato. + + + Quantity + Povinné pole + Celé číslo (Integer) + Počet identických komponent, které jsou potřebné k vytvoření instance.
                                                        Je převzat jako počet položky komponenty. + + + Designation + Povinné pole + Řetězec (String) + Popis nebo funkce součástky, např. hodnota odporu „10kΩ“ nebo kapacita kondenzátoru „100nF“.
                                                        Je převzato do názvu záznamu součástky. + + + Supplier and ref + Volitelné + Řetězec (String) + Volný údaj. Může obsahovat např. distribuční specifickou hodnotu.
                                                        Je převzato jako poznámka ke záznamu součástky. + + + + ]]> +
                                                        +
                                                        +
                                                        project.bom_import.flash.invalid_file diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 24d42ac6a..108df80eb 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -11078,6 +11078,18 @@ Oversættelsen Typ + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11090,6 +11102,319 @@ Oversættelsen let eksisterende styklisteposter før import + + + project.import_bom.template.header.json + JSON-importskabelon + + + + + project.import_bom.template.header.csv + CSV-importskabelon + + + + + project.import_bom.template.header.kicad_pcbnew + CSV-importskabelon (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Komponentens navn i projektet + + + + + project.bom_import.template.entry.part.mpnr + Unikt produktnummer hos producenten + + + + + project.bom_import.template.entry.part.ipn + Komponentens unikke IPN + + + + + project.bom_import.template.entry.part.name + Komponentens unikke navn + + + + + project.bom_import.template.entry.part.manufacturer.name + Producentens unikke navn + + + + + project.bom_import.template.json.table + + + + + Felt + Betingelse + Datatype + Beskrivelse + + + + + quantity + Obligatorisk + Decimaltal (Float) + Skal være angivet og skal indeholde en decimaltalsværdi (Float), der er større end 0.0. + + + name + Valgfrit + String + Hvis til stede, skal det være en ikke-tom streng. Navnet på posten i stykliste. + + + part + Valgfrit + Objekt/Array + + Hvis en komponent skal knyttes, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: +
                                                          +
                                                        • part.id
                                                        • +
                                                        • part.mpnr
                                                        • +
                                                        • part.ipn
                                                        • +
                                                        • part.name
                                                        • +
                                                        + + + + part.id + Valgfrit + Heltal (Integer) + Heltal (Integer) > 0. Svarer til det interne numeriske ID for komponenten i Part-DB. + + + part.mpnr + Valgfrit + String + En ikke-tom streng, hvis hverken part.id, part.ipn eller part.name er angivet. + + + part.ipn + Valgfrit + String + En ikke-tom streng, hvis hverken part.id, part.mpnr eller part.name er angivet. + + + part.name + Valgfrit + String + En ikke-tom streng, hvis hverken part.id, part.mpnr eller part.ipn er angivet. + + + part.manufacturer + Valgfrit + Objekt/Array + + Hvis en komponents producent skal justeres, eller hvis komponenten skal findes entydigt via part.mpnr, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: +
                                                          +
                                                        • manufacturer.id
                                                        • +
                                                        • manufacturer.name
                                                        • +
                                                        + + + + manufacturer.id + Valgfrit + Heltal (Integer) + Heltal (Integer) > 0. Svarer til producentens interne numeriske ID. + + + manufacturer.name + Valgfrit + String + En ikke-tom streng, hvis manufacturer.id ikke er angivet. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.csv.exptected_columns + Mulige kolonner: + + + + + project.bom_import.template.csv.table + + + + + Kolonne + Betingelse + Datatype + Beskrivelse + + + + + quantity + Obligatorisk + Decimaltal (Float) + Skal være angivet og indeholde en decimaltalsværdi (Float), som er større end 0,0. + + + name + Optional + String + Hvis tilgængelig, skal det være en ikke-tom streng. Navnet på elementet inden for stykliste. + + + Kolonner, der begynder med part_ + + Hvis en komponent skal tildeles, skal mindst én af følgende kolonner være angivet og udfyldt: +
                                                          +
                                                        • part_id
                                                        • +
                                                        • part_mpnr
                                                        • +
                                                        • part_ipn
                                                        • +
                                                        • part_name
                                                        • +
                                                        + + + + part_id + Valgfri + Heltal (Integer) + Heltal (Integer) > 0. Svarer til den interne numeriske ID for komponenten i Part-DB. + + + part_mpnr + Valgfri + Streng (String) + Skal angives, hvis kolonnerne part_id, part_ipn eller part_name ikke er udfyldt. + + + part_ipn + Valgfri + Streng (String) + Skal angives, hvis kolonnerne part_id, part_mpnr eller part_name ikke er udfyldt. + + + part_name + Valgfri + Streng (String) + Skal angives, hvis kolonnerne part_id, part_mpnr eller part_ipn ikke er udfyldt. + + + Kolonner, der begynder med part_manufacturer_ + + Hvis komponentens producent skal ændres eller identificeres entydigt baseret på part_mpnr, skal mindst én af følgende kolonner være angivet og udfyldt: +
                                                          +
                                                        • part_manufacturer_id
                                                        • +
                                                        • part_manufacturer_name
                                                        • +
                                                        + + + + part_manufacturer_id + Valgfri + Heltal (Integer) + Heltal (Integer) > 0. Svarer til den interne numeriske ID for producenten. + + + part_manufacturer_name + Valgfri + Streng (String) + Skal angives, hvis kolonnen part_manufacturer_id ikke er udfyldt. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Forventede kolonner: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Bemærk: Der sker ingen tilknytning til specifikke komponenter fra kategoristyringen.

                                                        + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.table + + + + + Felt + Betingelse + Datatype + Beskrivelse + + + + + Id + Valgfrit + Heltal (Integer) + Fri opgave. Et entydigt identifikationsnummer for hver komponent. + + + Designator + Valgfrit + Streng (String) + Fri opgave. En entydig referencemarkering for komponenten på PCB'et, fx "R1" for modstand 1.
                                                        Bliver overført til monteringsnavnet på komponentindgangen. + + + Package + Valgfrit + Streng (String) + Fri opgave. Komponentens pakning eller form, fx "0805" for SMD-modstande.
                                                        Bliver ikke overført til komponentindgangen. + + + Quantity + Obligatorisk felt + Heltal (Integer) + Antallet af identiske komponenter, der kræves for at oprette en instans.
                                                        Overtages som antallet af komponentposter. + + + Designation + Obligatorisk felt + Streng (String) + Beskrivelse eller funktion af komponenten, fx modstandsværdi "10kΩ" eller kondensatorværdi "100nF".
                                                        Bliver overført til komponentindgangens navn. + + + Supplier and ref + Valgfrit + Streng (String) + Fri opgave. Kan eksempelvis indeholde en distributørspecifik værdi.
                                                        Bliver overført som en note til komponentindgangen. + + + + ]]> +
                                                        +
                                                        +
                                                        project.bom_import.clear_existing_bom.help @@ -12777,13 +13102,13 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver - + assembly.bom_import.template.csv.exptected_columns Mulige kolonner: - + assembly.bom_import.template.csv.table @@ -12808,7 +13133,7 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver name Valgfrit Streng - Navnet på posten inden for samlingen. + Navnet på posten inden for stykliste. Kolonner, der starter med part_ diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index c25fd7e77..a7137b26a 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -11060,6 +11060,18 @@ Element 1 -> Element 1.2 Typ + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11078,6 +11090,319 @@ Element 1 -> Element 1.2 Wenn diese Option ausgewählt ist, werden alle bereits im Projekt existierenden BOM Einträge gelöscht und mit den importierten BOM Daten überschrieben. + + + project.import_bom.template.header.json + Import-Vorlage JSON + + + + + project.import_bom.template.header.csv + Import-Vorlage CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Import-Vorlage CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Name des Bauteils im Projekt + + + + + project.bom_import.template.entry.part.mpnr + Eindeutige Produktnummer innerhalb des Herstellers + + + + + project.bom_import.template.entry.part.ipn + Eideutige IPN des Bauteils + + + + + project.bom_import.template.entry.part.name + Eindeutiger Name des Bauteils + + + + + project.bom_import.template.entry.part.manufacturer.name + Eindeutiger Name des Herstellers + + + + + project.bom_import.template.json.table + + + + + Feld + Bedingung + Datentyp + Beschreibung + + + + + quantity + Pflichtfeld + Gleitkommazahl (Float) + Muss gegeben sein und enthält einen Gleitkommawert (Float), der größer als 0.0 ist. + + + name + Optional + String + Falls vorhanden, muss es ein nicht-leerer String sein. Name des Eintrags innerhalb der Stückliste. + + + part + Optional + Objekt/Array + + Falls ein Bauteil zugeordnet werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein: +
                                                          +
                                                        • part.id
                                                        • +
                                                        • part.mpnr
                                                        • +
                                                        • part.ipn
                                                        • +
                                                        • part.name
                                                        • +
                                                        + + + + part.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. + + + part.mpnr + Optional + String + Nicht-leerer String, falls keine part.id-, part-ipn- bzw. part.name-Angabe gegeben ist. + + + part.ipn + Optional + String + Nicht-leerer String, falls keine part.id-, part.mpnr bzw. part.name-Angabe gegeben ist. + + + part.name + Optional + String + Nicht-leerer String, falls keine part.id-, part.mpnr- bzw. part.ipn-Angabe gegeben ist. + + + part.manufacturer + Optional + Objekt/Array + + Falls der Hersteller eines Bauteils mit angepasst werden oder das Bauteil anhand der part.mpnr-Angabe eindeutig gesucht werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein: +
                                                          +
                                                        • manufacturer.id
                                                        • +
                                                        • manufacturer.name
                                                        • +
                                                        + + + + manufacturer.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID des Herstellers. + + + manufacturer.name + Optional + String + Nicht-leerer String, falls keine manufacturer.id-Angabe gegeben ist. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.csv.exptected_columns + Mögliche Spalten: + + + + + project.bom_import.template.csv.table + + + + + Spalte + Bedingung + Datentyp + Beschreibung + + + + + quantity + Pflichtfeld + Gleitkommazahl (Float) + Muss gegeben sein und enthält einen Gleitkommawert (Float), der größer als 0.0 ist. + + + name + Optional + String + Name des Eintrags innerhalb der Stückliste. + + + Spalten beginnend mit part_ + + Falls ein Bauteil zugeordnet werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
                                                          +
                                                        • part_id
                                                        • +
                                                        • part_mpnr
                                                        • +
                                                        • part_ipn
                                                        • +
                                                        • part_name
                                                        • +
                                                        + + + + part_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. + + + part_mpnr + Optional + String + Anzugeben, falls keine part_id-, part_ipn- bzw. part_name-Spalte ausgefüllt gegeben ist. + + + part_ipn + Optional + String + Anzugeben, falls keine part_id-, part_mpnr- bzw. part_name-Spalte ausgefüllt gegeben ist. + + + part_name + Optional + String + Anzugeben, falls keine part_id-, part_mpnr- bzw. part_ipn-Spalte ausgefüllt gegeben ist. + + + Spalten beginnend mit part_manufacturer_ + + Falls der Hersteller eines Bauteils mit angepasst werden oder das Bauteil anhand der part_mpnr-Angabe eindeutig gesucht werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
                                                          +
                                                        • part_manufacturer_id
                                                        • +
                                                        • part_manufacturer_name
                                                        • +
                                                        + + + + part_manufacturer_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID des Herstellers. + + + part_manufacturer_name + Optional + String + Anzugeben, falls keine part_manufacturer_id-Spalte ausgefüllt gegeben ist. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Erwartete Spalten: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Hinweis: Es findet keine Zuordnung zu konkreten Bauteilen aus der Kategorie-Verwaltung statt.

                                                        + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.table + + + + + Feld + Bedingung + Datentyp + Beschreibung + + + + + Id + Optional + Ganzzahl (Integer) + Offene Angabe. Eine eindeutige Identifikationsnummer für jedes Bauteil. + + + Designator + Optional + String + Offene Angabe. Ein eindeutiger Referenzbezeichner des Bauteils auf der Leiterplatte, z.B. „R1“ für Widerstand 1.
                                                        Wird in den Bestückungsnamen des Bauteil-Eintrags übernommen. + + + Package + Optional + String + Offene Angabe. Das Gehäuse oder die Bauform des Bauteils, z.B. „0805“ für SMD-Widerstände.
                                                        Wird für ein Bauteil-Eintrag nicht übernommen. + + + Quantity + Pflichtfeld + Ganzzahl (Integer) + Anzahl der identischen Bauteile, die benötigt werden, um eine Instanz zu erstellen.
                                                        Wird als Anzahl des Bauteil-Eintrags übernommen. + + + Designation + Pflichtfeld + String + Beschreibung oder Funktion des Bauteils, z.B. Widerstandswert „10kΩ“ oder Kondensatorwert „100nF“.
                                                        Wird in den Namen des Bauteil-Eintrags übernommen. + + + Supplier and ref + Optional + String + Offene Angabe. Kann z.B. Distributor spezifischen Wert enthalten.
                                                        Wird als Notiz zum Bauteil-Eintrag übernommen. + + + + ]]> +
                                                        +
                                                        +
                                                        project.bom_import.flash.invalid_file @@ -13332,7 +13657,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön name Optional String - Falls vorhanden, muss es ein nicht-leerer String sein. Name des Eintrags innerhalb der Baugruppe. + Falls vorhanden, muss es ein nicht-leerer String sein. Name des Eintrags innerhalb der Stückliste. part @@ -13432,13 +13757,13 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön - + assembly.bom_import.template.csv.exptected_columns Mögliche Spalten: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index a6dda3d47..3707e55dd 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -2116,13 +2116,13 @@ - + assembly.bom_import.template.csv.exptected_columns Δυνατές στήλες: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 763b6aff0..29c44ae44 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -11061,6 +11061,18 @@ Element 1 -> Element 1.2 Type + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11073,6 +11085,319 @@ Element 1 -> Element 1.2 Clear existing BOM entries before importing + + + project.import_bom.template.header.json + JSON Import Template + + + + + project.import_bom.template.header.csv + CSV Import Template + + + + + project.import_bom.template.header.kicad_pcbnew + CSV Import Template (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Component name in the project + + + + + project.bom_import.template.entry.part.mpnr + Unique product number within the manufacturer + + + + + project.bom_import.template.entry.part.ipn + Unique IPN of the component + + + + + project.bom_import.template.entry.part.name + Unique name of the component + + + + + project.bom_import.template.entry.part.manufacturer.name + Unique name of the manufacturer + + + + + project.bom_import.template.json.table + + + + + Field + Condition + Data Type + Description + + + + + quantity + Required + Decimal (Float) + Must be provided and contains a decimal value (Float) greater than 0.0. + + + name + Optional + String + If present, it must be a non-empty string. The name of the entry within the bill of materials. + + + part + Optional + Object/Array + + If a component is to be assigned, it must be an object/array, and at least one of the following fields must be filled in: +
                                                          +
                                                        • part.id
                                                        • +
                                                        • part.mpnr
                                                        • +
                                                        • part.ipn
                                                        • +
                                                        • part.name
                                                        • +
                                                        + + + + part.id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the component in the Part-DB. + + + part.mpnr + Optional + String + A non-empty string if no part.id, part.ipn, or part.name is provided. + + + part.ipn + Optional + String + A non-empty string if no part.id, part.mpnr, or part.name is provided. + + + part.name + Optional + String + A non-empty string if no part.id, part.mpnr, or part.ipn is provided. + + + part.manufacturer + Optional + Object/Array + + If a component's manufacturer is to be adjusted, or the component is to be unambiguously identified based on part.mpnr, it must be an object/array, and at least one of the following fields must be filled in: +
                                                          +
                                                        • manufacturer.id
                                                        • +
                                                        • manufacturer.name
                                                        • +
                                                        + + + + manufacturer.id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the manufacturer. + + + manufacturer.name + Optional + String + A non-empty string if no manufacturer.id is provided. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.csv.exptected_columns + Possible columns: + + + + + project.bom_import.template.csv.table + + + + + Column + Condition + Data Type + Description + + + + + quantity + Required + Floating-point number (Float) + Must be provided and contain a floating-point value (Float) greater than 0.0. + + + name + Optional + String + If available, it must be a non-empty string. The name of the entry within the bill of materials. + + + Columns starting with part_ + + If a component is to be assigned, at least one of the following columns must be provided and filled in: +
                                                          +
                                                        • part_id
                                                        • +
                                                        • part_mpnr
                                                        • +
                                                        • part_ipn
                                                        • +
                                                        • part_name
                                                        • +
                                                        + + + + part_id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the component in the Part-DB. + + + part_mpnr + Optional + String + Must be provided if the part_id, part_ipn, or part_name columns are not filled in. + + + part_ipn + Optional + String + Must be provided if the part_id, part_mpnr, or part_name columns are not filled in. + + + part_name + Optional + String + Must be provided if the part_id, part_mpnr, or part_ipn columns are not filled in. + + + Columns starting with part_manufacturer_ + + If the manufacturer of a component is to be adjusted or if the component is to be uniquely identified based on the part_mpnr, at least one of the following columns must be provided and filled in: +
                                                          +
                                                        • part_manufacturer_id
                                                        • +
                                                        • part_manufacturer_name
                                                        • +
                                                        + + + + part_manufacturer_id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the manufacturer. + + + part_manufacturer_name + Optional + String + Must be provided if the part_manufacturer_id column is not filled in. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Expected columns: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Note: No assignment to specific components from the category management is performed.

                                                        + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.table + + + + + Field + Condition + Data type + Description + + + + + Id + Optional + Integer + Free entry. A unique identification number for each component. + + + Designator + Optional + String + Free entry. A unique reference identifier of the component on the PCB, e.g., "R1" for resistor 1.
                                                        It is adopted into the assembly name of the component entry. + + + Package + Optional + String + Free entry. The housing or package type of the component, e.g., "0805" for SMD resistors.
                                                        It is not adopted into the component entry. + + + Quantity + Mandatory + Integer + The number of identical components required to create an instance.
                                                        It is adopted as the quantity of the entry. + + + Designation + Mandatory + String + Description or function of the component, e.g., resistor value "10kΩ" or capacitor value "100nF".
                                                        It is adopted into the name of the component entry. + + + Supplier and ref + Optional + String + Free entry. Can contain a distributor-specific value, for example.
                                                        It is adopted as a note to the component entry. + + + + ]]> +
                                                        +
                                                        +
                                                        project.bom_import.clear_existing_bom.help diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 0ff8df3f2..56bcf2a52 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -11076,6 +11076,18 @@ Elemento 3 Tipo + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11088,6 +11100,319 @@ Elemento 3 Eliminar entradas BOM existentes antes de importar + + + project.import_bom.template.header.json + Plantilla de importación JSON + + + + + project.import_bom.template.header.csv + Plantilla de importación CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Plantilla de importación CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Nombre del componente en el proyecto + + + + + project.bom_import.template.entry.part.mpnr + Número de producto único del fabricante + + + + + project.bom_import.template.entry.part.ipn + IPN único del componente + + + + + project.bom_import.template.entry.part.name + Nombre único del componente + + + + + project.bom_import.template.entry.part.manufacturer.name + Nombre único del fabricante + + + + + project.bom_import.template.json.table + + + + + Campo + Condición + Tipo de Datos + Descripción + + + + + quantity + Requerido + Decimal (Float) + Debe ser proporcionado y contener un valor decimal (Float) mayor que 0.0. + + + name + Opcional + Cadena (String) + Si está presente, debe ser una cadena no vacía. El nombre del elemento dentro de la lista de materiales. + + + part + Opcional + Objeto/Array + + Si se debe asignar un componente, debe ser un objeto/array, y al menos uno de los siguientes campos debe estar cumplimentado: +
                                                          +
                                                        • part.id
                                                        • +
                                                        • part.mpnr
                                                        • +
                                                        • part.ipn
                                                        • +
                                                        • part.name
                                                        • +
                                                        + + + + part.id + Opcional + Entero (Integer) + Entero (Integer) > 0. Corresponde al ID numérico interno del componente en la base de datos de componentes (Part-DB). + + + part.mpnr + Opcional + Cadena (String) + Una cadena no vacía si no se proporciona part.id, part.ipn o part.name. + + + part.ipn + Opcional + Cadena (String) + Una cadena no vacía si no se proporciona part.id, part.mpnr o part.name. + + + part.name + Opcional + Cadena (String) + Una cadena no vacía si no se proporciona part.id, part.mpnr o part.ipn. + + + part.manufacturer + Opcional + Objeto/Array + + Si se debe ajustar el fabricante de un componente, o si el componente debe identificarse de manera unívoca en base a part.mpnr, debe ser un objeto/array, y al menos uno de los siguientes campos debe estar cumplimentado: +
                                                          +
                                                        • manufacturer.id
                                                        • +
                                                        • manufacturer.name
                                                        • +
                                                        + + + + manufacturer.id + Opcional + Entero (Integer) + Entero (Integer) > 0. Corresponde al ID numérico interno del fabricante. + + + manufacturer.name + Opcional + Cadena (String) + Una cadena no vacía si no se proporciona manufacturer.id. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.csv.exptected_columns + Columnas posibles: + + + + + project.bom_import.template.csv.table + + + + + Columna + Condición + Tipo de dato + Descripción + + + + + quantity + Obligatoria + Número decimal (Float) + Debe proporcionarse y contener un valor decimal (Float) mayor que 0.0. + + + name + Optional + String + Si está disponible, debe ser una cadena no vacía. El nombre del elemento dentro de la lista de materiales. + + + Columnas que comienzan con part_ + + Si se va a asignar un componente, al menos una de las siguientes columnas debe proporcionarse y completarse: +
                                                          +
                                                        • part_id
                                                        • +
                                                        • part_mpnr
                                                        • +
                                                        • part_ipn
                                                        • +
                                                        • part_name
                                                        • +
                                                        + + + + part_id + Opcional + Entero + Entero > 0. Corresponde al ID numérico interno del componente en la base de datos de partes (Part-DB). + + + part_mpnr + Opcional + Cadena (String) + Debe proporcionarse si las columnas part_id, part_ipn o part_name no están completas. + + + part_ipn + Opcional + Cadena (String) + Debe proporcionarse si las columnas part_id, part_mpnr o part_name no están completas. + + + part_name + Opcional + Cadena (String) + Debe proporcionarse si las columnas part_id, part_mpnr o part_ipn no están completas. + + + Columnas que comienzan con part_manufacturer_ + + Si el fabricante de un componente debe ajustarse o si el componente debe identificarse de forma única según el valor part_mpnr, al menos una de las siguientes columnas debe proporcionarse y completarse: +
                                                          +
                                                        • part_manufacturer_id
                                                        • +
                                                        • part_manufacturer_name
                                                        • +
                                                        + + + + part_manufacturer_id + Opcional + Entero + Entero > 0. Corresponde al ID numérico interno del fabricante. + + + part_manufacturer_name + Opcional + Cadena (String) + Debe proporcionarse si la columna part_manufacturer_id no está completa. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Columnas esperadas: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Nota: No se realiza ninguna asignación a componentes específicos desde la gestión de categorías.

                                                        + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.table + + + + + Campo + Condición + Tipo de dato + Descripción + + + + + Id + Opcional + Entero + Entrada libre. Un número de identificación único para cada componente. + + + Designator + Opcional + Cadena (String) + Entrada libre. Un identificador de referencia único del componente en el PCB, por ejemplo, "R1" para la resistencia 1.
                                                        Se adopta en el nombre de ensamblaje del registro del componente. + + + Package + Opcional + Cadena (String) + Entrada libre. El encapsulado o tipo de la carcasa del componente, por ejemplo, "0805" para resistencias SMD.
                                                        No se adopta en el registro del componente. + + + Quantity + Obligatorio + Entero + El número de componentes idénticos necesarios para crear una instancia.
                                                        Se toma como la cantidad de la entrada del componente. + + + Designation + Obligatorio + Cadena (String) + Descripción o función del componente, por ejemplo, valor de resistencia "10kΩ" o valor de condensador "100nF".
                                                        Se adopta en el nombre del registro del componente. + + + Supplier and ref + Opcional + Cadena (String) + Entrada libre. Puede contener, por ejemplo, un valor específico del distribuidor.
                                                        Se adopta como una nota en el registro del componente. + + + + ]]> +
                                                        +
                                                        +
                                                        project.bom_import.clear_existing_bom.help @@ -12949,13 +13274,13 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S - + assembly.bom_import.template.csv.exptected_columns Columnas posibles: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 234dee6ef..6d455221e 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -9481,6 +9481,18 @@ exemple de ville CSV pour un assemblage + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + assembly.bom_import.type.kicad_pcbnew diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 57654f44e..1f56dfc93 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -11078,6 +11078,18 @@ Element 3 Tipo + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11090,6 +11102,319 @@ Element 3 Cancellare le voci della BOM (lista dei materiali) esistenti prima dell'importazione + + + project.import_bom.template.header.json + Modello di importazione JSON + + + + + project.import_bom.template.header.csv + Modello di importazione CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Modello di importazione CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Nome del componente nel progetto + + + + + project.bom_import.template.entry.part.mpnr + Codice prodotto unico del produttore + + + + + project.bom_import.template.entry.part.ipn + IPN unico del componente + + + + + project.bom_import.template.entry.part.name + Nome unico del componente + + + + + project.bom_import.template.entry.part.manufacturer.name + Nome unico del produttore + + + + + project.bom_import.template.json.table + + + + + Campo + Condizione + Tipo di Dati + Descrizione + + + + + quantity + Obbligatorio + Decimale (Float) + Deve essere fornito e contenere un valore decimale (Float) maggiore di 0.0. + + + name + Opzionale + Stringa (String) + Se presente, deve essere una stringa non vuota. Il nome dell'elemento all'interno della distinta materiali. + + + part + Opzionale + Oggetto/Array + + Se un componente deve essere assegnato, deve essere un oggetto/array e almeno uno dei seguenti campi deve essere compilato: +
                                                          +
                                                        • part.id
                                                        • +
                                                        • part.mpnr
                                                        • +
                                                        • part.ipn
                                                        • +
                                                        • part.name
                                                        • +
                                                        + + + + part.id + Opzionale + Intero (Integer) + Intero (Integer) > 0. Corrisponde all'ID numerico interno del componente nel database delle parti (Part-DB). + + + part.mpnr + Opzionale + Stringa (String) + Una stringa non vuota se non sono forniti part.id, part.ipn o part.name. + + + part.ipn + Opzionale + Stringa (String) + Una stringa non vuota se non sono forniti part.id, part.mpnr o part.name. + + + part.name + Opzionale + Stringa (String) + Una stringa non vuota se non sono forniti part.id, part.mpnr o part.ipn. + + + part.manufacturer + Opzionale + Oggetto/Array + + Se il produttore di un componente deve essere modificato o se è necessario identificare univocamente il componente basandosi su part.mpnr, deve essere un oggetto/array e almeno uno dei seguenti campi deve essere compilato: +
                                                          +
                                                        • manufacturer.id
                                                        • +
                                                        • manufacturer.name
                                                        • +
                                                        + + + + manufacturer.id + Opzionale + Intero (Integer) + Intero (Integer) > 0. Corrisponde all'ID numerico interno del produttore. + + + manufacturer.name + Opzionale + Stringa (String) + Una stringa non vuota se non è fornito manufacturer.id. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.csv.exptected_columns + Colonne possibili: + + + + + project.bom_import.template.csv.table + + + + + Colonna + Condizione + Tipo di dato + Descrizione + + + + + quantity + Obbligatoria + Numero decimale (Float) + Deve essere fornita e contenere un valore decimale (Float) maggiore di 0.0. + + + name + Optional + String + Se disponibile, deve essere una stringa non vuota. Il nome della voce all'interno della distinta base. + + + Colonne che iniziano con part_ + + Se un componente deve essere assegnato, almeno una delle seguenti colonne deve essere fornita e compilata: +
                                                          +
                                                        • part_id
                                                        • +
                                                        • part_mpnr
                                                        • +
                                                        • part_ipn
                                                        • +
                                                        • part_name
                                                        • +
                                                        + + + + part_id + Opzionale + Intero (Integer) + Intero > 0. Corrisponde all'ID numerico interno del componente nel database delle parti (Part-DB). + + + part_mpnr + Opzionale + Stringa (String) + Deve essere fornita se le colonne part_id, part_ipn o part_name non sono compilate. + + + part_ipn + Opzionale + Stringa (String) + Deve essere fornita se le colonne part_id, part_mpnr o part_name non sono compilate. + + + part_name + Opzionale + Stringa (String) + Deve essere fornita se le colonne part_id, part_mpnr o part_ipn non sono compilate. + + + Colonne che iniziano con part_manufacturer_ + + Se il produttore di un componente deve essere modificato o il componente deve essere identificato univocamente in base al valore part_mpnr, almeno una delle seguenti colonne deve essere fornita e compilata: +
                                                          +
                                                        • part_manufacturer_id
                                                        • +
                                                        • part_manufacturer_name
                                                        • +
                                                        + + + + part_manufacturer_id + Opzionale + Intero (Integer) + Intero > 0. Corrisponde all'ID numerico interno del produttore. + + + part_manufacturer_name + Opzionale + Stringa (String) + Deve essere fornita se la colonna part_manufacturer_id non è compilata. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Colonne previste: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Nota: Non viene effettuata alcuna associazione con componenti specifici dalla gestione delle categorie.

                                                        + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.table + + + + + Campo + Condizione + Tipo di dato + Descrizione + + + + + Id + Opzionale + Numero intero + Valore libero. Un numero identificativo univoco per ciascun componente. + + + Designator + Opzionale + Stringa + Valore libero. Un identificatore di riferimento univoco del componente sul PCB, ad esempio "R1" per il resistore 1.
                                                        Viene trasferito nel nome di montaggio del record del componente. + + + Package + Opzionale + Stringa + Valore libero. L'involucro o la forma del componente, ad esempio "0805" per i resistori SMD.
                                                        Non viene trasferito nel record del componente. + + + Quantity + Campo obbligatorio + Numero intero + Il numero dei componenti identici necessari per creare un'istanza.
                                                        Registrato come il numero della voce del componente. + + + Designation + Campo obbligatorio + Stringa + Descrizione o funzione del componente, ad esempio valore del resistore "10kΩ" o valore del condensatore "100nF".
                                                        Viene trasferita nel nome del record del componente. + + + Supplier and ref + Opzionale + Stringa + Valore libero. Può contenere ad esempio un valore specifico del distributore.
                                                        Viene trasferito come nota nel record del componente. + + + + ]]> +
                                                        +
                                                        +
                                                        project.bom_import.clear_existing_bom.help @@ -12927,13 +13252,13 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a - + assembly.bom_import.template.csv.exptected_columns Colonne possibili: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 2ff7a7446..8991fc170 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -9403,13 +9403,13 @@ Exampletown - + assembly.bom_import.template.csv.exptected_columns 可能なカラム: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index be8728ce5..9525ba29b 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1341,13 +1341,13 @@ - + assembly.bom_import.template.csv.exptected_columns Mogelijke kolommen: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index d48e0d930..c221767bb 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -11081,6 +11081,18 @@ Element 3 Typ + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11093,6 +11105,319 @@ Element 3 Wyczyść istniejące wpisy BOM przed importem + + + project.import_bom.template.header.json + Szablon importu JSON + + + + + project.import_bom.template.header.csv + Szablon importu CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Szablon importu CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Nazwa komponentu w projekcie + + + + + project.bom_import.template.entry.part.mpnr + Unikalny numer produktu producenta + + + + + project.bom_import.template.entry.part.ipn + Unikalny IPN komponentu + + + + + project.bom_import.template.entry.part.name + Unikalna nazwa komponentu + + + + + project.bom_import.template.entry.part.manufacturer.name + Unikalna nazwa producenta + + + + + project.bom_import.template.json.table + + + + + Pole + Warunek + Typ Danych + Opis + + + + + quantity + Wymagane + Dziesiętny (Float) + Musi być podane i zawierać wartość dziesiętną (Float) większą niż 0.0. + + + name + Opcjonalne + Ciąg (String) + Jeśli jest obecny, musi być niepustym ciągiem znaków. Nazwa elementu w wykazie materiałów. + + + part + Opcjonalne + Obiekt/Tablica + + Jeśli komponent musi być przypisany, musi być obiektem/tablą i co najmniej jedno z następujących pól musi zostać wypełnione: +
                                                          +
                                                        • part.id
                                                        • +
                                                        • part.mpnr
                                                        • +
                                                        • part.ipn
                                                        • +
                                                        • part.name
                                                        • +
                                                        + + + + part.id + Opcjonalne + Całkowity (Integer) + Całkowity (Integer) > 0. Odpowiada wewnętrznemu numerycznemu identyfikatorowi komponentu w bazie danych części (Part-DB). + + + part.mpnr + Opcjonalne + Ciag (String) + Niepusty ciąg, jeśli part.id, part.ipn ani part.name nie zostały podane. + + + part.ipn + Opcjonalne + Ciag (String) + Niepusty ciąg, jeśli part.id, part.mpnr ani part.name nie zostały podane. + + + part.name + Opcjonalne + Ciag (String) + Niepusty ciąg, jeśli part.id, part.mpnr ani part.ipn nie zostały podane. + + + part.manufacturer + Opcjonalne + Obiekt/Tablica + + Jeśli producent komponentu musi zostać dostosowany lub komponent musi zostać jednoznacznie zidentyfikowany na podstawie part.mpnr, musi być obiektem/tablą, a co najmniej jedno z następujących pól musi zostać wypełnione: +
                                                          +
                                                        • manufacturer.id
                                                        • +
                                                        • manufacturer.name
                                                        • +
                                                        + + + + manufacturer.id + Opcjonalne + Całkowity (Integer) + Całkowity (Integer) > 0. Odpowiada wewnętrznemu numerycznemu identyfikatorowi producenta. + + + manufacturer.name + Opcjonalne + Ciag (String) + Niepusty ciąg, jeśli manufacturer.id nie został podany. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.csv.exptected_columns + Możliwe kolumny: + + + + + project.bom_import.template.csv.table + + + + + Kolumna + Warunek + Typ danych + Opis + + + + + quantity + Wymagana + Liczba zmiennoprzecinkowa (Float) + Liczba identycznych komponentów potrzebnych do utworzenia instancji.
                                                        Traktowane jako liczba wpisów komponentu. + + + name + Optional + String + Jeśli dostępny, musi być niepustym ciągiem znaków. Nazwa elementu w wykazie materiałów. + + + Kolumny zaczynające się od part_ + + Jeśli ma zostać przypisany komponent, co najmniej jedna z poniższych kolumn musi zostać podana i uzupełniona: +
                                                          +
                                                        • part_id
                                                        • +
                                                        • part_mpnr
                                                        • +
                                                        • part_ipn
                                                        • +
                                                        • part_name
                                                        • +
                                                        + + + + part_id + Opcjonalna + Liczba całkowita (Integer) + Liczba całkowita > 0. Odpowiada wewnętrznemu ID numerycznemu komponentu w Part-DB. + + + part_mpnr + Opcjonalna + Cišg znaków (String) + Musi być podana, jeśli kolumny part_id, part_ipn ani part_name nie są podane. + + + part_ipn + Opcjonalna + Cišg znaków (String) + Musi być podana, jeśli kolumny part_id, part_mpnr ani part_name nie są podane. + + + part_name + Opcjonalna + Cišg znaków (String) + Musi być podana, jeśli kolumny part_id, part_mpnr ani part_ipn nie są podane. + + + Kolumny zaczynające się od part_manufacturer_ + + Jeśli producent komponentu ma zostać zmieniony lub komponent ma zostać jednoznacznie zidentyfikowany na podstawie wartości part_mpnr, co najmniej jedna z poniższych kolumn musi zostać podana i uzupełniona: +
                                                          +
                                                        • part_manufacturer_id
                                                        • +
                                                        • part_manufacturer_name
                                                        • +
                                                        + + + + part_manufacturer_id + Opcjonalna + Liczba całkowita (Integer) + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID producenta. + + + part_manufacturer_name + Opcjonalna + Cišg znaków (String) + Musi być podana, jeśli kolumna part_manufacturer_id nie jest uzupełniona. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Oczekiwane kolumny: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Uwaga: Nie następuje przypisanie do konkretnych komponentów z zarządzania kategoriami.

                                                        + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.table + + + + + Pole + Warunek + Typ danych + Opis + + + + + Id + Opcjonalne + Liczba całkowita (Integer) + Dowolna wartość. Unikalny numer identyfikacyjny dla każdego komponentu. + + + Designator + Opcjonalne + String + Dowolna wartość. Unikalny identyfikator referencyjny komponentu na płytce PCB, np. „R1” dla rezystora 1.
                                                        Zostaje przeniesiony do nazwy montażowej wpisu komponentu. + + + Package + Opcjonalne + String + Dowolna wartość. Obudowa lub typ komponentu, np. „0805” dla rezystorów SMD.
                                                        Nie zostaje przeniesiony do wpisu komponentu. + + + Quantity + Pole obowiązkowe + Liczba całkowita (Integer) + Liczba identycznych komponentów potrzebnych do stworzenia instancji zestawu.
                                                        Zostaje przeniesiona jako ilość wpisu komponentu. + + + Designation + Pole obowiązkowe + String + Opis lub funkcja komponentu, np. wartość rezystora „10kΩ” lub wartość kondensatora „100nF”.
                                                        Zostaje przeniesiony do nazwy wpisu komponentu. + + + Supplier and ref + Opcjonalne + String + Dowolna wartość. Może zawierać np. wartość specyficzną dla dystrybutora.
                                                        Zostaje przeniesiona jako notatka do wpisu komponentu. + + + + ]]> +
                                                        +
                                                        +
                                                        project.bom_import.clear_existing_bom.help @@ -12804,13 +13129,13 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli - + assembly.bom_import.template.csv.exptected_columns Możliwe kolumny: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index f27cd8586..5d4c8885f 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -11085,6 +11085,18 @@ Тип + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11097,6 +11109,319 @@ Удалить существующие записи BOM перед импортом. + + + project.import_bom.template.header.json + Шаблон импорта JSON + + + + + project.import_bom.template.header.csv + Шаблон импорта CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Шаблон импорта CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Название компонента в проекте + + + + + project.bom_import.template.entry.part.mpnr + Уникальный номер продукта производителя + + + + + project.bom_import.template.entry.part.ipn + Уникальный IPN компонента + + + + + project.bom_import.template.entry.part.name + Уникальное название компонента + + + + + project.bom_import.template.entry.part.manufacturer.name + Уникальное название производителя + + + + + project.bom_import.template.json.table + + + + + Поле + Условие + Тип данных + Описание + + + + + quantity + Обязательно + Дробное число (Float) + Должно быть указано и содержать дробное значение (Float), большее 0.0. + + + name + Опционально + Строка (String) + Если присутствует, должно быть непустой строкой. Название элемента в спецификации материалов. + + + part + Опционально + Объект/Массив + + Если необходимо назначить компонент, он должен быть объектом/массивом, и должно быть заполнено хотя бы одно из следующих полей: +
                                                          +
                                                        • part.id
                                                        • +
                                                        • part.mpnr
                                                        • +
                                                        • part.ipn
                                                        • +
                                                        • part.name
                                                        • +
                                                        + + + + part.id + Опционально + Целое число (Integer) + Целое число (Integer) > 0. Соответствует внутреннему числовому идентификатору компонента в базе данных компонентов (Part-DB). + + + part.mpnr + Опционально + Строка (String) + Непустая строка, если не указаны part.id, part.ipn или part.name. + + + part.ipn + Опционально + Строка (String) + Непустая строка, если не указаны part.id, part.mpnr или part.name. + + + part.name + Опционально + Строка (String) + Непустая строка, если не указаны part.id, part.mpnr или part.ipn. + + + part.manufacturer + Опционально + Объект/Массив + + Если необходимо указать производителя компонента или однозначно идентифицировать компонент на основе part.mpnr, он должен быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: +
                                                          +
                                                        • manufacturer.id
                                                        • +
                                                        • manufacturer.name
                                                        • +
                                                        + + + + manufacturer.id + Опционально + Целое число (Integer) + Целое число (Integer) > 0. Соответствует внутреннему числовому идентификатору производителя. + + + manufacturer.name + Опционально + Строка (String) + Непустая строка, если manufacturer.id не указан. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.csv.exptected_columns + Возможные колонки: + + + + + project.bom_import.template.csv.table + + + + + Колонка + Условие + Тип данных + Описание + + + + + quantity + Обязательная + Число с плавающей запятой (Float) + Количество идентичных компонентов, необходимых для создания экземпляра.
                                                        Считается количеством записей компонента. + + + name + Optional + String + Если доступно, должна быть непустая строка. Название элемента в спецификации материалов. + + + Колонки, начинающиеся с part_ + + Если нужно назначить компонент, должна быть указана и заполнена по крайней мере одна из следующих колонок: +
                                                          +
                                                        • part_id
                                                        • +
                                                        • part_mpnr
                                                        • +
                                                        • part_ipn
                                                        • +
                                                        • part_name
                                                        • +
                                                        + + + + part_id + Необязательная + Целое число (Integer) + Целое > 0. Соответствует внутреннему числовому ID компонента в базе данных компонентов (Part-DB). + + + part_mpnr + Необязательная + Строка (String) + Должна быть указана, если колонки part_id, part_ipn или part_name не заполнены. + + + part_ipn + Необязательная + Строка (String) + Должна быть указана, если колонки part_id, part_mpnr или part_name не заполнены. + + + part_name + Необязательная + Строка (String) + Должна быть указана, если колонки part_id, part_mpnr или part_ipn не заполнены. + + + Колонки, начинающиеся с part_manufacturer_ + + Если требуется указать производителя компонента или уникально идентифицировать компонент на основе значения part_mpnr, должна быть указана и заполнена по крайней мере одна из следующих колонок: +
                                                          +
                                                        • part_manufacturer_id
                                                        • +
                                                        • part_manufacturer_name
                                                        • +
                                                        + + + + part_manufacturer_id + Необязательная + Целое число (Integer) + Целое > 0. Соответствует внутреннему числовому ID производителя. + + + part_manufacturer_name + Необязательная + Строка (String) + Должна быть указана, если колонка part_manufacturer_id не заполнена. + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Ожидаемые столбцы: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Примечание: Не выполняется привязка к конкретным компонентам из управления категориями.

                                                        + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.table + + + + + Поле + Условие + Тип данных + Описание + + + + + Id + Необязательно + Целое число (Integer) + Свободный ввод. Уникальный идентификационный номер для каждого компонента. + + + Designator + Необязательно + Строка (String) + Свободный ввод. Уникальный идентификатор компонента на печатной плате, например, «R1» для резистора 1.
                                                        Добавляется в название сборочного узла записи компонента. + + + Package + Необязательно + Строка (String) + Свободный ввод. Корпус или тип компонента, например, «0805» для SMD резисторов.
                                                        Не добавляется в запись компонента. + + + Quantity + Обязательно + Целое число (Integer) + Число идентичных компонентов, необходимых для создания экземпляра сборки.
                                                        Добавляется как количество записи компонента. + + + Designation + Обязательно + Строка (String) + Описание или функция компонента, например, значение резистора «10kΩ» или значение конденсатора «100nF».
                                                        Добавляется в название записи компонента. + + + Supplier and ref + Необязательно + Строка (String) + Свободный ввод. Может содержать дистрибьюторское значение, например.
                                                        Добавляется как примечание к записи компонента. + + + + ]]> +
                                                        +
                                                        +
                                                        project.bom_import.clear_existing_bom.help @@ -12904,13 +13229,13 @@ - + assembly.bom_import.template.csv.exptected_columns Возможные столбцы: - + assembly.bom_import.template.csv.table diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 9fd6855a0..3a6f53755 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -11084,6 +11084,18 @@ Element 3 Type + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11096,6 +11108,319 @@ Element 3 导入前删除现有BOM条目 + + + project.import_bom.template.header.json + JSON导入模板 + + + + + project.import_bom.template.header.csv + CSV导入模板 + + + + + project.import_bom.template.header.kicad_pcbnew + CSV导入模板(KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + 项目中的组件名称 + + + + + project.bom_import.template.entry.part.mpnr + 制造商的唯一产品编号 + + + + + project.bom_import.template.entry.part.ipn + 唯一的组件IPN + + + + + project.bom_import.template.entry.part.name + 组件唯一名称 + + + + + project.bom_import.template.entry.part.manufacturer.name + 制造商唯一名称 + + + + + project.bom_import.template.json.table + + + + + 字段 + 条件 + 数据类型 + 描述 + + + + + quantity + 必填 + 小数 (Float) + 必须提供,并包含大于 0.0 的小数值 (Float)。 + + + name + 可选 + 字符串 (String) + 如果存在,必须是非空字符串。物料清单中元素的名称。 + + + part + 可选 + 对象/数组 + + 如果需要分配组件,则必须是对象/数组,并且以下字段中的至少一个必须填写: +
                                                          +
                                                        • part.id
                                                        • +
                                                        • part.mpnr
                                                        • +
                                                        • part.ipn
                                                        • +
                                                        • part.name
                                                        • +
                                                        + + + + part.id + 可选 + 整数 (Integer) + 整数 (Integer) > 0。对应于零件数据库 (Part-DB) 中组件的内部数字 ID。 + + + part.mpnr + 可选 + 字符串 (String) + 如果未提供 part.id、part.ipn 或 part.name,则为非空字符串。 + + + part.ipn + 可选 + 字符串 (String) + 如果未提供 part.id、part.mpnr 或 part.name,则为非空字符串。 + + + part.name + 可选 + 字符串 (String) + 如果未提供 part.id、part.mpnr 或 part.ipn,则为非空字符串。 + + + part.manufacturer + 可选 + 对象/数组 + + 如果需要调整组件的制造商,或者需要基于 part.mpnr 唯一标识组件,则必须是对象/数组,并且以下字段中的至少一个必须填写: +
                                                          +
                                                        • manufacturer.id
                                                        • +
                                                        • manufacturer.name
                                                        • +
                                                        + + + + manufacturer.id + 可选 + 整数 (Integer) + 整数 (Integer) > 0。对应于制造商的内部数字 ID。 + + + manufacturer.name + 可选 + 字符串 (String) + 如果未提供 manufacturer.id,则为非空字符串。 + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.csv.exptected_columns + 可能的列: + + + + + project.bom_import.template.csv.table + + + + + 列 + 条件 + 数据类型 + 描述 + + + + + quantity + 必填 + 浮点数 (Float) + 创建一个实例所需的相同组件数量。
                                                        记录为组件条目的数量。 + + + name + Optional + String + 如果可用,则必须是非空字符串。材料清单中项目的名称。 + + + 以 part_ 开头的列 + + 如果需要分配一个组件,必须提供并填写以下列之一: +
                                                          +
                                                        • part_id
                                                        • +
                                                        • part_mpnr
                                                        • +
                                                        • part_ipn
                                                        • +
                                                        • part_name
                                                        • +
                                                        + + + + part_id + 可选 + 整数 (Integer) + 整数 > 0。对应于零件数据库 (Part-DB) 中组件的内部数字 ID。 + + + part_mpnr + 可选 + 字符串 (String) + 如果 part_id、part_ipn 或 part_name 列未填写,则必须提供此列。 + + + part_ipn + 可选 + 字符串 (String) + 如果 part_id、part_mpnr 或 part_name 列未填写,则必须提供此列。 + + + part_name + 可选 + 字符串 (String) + 如果 part_id、part_mpnr 或 part_ipn 列未填写,则必须提供此列。 + + + 以 part_manufacturer_ 开头的列 + + 如果需要调整组件的制造商,或者组件需要根据 part_mpnr 值唯一标识,必须提供并填写以下列之一: +
                                                          +
                                                        • part_manufacturer_id
                                                        • +
                                                        • part_manufacturer_name
                                                        • +
                                                        + + + + part_manufacturer_id + 可选 + 整数 (Integer) + 整数 > 0。对应于制造商的内部数字 ID。 + + + part_manufacturer_name + 可选 + 字符串 (String) + 如果 part_manufacturer_id 列未填写,则必须提供此列。 + + + + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.exptected_columns + 预期的列: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + 注意:分类管理中不会执行与特定组件的映射。

                                                        + ]]> +
                                                        +
                                                        +
                                                        + + + project.bom_import.template.kicad_pcbnew.table + + + + + 字段 + 条件 + 数据类型 + 描述 + + + + + Id + 可选 + 整数 + 自由输入。每个组件的唯一标识编号。 + + + Designator + 可选 + 字符串 (String) + 自由输入。PCB 上组件的唯一参考标识符,例如“R1”代表电阻 1。
                                                        会被采用到组件记录的装配名称中。 + + + Package + 可选 + 字符串 (String) + 自由输入。组件的封装或形状,例如 "0805" 表示 SMD 电阻。
                                                        不会被采用到组件记录中。 + + + Quantity + 必填 + 整数 + 创建组件实例所需的相同组件的数量。
                                                        会被采用为组件记录的数量。 + + + Designation + 必填 + 字符串 (String) + 组件的描述或功能,例如电阻值 “10kΩ” 或电容值 “100nF”。
                                                        会被采用到组件记录的名称中。 + + + Supplier and ref + 可选 + 字符串 (String) + 自由输入。例如,可以包含供应商的特定值。
                                                        会被采用为组件记录的备注。 + + + + ]]> +
                                                        +
                                                        +
                                                        project.bom_import.clear_existing_bom.help @@ -12789,13 +13114,13 @@ Element 3 - + assembly.bom_import.template.csv.exptected_columns 可用列: - + assembly.bom_import.template.csv.table From b3e2986e8d74a6a37f07394663b58a2dde0e806c Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 24 Jun 2025 11:07:20 +0200 Subject: [PATCH 47/83] =?UTF-8?q?Part-=C3=9Cbersicht=20sowie=20-Detailansi?= =?UTF-8?q?cht=20um=20Assembly=20Information=20erweitern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DataTables/PartsDataTable.php | 29 +++++++++++++++++ templates/parts/info/_assemblies.html.twig | 31 +++++++++++++++++++ templates/parts/info/show_part_info.html.twig | 27 ++++++++++------ translations/messages.cs.xlf | 6 ++++ translations/messages.da.xlf | 6 ++++ translations/messages.de.xlf | 6 ++++ translations/messages.el.xlf | 6 ++++ translations/messages.en.xlf | 6 ++++ translations/messages.es.xlf | 6 ++++ translations/messages.fr.xlf | 6 ++++ translations/messages.it.xlf | 6 ++++ translations/messages.ja.xlf | 6 ++++ translations/messages.nl.xlf | 6 ++++ translations/messages.pl.xlf | 6 ++++ translations/messages.ru.xlf | 6 ++++ translations/messages.zh.xlf | 6 ++++ 16 files changed, 156 insertions(+), 9 deletions(-) create mode 100644 templates/parts/info/_assemblies.html.twig diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index f0decf271..b98c97629 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -39,6 +39,7 @@ use App\DataTables\Helpers\ColumnSortHelper; use App\DataTables\Helpers\PartDataTableHelper; use App\Doctrine\Helpers\FieldHelper; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; @@ -238,6 +239,34 @@ public function configure(DataTable $dataTable, array $options): void ]); } + //Add a assembly column to list where the part is used, when the user has the permission to see the assemblies + if ($this->security->isGranted('read', Assembly::class)) { + $this->csh->add('assemblies', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.labelp'), + 'render' => function ($value, Part $context): string { + //Only show the first 5 assembly names + $assemblies = $context->getAssemblies(); + $tmp = ""; + + $max = 5; + + for ($i = 0; $i < min($max, count($assemblies)); $i++) { + $url = $this->urlGenerator->infoURL($assemblies[$i]); + $tmp .= sprintf('%s', $url, htmlspecialchars($assemblies[$i]->getName())); + if ($i < count($assemblies) - 1) { + $tmp .= ", "; + } + } + + if (count($assemblies) > $max) { + $tmp .= ", + ".(count($assemblies) - $max); + } + + return $tmp; + } + ]); + } + $this->csh ->add('edit', IconLinkColumn::class, [ 'label' => $this->translator->trans('part.table.edit'), diff --git a/templates/parts/info/_assemblies.html.twig b/templates/parts/info/_assemblies.html.twig new file mode 100644 index 000000000..d4996c592 --- /dev/null +++ b/templates/parts/info/_assemblies.html.twig @@ -0,0 +1,31 @@ +{% import "components/attachments.macro.html.twig" as attachments %} +{% import "helper.twig" as helper %} + + + + + + + + + + + + + {% for bom_entry in part.assemblyBomEntries %} + {# @var bom_entry App\Entity\Assembly\AssemblyBOMEntry #} + + + {# Name #} + {# Description #} + + + {% endfor %} + +
                                                        {% trans %}entity.info.name{% endtrans %}{% trans %}description.label{% endtrans %}{% trans %}assembly.bom.quantity{% endtrans %}
                                                        {% if bom_entry.assembly.masterPictureAttachment is not null %}{{ attachments.attachment_icon(bom_entry.assembly.masterPictureAttachment, attachment_manager) }}{% endif %}{{ bom_entry.assembly.name }}{{ bom_entry.assembly.description|format_markdown }}{{ bom_entry.quantity | format_amount(part.partUnit) }}
                                                        + + + + {% trans %}part.info.add_part_to_assembly{% endtrans %} + \ No newline at end of file diff --git a/templates/parts/info/show_part_info.html.twig b/templates/parts/info/show_part_info.html.twig index 96b5e2091..cd7b4ce7a 100644 --- a/templates/parts/info/show_part_info.html.twig +++ b/templates/parts/info/show_part_info.html.twig @@ -109,15 +109,20 @@ {% trans %}vendor.partinfo.history{% endtrans %} - {% if part.projectBomEntries is not empty %} - - {% endif %} + +
    +
    + {% include "parts/info/_assemblies.html.twig" %} +
    +
    {% include "parts/info/_history.html.twig" %}
    diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 1acfef098..de9d29182 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -14092,6 +14092,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Přidat součásti do sestavy + + + part.info.add_part_to_assembly + Přidat tuto součástku do sestavy + + assembly.bom.project diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 108df80eb..cb08bf9a7 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -12809,6 +12809,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Tilføj dele til samlingen + + + part.info.add_part_to_assembly + Tilføj denne del til en samling + + assembly.bom.project diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index a7137b26a..9e79e5b1e 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -13464,6 +13464,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Bauteile zur Baugruppe hinzufügen + + + part.info.add_part_to_assembly + Dieses Bauteil zu einer Baugruppe hinzufügen + + assembly.bom.project diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 3707e55dd..fc03f08be 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1823,6 +1823,12 @@ Προσθήκη εξαρτημάτων στη συναρμολόγηση + + + part.info.add_part_to_assembly + Προσθέστε αυτό το εξάρτημα σε μια συναρμολόγηση + + assembly.bom.project diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 29c44ae44..2a3719563 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13465,6 +13465,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Add parts to assembly + + + part.info.add_part_to_assembly + Add this part to an assembly + + assembly.bom.project diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 56bcf2a52..7b41a5a8f 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -12981,6 +12981,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Añadir piezas al ensamblaje + + + part.info.add_part_to_assembly + Agregar esta parte a un ensamblaje + + assembly.bom.project diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 6d455221e..f5c7dc97d 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -9385,6 +9385,12 @@ exemple de ville Ajouter des pièces à l'assemblage + + + part.info.add_part_to_assembly + Ajouter cette pièce à un assemblage + + assembly.bom.project diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 1f56dfc93..55d963caf 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -12959,6 +12959,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Aggiungi componenti al gruppo + + + part.info.add_part_to_assembly + Aggiungi questa parte a un assemblaggio + + assembly.bom.project diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 8991fc170..b61b585f3 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -9122,6 +9122,12 @@ Exampletown アセンブリに部品を追加 + + + part.info.add_part_to_assembly + このパーツをアセンブリに追加 + + assembly.bom.project diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 9525ba29b..c52a4733a 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1012,6 +1012,12 @@ Onderdelen toevoegen aan assemblage + + + part.info.add_part_to_assembly + Dit onderdeel aan een assemblage toevoegen + + assembly.bom.project diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index c221767bb..6ae246be0 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -12836,6 +12836,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Dodaj części do zespołu + + + part.info.add_part_to_assembly + Dodaj tę część do zespołu + + assembly.bom.project diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 5d4c8885f..5f6584221 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -12936,6 +12936,12 @@ Добавить детали в сборку + + + part.info.add_part_to_assembly + Добавить эту часть в сборку + + assembly.bom.project diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 3a6f53755..e4b236db7 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -12821,6 +12821,12 @@ Element 3 添加零件到组件 + + + part.info.add_part_to_assembly + 将此零件添加到装配体中 + + assembly.bom.project From c4f34ef6c206fe1b70c77d7e38ad60a7ba7fc21e Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 26 Jun 2025 14:43:25 +0200 Subject: [PATCH 48/83] =?UTF-8?q?Assembly=20um=20IPN-Eingabem=C3=B6glichke?= =?UTF-8?q?it=20und=20Automatismus=20zur=20Name-Angabe=20erweitern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/parameters.yaml | 2 + config/services.yaml | 4 + docs/configuration.md | 1 + migrations/Version20250304081039.php | 7 ++ migrations/Version20250624095045.php | 84 +++++++++++++++++++ .../AdminPages/BaseAdminController.php | 19 +++++ src/Entity/AssemblySystem/Assembly.php | 33 +++++++- src/Form/AdminPages/AssemblyAdminForm.php | 17 ++++ src/Form/AdminPages/BaseEntityAdminForm.php | 8 +- templates/admin/assembly_admin.html.twig | 1 + translations/messages.cs.xlf | 6 ++ translations/messages.da.xlf | 6 ++ translations/messages.de.xlf | 6 ++ translations/messages.en.xlf | 6 ++ translations/messages.es.xlf | 6 ++ translations/messages.it.xlf | 6 ++ translations/messages.pl.xlf | 6 ++ translations/messages.ru.xlf | 6 ++ translations/messages.zh.xlf | 6 ++ translations/validators.cs.xlf | 6 ++ translations/validators.da.xlf | 6 ++ translations/validators.de.xlf | 6 ++ translations/validators.el.xlf | 6 ++ translations/validators.en.xlf | 6 ++ translations/validators.hr.xlf | 6 ++ translations/validators.it.xlf | 6 ++ translations/validators.pl.xlf | 6 ++ translations/validators.ru.xlf | 6 ++ translations/validators.zh.xlf | 6 ++ 29 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 migrations/Version20250624095045.php diff --git a/config/parameters.yaml b/config/parameters.yaml index a993d2d5e..fc2400ce6 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -14,6 +14,8 @@ parameters: partdb.db.emulate_natural_sort: '%env(bool:DATABASE_EMULATE_NATURAL_SORT)%' # If this is set to true, natural sorting is emulated on platforms that do not support it natively. This can be slow on large datasets. + partdb.create_assembly_use_ipn_placeholder_in_name: '%env(bool:CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME)%' # Use an %%ipn%% placeholder in the name of an assembly. Placeholder is replaced with the ipn input while saving. + ###################################################################################################################### # Users and Privacy ###################################################################################################################### diff --git a/config/services.yaml b/config/services.yaml index be293d389..4fdf18ad0 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -164,6 +164,10 @@ services: arguments: $saml_enabled: '%partdb.saml.enabled%' + App\Form\AdminPages\AssemblyAdminForm: + arguments: + $useAssemblyIpnPlaceholder: '%partdb.create_assembly_use_ipn_placeholder_in_name%' + #################################################################################################################### # Table settings #################################################################################################################### diff --git a/docs/configuration.md b/docs/configuration.md index efa3efd34..19899c061 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -141,6 +141,7 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns are: `name`, `id`, `quantity`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `mountnames`, `instockAmount`, `storageLocations`, `addedDate`, `lastModified`. +* `CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME`: Use an %%ipn%% placeholder in the name of a assembly. Placeholder is replaced with the ipn input while saving. ### History/Eventlog-related settings diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php index c0fc08d96..ae2d6261b 100644 --- a/migrations/Version20250304081039.php +++ b/migrations/Version20250304081039.php @@ -120,6 +120,7 @@ public function sqLiteUp(Schema $schema): void last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, status VARCHAR(64) DEFAULT NULL, + ipn VARCHAR(100) DEFAULT NULL, description CLOB NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, @@ -132,6 +133,12 @@ public function sqLiteUp(Schema $schema): void $this->addSql(<<<'SQL' CREATE INDEX IDX_5F3832C0EA7100A1 ON assemblies (id_preview_attachment) SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_5F3832C03D721C14 ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX assembly_idx_ipn ON assemblies (ipn) + SQL); $this->addSql(<<<'SQL' CREATE TABLE assembly_bom_entries ( diff --git a/migrations/Version20250624095045.php b/migrations/Version20250624095045.php new file mode 100644 index 000000000..d875f1d80 --- /dev/null +++ b/migrations/Version20250624095045.php @@ -0,0 +1,84 @@ +addSql(<<<'SQL' + ALTER TABLE assemblies ADD ipn VARCHAR(100) DEFAULT NULL AFTER status + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_5F3832C03D721C14 ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX assembly_idx_ipn ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e2f180363 TO IDX_8C74887E4AD2039E + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_5F3832C03D721C14 ON assemblies + SQL); + $this->addSql(<<<'SQL' + DROP INDEX assembly_idx_ipn ON assemblies + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP ipn + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e4ad2039e TO IDX_8C74887E2F180363 + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function sqLiteDown(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD ipn VARCHAR(100) DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_5F3832C03D721C14 ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX assembly_idx_ipn ON assemblies (ipn) + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_5F3832C03D721C14 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX assembly_idx_ipn + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP ipn + SQL); + } +} diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index 8c8d7520d..c6d9cf20c 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -23,6 +23,7 @@ namespace App\Controller\AdminPages; use App\DataTables\LogDataTable; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentUpload; @@ -193,6 +194,15 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit $entity->setMasterPictureAttachment(null); } + if ($entity instanceof Assembly) { + /* Replace ipn placeholder with the IPN information if applicable. + * The '%%ipn%%' placeholder is automatically inserted into the Name property, + * depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one, + * to avoid having to insert it manually */ + + $entity->setName(str_ireplace('%%ipn%%', $entity->getIpn(), $entity->getName())); + } + $this->commentHelper->setMessage($form['log_comment']->getData()); $em->persist($entity); @@ -287,6 +297,15 @@ protected function _new(Request $request, EntityManagerInterface $em, EntityImpo $new_entity->setMasterPictureAttachment(null); } + if ($new_entity instanceof Assembly) { + /* Replace ipn placeholder with the IPN information if applicable. + * The '%%ipn%%' placeholder is automatically inserted into the Name property, + * depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one, + * to avoid having to insert it manually */ + + $new_entity->setName(str_ireplace('%%ipn%%', $new_entity->getIpn(), $new_entity->getName())); + } + $this->commentHelper->setMessage($form['log_comment']->getData()); $em->persist($new_entity); $em->flush(); diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 54305a6fa..7897ea36b 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -47,8 +47,10 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use InvalidArgumentException; +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\Length; use Symfony\Component\Validator\Context\ExecutionContextInterface; /** @@ -58,6 +60,8 @@ */ #[ORM\Entity] #[ORM\Table(name: 'assemblies')] +#[UniqueEntity(fields: ['ipn'], message: 'assembly.ipn.must_be_unique')] +#[ORM\Index(columns: ['ipn'], name: 'assembly_idx_ipn')] #[ApiResource( operations: [ new Get(security: 'is_granted("read", object)'), @@ -83,7 +87,7 @@ normalizationContext: ['groups' => ['assembly:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] )] #[ApiFilter(PropertyFilter::class)] -#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "ipn"])] #[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] class Assembly extends AbstractStructuralDBElement { @@ -122,6 +126,14 @@ class Assembly extends AbstractStructuralDBElement #[ORM\Column(type: Types::STRING, length: 64, nullable: true)] protected ?string $status = null; + /** + * @var string|null The internal ipn number of the assembly + */ + #[Assert\Length(max: 100)] + #[Groups(['extended', 'full', 'project:read', 'project:write', 'import'])] + #[ORM\Column(type: Types::STRING, length: 100, unique: true, nullable: true)] + #[Length(max: 100)] + protected ?string $ipn = null; /** * @var Part|null The (optional) part that represents the builds of this assembly in the stock @@ -301,6 +313,25 @@ public function setStatus(?string $status): void $this->status = $status; } + /** + * Returns the internal part number of the assembly. + * @return string + */ + public function getIpn(): ?string + { + return $this->ipn; + } + + /** + * Sets the internal part number of the assembly. + * @param string $ipn The new IPN of the assembly + */ + public function setIpn(?string $ipn): Assembly + { + $this->ipn = $ipn; + return $this; + } + /** * Checks if this assembly has an associated part representing the builds of this assembly in the stock. */ diff --git a/src/Form/AdminPages/AssemblyAdminForm.php b/src/Form/AdminPages/AssemblyAdminForm.php index be1564d21..0512f64a4 100644 --- a/src/Form/AdminPages/AssemblyAdminForm.php +++ b/src/Form/AdminPages/AssemblyAdminForm.php @@ -25,11 +25,22 @@ use App\Entity\Base\AbstractNamedDBElement; use App\Form\AssemblySystem\AssemblyBOMEntryCollectionType; use App\Form\Type\RichTextEditorType; +use App\Services\LogSystem\EventCommentNeededHelper; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; class AssemblyAdminForm extends BaseEntityAdminForm { + public function __construct( + protected Security $security, + protected EventCommentNeededHelper $eventCommentNeededHelper, + protected bool $useAssemblyIpnPlaceholder = false + ) { + parent::__construct($security, $eventCommentNeededHelper, $useAssemblyIpnPlaceholder); + } + protected function additionalFormElements(FormBuilderInterface $builder, array $options, AbstractNamedDBElement $entity): void { $builder->add('description', RichTextEditorType::class, [ @@ -60,5 +71,11 @@ protected function additionalFormElements(FormBuilderInterface $builder, array $ 'assembly.status.archived' => 'archived', ], ]); + + $builder->add('ipn', TextType::class, [ + 'required' => false, + 'empty_data' => null, + 'label' => 'assembly.edit.ipn', + ]); } } diff --git a/src/Form/AdminPages/BaseEntityAdminForm.php b/src/Form/AdminPages/BaseEntityAdminForm.php index e5d69b35c..bbc437e3a 100644 --- a/src/Form/AdminPages/BaseEntityAdminForm.php +++ b/src/Form/AdminPages/BaseEntityAdminForm.php @@ -48,8 +48,11 @@ class BaseEntityAdminForm extends AbstractType { - public function __construct(protected Security $security, protected EventCommentNeededHelper $eventCommentNeededHelper) - { + public function __construct( + protected Security $security, + protected EventCommentNeededHelper $eventCommentNeededHelper, + protected bool $useAssemblyIpnPlaceholder = false + ) { } public function configureOptions(OptionsResolver $resolver): void @@ -70,6 +73,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('name', TextType::class, [ 'empty_data' => '', 'label' => 'name.label', + 'data' => $is_new && $entity instanceof Assembly && $this->useAssemblyIpnPlaceholder ? '%%ipn%%' : $entity->getName(), 'attr' => [ 'placeholder' => 'part.name.placeholder', ], diff --git a/templates/admin/assembly_admin.html.twig b/templates/admin/assembly_admin.html.twig index 57dde7d15..e6a90dc09 100644 --- a/templates/admin/assembly_admin.html.twig +++ b/templates/admin/assembly_admin.html.twig @@ -29,6 +29,7 @@ {% block additional_controls %} {{ form_row(form.description) }} {{ form_row(form.status) }} + {{ form_row(form.ipn) }} {% if entity.id %}
    diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index de9d29182..b8c4f6033 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -9870,6 +9870,12 @@ Element 3 Stav + + + assembly.edit.ipn + Interní číslo dílu (IPN) + + assembly.status.draft diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index cb08bf9a7..4cec94054 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -9896,6 +9896,12 @@ Element 3 Status + + + assembly.edit.ipn + Internt Partnummer (IPN) + + assembly.status.draft diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 9e79e5b1e..3140a1368 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -9884,6 +9884,12 @@ Element 1 -> Element 1.2 Status + + + assembly.edit.ipn + Internal Part Number (IPN) + + assembly.status.draft diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 2a3719563..18c124ff7 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -9885,6 +9885,12 @@ Element 1 -> Element 1.2 Project status + + + assembly.edit.ipn + Internal Part Number (IPN) + + assembly.status.draft diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 7b41a5a8f..edef6d968 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -9900,6 +9900,12 @@ Elemento 3 Estatus + + + assembly.edit.ipn + Número de Componente Interno (IPN) + + assembly.status.draft diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 55d963caf..e7628984a 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -9902,6 +9902,12 @@ Element 3 Stato + + + assembly.edit.ipn + Codice interno (IPN) + + assembly.status.draft diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 6ae246be0..fc6eba18d 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -9905,6 +9905,12 @@ Element 3 Status + + + assembly.edit.ipn + Internal Part Number (IPN) + + assembly.status.draft diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 5f6584221..252b857bd 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -9909,6 +9909,12 @@ Статус + + + assembly.edit.ipn + Внутренний номер компонента (IPN) + + assembly.status.draft diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index e4b236db7..9c0b5a76f 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -9908,6 +9908,12 @@ Element 3 状态 + + + assembly.edit.ipn + 内部零件号 (IPN) + + assembly.status.draft diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index ee69c98c5..607b8a3b6 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -239,6 +239,12 @@ Interní číslo dílu musí být jedinečné. {{ value }} se již používá! + + + assembly.ipn.must_be_unique + Interní číslo dílu musí být jedinečné. {{ value }} se již používá! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 8494e436e..f25712719 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -239,6 +239,12 @@ Det interne partnummer skal være unikt. {{ value }} værdien er allerede i brug! + + + assembly.ipn.must_be_unique + Det interne partnummer skal være unikt. {{ value }} værdien er allerede i brug! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 23794c3a4..abd191d9e 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -239,6 +239,12 @@ Die Internal Part Number (IPN) muss einzigartig sein. Der Wert {{value}} wird bereits benutzt! + + + assembly.ipn.must_be_unique + Die Internal Part Number (IPN) muss einzigartig sein. Der Wert {{value}} wird bereits benutzt! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index e04b9dae3..580c91275 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -7,6 +7,12 @@ Ο εσωτερικός αριθμός εξαρτήματος πρέπει να είναι μοναδικός. {{ value }} χρησιμοποιείται ήδη! + + + assembly.ipn.must_be_unique + Ο εσωτερικός αριθμός εξαρτήματος πρέπει να είναι μοναδικός. {{ value }} χρησιμοποιείται ήδη! + + validator.project.bom_entry.only_part_or_assembly_allowed diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index ef4606ba6..e20e7ff8c 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -239,6 +239,12 @@ The internal part number must be unique. {{ value }} is already in use! + + + assembly.ipn.must_be_unique + The internal part number must be unique. {{ value }} is already in use! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 4df9c735b..c0af03354 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -239,6 +239,12 @@ Internal part number (IPN) mora biti jedinstven. {{ value }} je već u uporabi! + + + assembly.ipn.must_be_unique + Internal part number (IPN) mora biti jedinstven. {{ value }} je već u uporabi! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index cbc331d57..88369640f 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -239,6 +239,12 @@ Il codice interno (IPN) deve essere univoco. Il valore {{value}} è già in uso! + + + assembly.ipn.must_be_unique + Il codice interno (IPN) deve essere univoco. Il valore {{value}} è già in uso! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 1ef74c8eb..60713fa02 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -239,6 +239,12 @@ Wewnętrzny numer części musi być unikalny. {{value }} jest już w użyciu! + + + assembly.ipn.must_be_unique + Wewnętrzny numer części musi być unikalny. {{value }} jest już w użyciu! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index a878cc931..32540f8c7 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -239,6 +239,12 @@ Внутренний номер детали (IPN) должен быть уникальным. Значение {{value}} уже используется! + + + assembly.ipn.must_be_unique + Внутренний номер детали (IPN) должен быть уникальным. Значение {{value}} уже используется! + + validator.project.bom_entry.name_or_part_needed diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 3ac139f16..4a0ec79e8 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -239,6 +239,12 @@ 内部部件号是唯一的。{{ value }} 已被使用! + + + assembly.ipn.must_be_unique + 内部部件号是唯一的。{{ value }} 已被使用! + + validator.project.bom_entry.name_or_part_needed From ca11898d98beeb4ce06c6222a5916091f9f062ff Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 3 Jul 2025 13:38:51 +0200 Subject: [PATCH 49/83] =?UTF-8?q?Baugruppen=20St=C3=BCckliste=20um=20refer?= =?UTF-8?q?enzierte=20Baugruppe=20erweitern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/parameters.yaml | 7 + config/services.yaml | 5 +- docs/configuration.md | 5 +- migrations/Version20250304081039.php | 9 +- migrations/Version20250627130848.php | 78 +++++++++++ .../AdminPages/BaseAdminController.php | 22 +++- src/Controller/TypeaheadController.php | 40 ++++++ src/Entity/AssemblySystem/Assembly.php | 25 +++- .../AssemblySystem/AssemblyBOMEntry.php | 23 ++++ .../AssemblySystem/AssemblyAddPartsType.php | 7 +- .../AssemblySystem/AssemblyBOMEntryType.php | 5 + src/Form/Type/AssemblySelectType.php | 124 ++++++++++++++++++ src/Repository/AssemblyRepository.php | 69 ++++++++++ .../AssemblySystem/AssemblyBuildHelper.php | 12 +- .../Attachments/AssemblyPreviewGenerator.php | 93 +++++++++++++ src/Services/Trees/TreeViewGenerator.php | 9 ++ src/Twig/AssemblyTwigExtension.php | 4 + .../UniqueReferencedAssembly.php | 34 +++++ .../UniqueReferencedAssemblyValidator.php | 48 +++++++ templates/assemblies/build/_form.html.twig | 2 + .../info/_attachments_info.html.twig | 91 +++++++++++++ templates/assemblies/info/info.html.twig | 2 +- ...collection_types_layout_assembly.html.twig | 17 +-- translations/messages.cs.xlf | 32 ++++- translations/messages.da.xlf | 32 ++++- translations/messages.de.xlf | 32 ++++- translations/messages.el.xlf | 32 ++++- translations/messages.en.xlf | 32 ++++- translations/messages.es.xlf | 32 ++++- translations/messages.fr.xlf | 32 ++++- translations/messages.it.xlf | 32 ++++- translations/messages.ja.xlf | 26 +++- translations/messages.nl.xlf | 32 ++++- translations/messages.pl.xlf | 32 ++++- translations/messages.ru.xlf | 32 ++++- translations/messages.zh.xlf | 32 ++++- translations/validators.cs.xlf | 18 ++- translations/validators.da.xlf | 18 ++- translations/validators.de.xlf | 18 ++- translations/validators.el.xlf | 18 ++- translations/validators.en.xlf | 18 ++- translations/validators.fr.xlf | 18 ++- translations/validators.hr.xlf | 18 ++- translations/validators.it.xlf | 18 ++- translations/validators.ja.xlf | 18 ++- translations/validators.pl.xlf | 18 ++- translations/validators.ru.xlf | 18 ++- translations/validators.zh.xlf | 18 ++- 48 files changed, 1205 insertions(+), 152 deletions(-) create mode 100644 migrations/Version20250627130848.php create mode 100644 src/Form/Type/AssemblySelectType.php create mode 100644 src/Repository/AssemblyRepository.php create mode 100644 src/Services/Attachments/AssemblyPreviewGenerator.php create mode 100644 src/Validator/Constraints/AssemblySystem/UniqueReferencedAssembly.php create mode 100644 src/Validator/Constraints/AssemblySystem/UniqueReferencedAssemblyValidator.php create mode 100644 templates/assemblies/info/_attachments_info.html.twig diff --git a/config/parameters.yaml b/config/parameters.yaml index fc2400ce6..8c45b95a9 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -48,7 +48,14 @@ parameters: ###################################################################################################################### # Table settings ###################################################################################################################### +<<<<<<< HEAD partdb.table.assemblies.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_DEFAULT_COLUMNS)%' # The default columns in assembly tables and their order +======= + partdb.table.default_page_size: '%env(int:TABLE_DEFAULT_PAGE_SIZE)%' # The default number of entries shown per page in tables + partdb.table.parts.default_columns: '%env(trim:string:TABLE_PARTS_DEFAULT_COLUMNS)%' # The default columns in part tables and their order + partdb.table.assemblies.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_DEFAULT_COLUMNS)%' # The default columns in assembly tables and their order + partdb.table.assemblies_bom.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS)%' # The default columns in assembly bom tables and their order +>>>>>>> 2779c55a (Baugruppen Stückliste um referenzierte Baugruppe erweitern) ###################################################################################################################### # Miscellaneous diff --git a/config/services.yaml b/config/services.yaml index 4fdf18ad0..ff8ce2ec2 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -171,9 +171,12 @@ services: #################################################################################################################### # Table settings #################################################################################################################### - App\DataTables\AssemblyBomEntriesDataTable: + App\DataTables\AssemblyDataTable: arguments: $visible_columns: '%partdb.table.assemblies.default_columns%' + App\DataTables\AssemblyBomEntriesDataTable: + arguments: + $visible_columns: '%partdb.table.assemblies_bom.default_columns%' App\DataTables\Helpers\ColumnSortHelper: shared: false # Service has a state so not share it between different tables diff --git a/docs/configuration.md b/docs/configuration.md index 19899c061..498308b00 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -140,7 +140,10 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept * `TABLE_ASSEMBLIES_DEFAULT_COLUMNS`: The columns in assemblies tables, which are visible by default (when loading table for first time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns - are: `name`, `id`, `quantity`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `mountnames`, `instockAmount`, `storageLocations`, `addedDate`, `lastModified`. + are: `name`, `id`, `ipn`, `description`, `referencedAssemblies`, `edit`, `addedDate`, `lastModified`. +* `TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS`: The columns in assemblies bom tables, which are visible by default (when loading table for first time). + Also specify the default order of the columns. This is a comma separated list of column names. Available columns + are: `quantity`, `name`, `id`, `ipn`, `description`, `addedDate`, `lastModified`. * `CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME`: Use an %%ipn%% placeholder in the name of a assembly. Placeholder is replaced with the ipn input while saving. ### History/Eventlog-related settings diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php index ae2d6261b..ccdb24ac8 100644 --- a/migrations/Version20250304081039.php +++ b/migrations/Version20250304081039.php @@ -146,6 +146,7 @@ public function sqLiteUp(Schema $schema): void id_assembly INTEGER DEFAULT NULL, id_part INTEGER DEFAULT NULL, id_project INTEGER DEFAULT NULL, + id_referenced_assembly INTEGER DEFAULT NULL, price_currency_id INTEGER DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames CLOB NOT NULL, @@ -156,8 +157,9 @@ public function sqLiteUp(Schema $schema): void datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_8C74887E4AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE - CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887E22522999 FOREIGN KEY (id_referenced_assembly) REFERENCES assemblies (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE ) SQL); $this->addSql(<<<'SQL' @@ -169,6 +171,9 @@ public function sqLiteUp(Schema $schema): void $this->addSql(<<<'SQL' CREATE INDEX IDX_8C74887EF12E799E ON assembly_bom_entries (id_project) SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E22522999 ON assembly_bom_entries (id_referenced_assembly) + SQL); $this->addSql(<<<'SQL' CREATE INDEX IDX_8C74887E3FFDCD60 ON assembly_bom_entries (price_currency_id) SQL); diff --git a/migrations/Version20250627130848.php b/migrations/Version20250627130848.php new file mode 100644 index 000000000..16a6f3185 --- /dev/null +++ b/migrations/Version20250627130848.php @@ -0,0 +1,78 @@ +addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD id_referenced_assembly INT DEFAULT NULL AFTER id_project + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E22522999 FOREIGN KEY (id_referenced_assembly) REFERENCES assemblies (id) ON DELETE SET NULL + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E22522999 ON assembly_bom_entries (id_referenced_assembly) + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E22522999 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_8C74887E22522999 ON assembly_bom_entries + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP id_referenced_assembly + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function sqLiteDown(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD id_referenced_assembly INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E22522999 FOREIGN KEY (id_referenced_assembly) REFERENCES assemblies (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E22522999 ON assembly_bom_entries (id_referenced_assembly) + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887E22522999 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_8C74887E22522999 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP id_referenced_assembly + SQL); + } +} diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index c6d9cf20c..039751ea6 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -200,7 +200,7 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit * depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one, * to avoid having to insert it manually */ - $entity->setName(str_ireplace('%%ipn%%', $entity->getIpn(), $entity->getName())); + $entity->setName(str_ireplace('%%ipn%%', $entity->getIpn() ?? '', $entity->getName())); } $this->commentHelper->setMessage($form['log_comment']->getData()); @@ -233,6 +233,13 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit $repo = $this->entityManager->getRepository($this->entity_class); + $showParameters = true; + if ($this instanceof AssemblyAdminController) { + //currently not needed for assemblies + + $showParameters = false; + } + return $this->render($this->twig_template, [ 'entity' => $entity, 'form' => $form, @@ -242,7 +249,7 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit 'timeTravel' => $timeTravel_timestamp, 'repo' => $repo, 'partsContainingElement' => $repo instanceof PartsContainingRepositoryInterface, - 'showParameters' => !($this instanceof AssemblyAdminController), + 'showParameters' => $showParameters, ]); } @@ -303,7 +310,7 @@ protected function _new(Request $request, EntityManagerInterface $em, EntityImpo * depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one, * to avoid having to insert it manually */ - $new_entity->setName(str_ireplace('%%ipn%%', $new_entity->getIpn(), $new_entity->getName())); + $new_entity->setName(str_ireplace('%%ipn%%', $new_entity->getIpn() ?? '', $new_entity->getName())); } $this->commentHelper->setMessage($form['log_comment']->getData()); @@ -396,13 +403,20 @@ protected function _new(Request $request, EntityManagerInterface $em, EntityImpo } } + $showParameters = true; + if ($this instanceof AssemblyAdminController) { + //currently not needed for assemblies + + $showParameters = false; + } + return $this->render($this->twig_template, [ 'entity' => $new_entity, 'form' => $form, 'import_form' => $import_form, 'mass_creation_form' => $mass_creation_form, 'route_base' => $this->route_base, - 'showParameters' => !($this instanceof AssemblyAdminController), + 'showParameters' => $showParameters, ]); } diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 4335492e8..ca6ed8639 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -22,8 +22,10 @@ namespace App\Controller; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Parameters\AbstractParameter; use App\Entity\ProjectSystem\Project; +use App\Services\Attachments\AssemblyPreviewGenerator; use App\Services\Attachments\ProjectPreviewGenerator; use Symfony\Component\HttpFoundation\Response; use App\Entity\Attachments\Attachment; @@ -195,6 +197,44 @@ public function projects( return new JsonResponse($result); } + #[Route(path: '/assemblies/search/{query}', name: 'typeahead_assemblies')] + public function assemblies( + EntityManagerInterface $entityManager, + AssemblyPreviewGenerator $assemblyPreviewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" + ): JsonResponse { + $this->denyAccessUnlessGranted('@assemblies.read'); + + $result = []; + + $assemblyRepository = $entityManager->getRepository(Assembly::class); + + $assemblies = $assemblyRepository->autocompleteSearch($query, 100); + + foreach ($assemblies as $assembly) { + $preview_attachment = $assemblyPreviewGenerator->getTablePreviewAttachment($assembly); + + if($preview_attachment instanceof Attachment) { + $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); + } else { + $preview_url = ''; + } + + /** @var Assembly $assembly */ + $result[] = [ + 'id' => $assembly->getID(), + 'name' => $assembly->getName(), + 'category' => '', + 'footprint' => '', + 'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'), + 'image' => $preview_url, + ]; + } + + return new JsonResponse($result); + } + #[Route(path: '/parameters/{type}/search/{query}', name: 'typeahead_parameters', requirements: ['type' => '.+'])] public function parameters(string $type, EntityManagerInterface $entityManager, string $query = ""): JsonResponse { diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 7897ea36b..5991b9e1d 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -22,6 +22,7 @@ namespace App\Entity\AssemblySystem; +use App\Repository\AssemblyRepository; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; @@ -38,6 +39,7 @@ use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Attachments\Attachment; use App\Validator\Constraints\UniqueObjectCollection; +use App\Validator\Constraints\AssemblySystem\UniqueReferencedAssembly; use Doctrine\DBAL\Types\Types; use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Base\AbstractStructuralDBElement; @@ -58,7 +60,7 @@ * * @extends AbstractStructuralDBElement */ -#[ORM\Entity] +#[ORM\Entity(repositoryClass: AssemblyRepository::class)] #[ORM\Table(name: 'assemblies')] #[UniqueEntity(fields: ['ipn'], message: 'assembly.ipn.must_be_unique')] #[ORM\Index(columns: ['ipn'], name: 'assembly_idx_ipn')] @@ -109,8 +111,9 @@ class Assembly extends AbstractStructuralDBElement */ #[Assert\Valid] #[Groups(['extended', 'full', 'import'])] - #[ORM\OneToMany(mappedBy: 'assembly', targetEntity: AssemblyBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OneToMany(targetEntity: AssemblyBOMEntry::class, mappedBy: 'assembly', cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])] + #[UniqueReferencedAssembly] #[UniqueObjectCollection(message: 'assembly.bom_entry.project_already_in_bom', fields: ['project'])] #[UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name'])] protected Collection $bom_entries; @@ -386,4 +389,22 @@ public function validate(ExecutionContextInterface $context, $payload): void } } } + + /** + * Get all referenced assemblies which uses this assembly. + * + * @return Assembly[] all referenced assemblies which uses this assembly as a one-dimensional array of assembly objects + */ + public function getReferencedAssemblies(): array + { + $assemblies = []; + + foreach($this->bom_entries as $entry) { + if ($entry->getAssembly() !== null) { + $assemblies[] = $entry->getReferencedAssembly(); + } + } + + return $assemblies; + } } diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index f6ede2194..820fc2f5c 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -133,6 +133,18 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt #[Groups(['bom_entry:read', 'bom_entry:write', 'full'])] protected ?Part $part = null; + /** + * @var Assembly|null The associated assembly + */ + #[Assert\Expression( + '(this.getPart() === null or this.getReferencedAssembly() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))', + message: 'validator.assembly.bom_entry.only_part_or_assembly_allowed' + )] + #[ORM\ManyToOne(targetEntity: Assembly::class)] + #[ORM\JoinColumn(name: 'id_referenced_assembly', nullable: true, onDelete: 'SET NULL')] + #[Groups(['bom_entry:read', 'bom_entry:write', ])] + protected ?Assembly $referencedAssembly = null; + /** * @var Project|null The associated project */ @@ -237,6 +249,17 @@ public function setPart(?Part $part): AssemblyBOMEntry return $this; } + public function getReferencedAssembly(): ?Assembly + { + return $this->referencedAssembly; + } + + public function setReferencedAssembly(?Assembly $referencedAssembly): AssemblyBOMEntry + { + $this->referencedAssembly = $referencedAssembly; + return $this; + } + public function getProject(): ?Project { return $this->project; diff --git a/src/Form/AssemblySystem/AssemblyAddPartsType.php b/src/Form/AssemblySystem/AssemblyAddPartsType.php index 4d84881f0..1fa671266 100644 --- a/src/Form/AssemblySystem/AssemblyAddPartsType.php +++ b/src/Form/AssemblySystem/AssemblyAddPartsType.php @@ -51,14 +51,17 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder->add('bom_entries', AssemblyBOMEntryCollectionType::class, [ 'entry_options' => [ 'constraints' => [ - new UniqueEntity(fields: ['part', 'assembly'], message: 'assembly.bom_entry.part_already_in_bom', + new UniqueEntity(fields: ['part'], message: 'assembly.bom_entry.part_already_in_bom', entityClass: AssemblyBOMEntry::class), - new UniqueEntity(fields: ['name', 'assembly'], message: 'assembly.bom_entry.name_already_in_bom', + new UniqueEntity(fields: ['referencedAssembly'], message: 'assembly.bom_entry.assembly_already_in_bom', + entityClass: AssemblyBOMEntry::class), + new UniqueEntity(fields: ['name'], message: 'assembly.bom_entry.name_already_in_bom', entityClass: AssemblyBOMEntry::class, ignoreNull: true), ] ], 'constraints' => [ new UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part']), + new UniqueObjectCollection(message: 'assembly.bom_entry.assembly_already_in_bom', fields: ['referencedAssembly']), new UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name']), ] ]); diff --git a/src/Form/AssemblySystem/AssemblyBOMEntryType.php b/src/Form/AssemblySystem/AssemblyBOMEntryType.php index 42d463bd9..b86b2fd1b 100644 --- a/src/Form/AssemblySystem/AssemblyBOMEntryType.php +++ b/src/Form/AssemblySystem/AssemblyBOMEntryType.php @@ -5,6 +5,7 @@ namespace App\Form\AssemblySystem; use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Form\Type\AssemblySelectType; use App\Form\Type\BigDecimalNumberType; use App\Form\Type\CurrencyEntityType; use App\Form\Type\PartSelectType; @@ -42,6 +43,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'assembly.bom.project', 'required' => false, ]) + ->add('referencedAssembly', AssemblySelectType::class, [ + 'label' => 'assembly.bom.referencedAssembly', + 'required' => false, + ]) ->add('name', TextType::class, [ 'label' => 'assembly.bom.name', 'required' => false, diff --git a/src/Form/Type/AssemblySelectType.php b/src/Form/Type/AssemblySelectType.php new file mode 100644 index 000000000..10e858f26 --- /dev/null +++ b/src/Form/Type/AssemblySelectType.php @@ -0,0 +1,124 @@ +addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { + $form = $event->getForm(); + $config = $form->getConfig()->getOptions(); + $data = $event->getData() ?? []; + + $config['compound'] = false; + $config['choices'] = is_iterable($data) ? $data : [$data]; + $config['error_bubbling'] = true; + + $form->add('autocomplete', EntityType::class, $config); + }); + + //After form submit, we have to add the selected element as choice, otherwise the form will not accept this element + $builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) { + $data = $event->getData(); + $form = $event->getForm(); + $options = $form->get('autocomplete')->getConfig()->getOptions(); + + + if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) { + $options['choices'] = []; + } else { + //Extract the ID from the submitted data + $id = $data['autocomplete']; + //Find the element in the database + $element = $this->em->find($options['class'], $id); + + //Add the element as choice + $options['choices'] = [$element]; + $options['error_bubbling'] = true; + $form->add('autocomplete', EntityType::class, $options); + } + }); + + $builder->setDataMapper($this); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'class' => Assembly::class, + 'choice_label' => 'name', + 'compound' => true, + 'error_bubbling' => false, + ]); + + error_log($this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__'])); + + $resolver->setDefaults([ + 'attr' => [ + 'data-controller' => 'elements--assembly-select', + 'data-autocomplete' => $this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']), + 'autocomplete' => 'off', + ], + ]); + + $resolver->setDefaults([ + //Prefill the selected choice with the needed data, so the user can see it without an additional Ajax request + 'choice_attr' => ChoiceList::attr($this, function (?Assembly $assembly) { + if($assembly instanceof Assembly) { + //Determine the picture to show: + $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($assembly); + if ($preview_attachment instanceof Attachment) { + $preview_url = $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, + 'thumbnail_sm'); + } else { + $preview_url = ''; + } + } + + return $assembly instanceof Assembly ? [ + 'data-description' => $assembly->getDescription() ? mb_strimwidth($assembly->getDescription(), 0, 127, '...') : '', + 'data-category' => '', + 'data-footprint' => '', + 'data-image' => $preview_url, + ] : []; + }) + ]); + } + + public function mapDataToForms($data, \Traversable $forms): void + { + $form = current(iterator_to_array($forms, false)); + $form->setData($data); + } + + public function mapFormsToData(\Traversable $forms, &$data): void + { + $form = current(iterator_to_array($forms, false)); + $data = $form->getData(); + } + +} diff --git a/src/Repository/AssemblyRepository.php b/src/Repository/AssemblyRepository.php new file mode 100644 index 000000000..031e6e82b --- /dev/null +++ b/src/Repository/AssemblyRepository.php @@ -0,0 +1,69 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 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 . + */ + +namespace App\Repository; + +use App\Entity\AssemblySystem\Assembly; + +/** + * @template TEntityClass of Assembly + * @extends DBElementRepository + */ +class AssemblyRepository extends StructuralDBElementRepository +{ + /** + * @return Assembly[] + */ + public function autocompleteSearch(string $query, int $max_limits = 50): array + { + $qb = $this->createQueryBuilder('assembly'); + $qb->select('assembly') + ->where('ILIKE(assembly.name, :query) = TRUE') + ->orWhere('ILIKE(assembly.description, :query) = TRUE'); + + $qb->setParameter('query', '%'.$query.'%'); + + $qb->setMaxResults($max_limits); + $qb->orderBy('NATSORT(assembly.name)', 'ASC'); + + return $qb->getQuery()->getResult(); + } +} \ No newline at end of file diff --git a/src/Services/AssemblySystem/AssemblyBuildHelper.php b/src/Services/AssemblySystem/AssemblyBuildHelper.php index 3fb3221ac..b7f2df3ce 100644 --- a/src/Services/AssemblySystem/AssemblyBuildHelper.php +++ b/src/Services/AssemblySystem/AssemblyBuildHelper.php @@ -67,6 +67,7 @@ public function getMaximumBuildableCountForBOMEntry(AssemblyBOMEntry $assemblyBO public function getMaximumBuildableCount(Assembly $assembly): int { $maximum_buildable_count = PHP_INT_MAX; + /** @var AssemblyBOMEntry $bom_entry */ foreach ($assembly->getBomEntries() as $bom_entry) { //Skip BOM entries without a part (as we can not determine that) if (!$bom_entry->isPartBomEntry() && $bom_entry->getProject() === null) { @@ -76,8 +77,8 @@ public function getMaximumBuildableCount(Assembly $assembly): int //The maximum buildable count for the whole project is the minimum of all BOM entries if ($bom_entry->getPart() !== null) { $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); - } elseif ($bom_entry->getProject() !== null) { - $maximum_buildable_count = min($maximum_buildable_count, $this->projectBuildHelper->getMaximumBuildableCount($bom_entry->getProject())); + } elseif ($bom_entry->getReferencedAssembly() !== null) { + $maximum_buildable_count = min($maximum_buildable_count, $this->projectBuildHelper->getMaximumBuildableCount($bom_entry->getReferencedAssembly())); } } @@ -117,11 +118,12 @@ public function getNonBuildableAssemblyBomEntries(Assembly $assembly, int $numbe $nonBuildableEntries = []; + /** @var AssemblyBOMEntry $bomEntry */ foreach ($assembly->getBomEntries() as $bomEntry) { $part = $bomEntry->getPart(); //Skip BOM entries without a part (as we can not determine that) - if (!$part instanceof Part && $bomEntry->getAssembly() === null) { + if (!$part instanceof Part && $bomEntry->getReferencedAssembly() === null) { continue; } @@ -131,8 +133,8 @@ public function getNonBuildableAssemblyBomEntries(Assembly $assembly, int $numbe if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { $nonBuildableEntries[] = $bomEntry; } - } elseif ($bomEntry->getAssembly() !== null) { - $nonBuildableAssemblyEntries = $this->projectBuildHelper->getNonBuildableProjectBomEntries($bomEntry->getProject(), $number_of_builds); + } elseif ($bomEntry->getReferencedAssembly() !== null) { + $nonBuildableAssemblyEntries = $this->getNonBuildableAssemblyBomEntries($bomEntry->getReferencedAssembly(), $number_of_builds); $nonBuildableEntries = array_merge($nonBuildableEntries, $nonBuildableAssemblyEntries); } } diff --git a/src/Services/Attachments/AssemblyPreviewGenerator.php b/src/Services/Attachments/AssemblyPreviewGenerator.php new file mode 100644 index 000000000..9ecbbd070 --- /dev/null +++ b/src/Services/Attachments/AssemblyPreviewGenerator.php @@ -0,0 +1,93 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\Attachments; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\Attachment; + +class AssemblyPreviewGenerator +{ + public function __construct(protected AttachmentManager $attachmentHelper) + { + } + + /** + * Returns a list of attachments that can be used for previewing the assembly ordered by priority. + * + * @param Assembly $assembly the assembly for which the attachments should be determined + * + * @return (Attachment|null)[] + * + * @psalm-return list + */ + public function getPreviewAttachments(Assembly $assembly): array + { + $list = []; + + //Master attachment has top priority + $attachment = $assembly->getMasterPictureAttachment(); + if ($this->isAttachmentValidPicture($attachment)) { + $list[] = $attachment; + } + + //Then comes the other images of the assembly + foreach ($assembly->getAttachments() as $attachment) { + //Dont show the master attachment twice + if ($this->isAttachmentValidPicture($attachment) && $attachment !== $assembly->getMasterPictureAttachment()) { + $list[] = $attachment; + } + } + + return $list; + } + + /** + * Determines what attachment should be used for previewing a assembly (especially in assembly table). + * The returned attachment is guaranteed to be existing and be a picture. + * + * @param Assembly $assembly The assembly for which the attachment should be determined + */ + public function getTablePreviewAttachment(Assembly $assembly): ?Attachment + { + $attachment = $assembly->getMasterPictureAttachment(); + if ($this->isAttachmentValidPicture($attachment)) { + return $attachment; + } + + return null; + } + + /** + * Checks if a attachment is exising and a valid picture. + * + * @param Attachment|null $attachment the attachment that should be checked + * + * @return bool true if the attachment is valid + */ + protected function isAttachmentValidPicture(?Attachment $attachment): bool + { + return $attachment instanceof Attachment + && $attachment->isPicture() + && $this->attachmentHelper->isFileExisting($attachment); + } +} diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index fa9935c8f..3a0979028 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -188,6 +188,15 @@ private function getTreeViewUncached( $root_node->setExpanded($this->rootNodeExpandedByDefault); $root_node->setIcon($this->entityClassToRootNodeIcon($class)); + $generic = [$root_node]; + } elseif ($mode === 'assemblies' && $this->rootNodeEnabled) { + //We show the root node as a link to the list of all assemblies + $show_all_parts_url = $this->router->generate('assemblies_list'); + + $root_node = new TreeViewNode($this->entityClassToRootNodeString($class), $show_all_parts_url, $generic); + $root_node->setExpanded($this->rootNodeExpandedByDefault); + $root_node->setIcon($this->entityClassToRootNodeIcon($class)); + $generic = [$root_node]; } diff --git a/src/Twig/AssemblyTwigExtension.php b/src/Twig/AssemblyTwigExtension.php index d43c201e8..a8ca7719a 100644 --- a/src/Twig/AssemblyTwigExtension.php +++ b/src/Twig/AssemblyTwigExtension.php @@ -2,6 +2,7 @@ namespace App\Twig; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; @@ -14,6 +15,9 @@ public function getFunctions(): array ]; } + /** + * @param AssemblyBOMEntry[] $bomEntries + */ public function hasProject(array $bomEntries): bool { foreach ($bomEntries as $entry) { diff --git a/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssembly.php b/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssembly.php new file mode 100644 index 000000000..55a31440a --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssembly.php @@ -0,0 +1,34 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use Symfony\Component\Validator\Constraint; + +/** + * This constraint checks that the given UniqueReferencedAssembly is valid. + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class UniqueReferencedAssembly extends Constraint +{ + public string $message = 'assembly.bom_entry.assembly_already_in_bom'; +} \ No newline at end of file diff --git a/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssemblyValidator.php b/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssemblyValidator.php new file mode 100644 index 000000000..0e58c0663 --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssemblyValidator.php @@ -0,0 +1,48 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; + +class UniqueReferencedAssemblyValidator extends ConstraintValidator +{ + public function validate($value, Constraint $constraint) + { + $assemblies = []; + foreach ($value as $entry) { + $referencedAssemblyId = $entry->getReferencedAssembly()?->getId(); + if ($referencedAssemblyId === null) { + continue; + } + + if (isset($assemblies[$referencedAssemblyId])) { + $this->context->buildViolation($constraint->message) + ->atPath('referencedAssembly') + ->addViolation(); + return; + } + $assemblies[$referencedAssemblyId] = true; + } + } +} \ No newline at end of file diff --git a/templates/assemblies/build/_form.html.twig b/templates/assemblies/build/_form.html.twig index 0123ab010..97cace564 100644 --- a/templates/assemblies/build/_form.html.twig +++ b/templates/assemblies/build/_form.html.twig @@ -28,6 +28,8 @@ {% if bom_entry.part %} {{ bom_entry.part.name }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} + {% elseif bom_entry.referencedAssembly %} + {{ 'assembly.build.form.referencedAssembly'|trans({'%name%': bom_entry.referencedAssembly.name}) }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} {% else %} {{ bom_entry.name }} {% endif %} diff --git a/templates/assemblies/info/_attachments_info.html.twig b/templates/assemblies/info/_attachments_info.html.twig new file mode 100644 index 000000000..747426c3a --- /dev/null +++ b/templates/assemblies/info/_attachments_info.html.twig @@ -0,0 +1,91 @@ +{% import "helper.twig" as helper %} + + + + + + + + + + + + + + + + + {% for attachment in assembly.attachments %} + + + + + + + + + + {% endfor %} + + + +
    {% trans %}attachment.name{% endtrans %}{% trans %}attachment.attachment_type{% endtrans %}{% trans %}attachment.file_name{% endtrans %}{% trans %}attachment.file_size{% endtrans %}
    + {% import "components/attachments.macro.html.twig" as attachments %} + {{ attachments.attachment_icon(attachment, attachment_manager) }} + {{ attachment.name }}{{ attachment.attachmentType.fullPath }} + {% if attachment.hasInternal() %} + {{ attachment.filename }} + {% endif %} + + {% if not attachment.hasInternal() %} + + {% trans %}attachment.external_only{% endtrans %} + + {% elseif attachment_manager.internalFileExisting(attachment) %} + + {{ attachment_manager.humanFileSize(attachment) }} + + {% else %} + + {% trans %}attachment.file_not_found{% endtrans %} + + {% endif %} + {% if attachment.secure %} +
    + {% trans %}attachment.secure{% endtrans %} + + {% endif %} + {% if attachment == assembly.masterPictureAttachment %} +
    + + {% trans %}attachment.preview{% endtrans %} + + {% endif %} +
    + + + + + + + + + + +
    + + +
    +
    \ No newline at end of file diff --git a/templates/assemblies/info/info.html.twig b/templates/assemblies/info/info.html.twig index 166535a64..d787ea086 100644 --- a/templates/assemblies/info/info.html.twig +++ b/templates/assemblies/info/info.html.twig @@ -108,7 +108,7 @@ {% include "assemblies/info/_builds.html.twig" %}
    - {% include "parts/info/_attachments_info.html.twig" with {"part": assembly} %} + {% include "assemblies/info/_attachments_info.html.twig" with {"assembly": assembly} %}
    diff --git a/templates/form/collection_types_layout_assembly.html.twig b/templates/form/collection_types_layout_assembly.html.twig index 24964801a..6dc6d49a0 100644 --- a/templates/form/collection_types_layout_assembly.html.twig +++ b/templates/form/collection_types_layout_assembly.html.twig @@ -6,7 +6,7 @@ {# expand button #} {% trans %}assembly.bom.quantity{% endtrans %} - {% trans %}assembly.bom.partOrProject{% endtrans %} + {% trans %}assembly.bom.partOrAssembly{% endtrans %} {% trans %}assembly.bom.name{% endtrans %} {# Remove button #} @@ -44,18 +44,9 @@ {{ form_row(form.part) }} {{ form_errors(form.part) }} - - {% if form.vars.value is not null and form.vars.value.assembly is not null %} - {% if is_granted("@projects.read") or has_project(form.vars.value.assembly.bomEntries.toArray) %} -
    - {{ form_widget(form.project) }} - {{ form_errors(form.project) }} - {% endif %} - {% elseif is_granted("@projects.read") %} -
    - {{ form_widget(form.project) }} - {{ form_errors(form.project) }} - {% endif %} +
    + {{ form_widget(form.referencedAssembly) }} + {{ form_errors(form.referencedAssembly) }} {{ form_widget(form.name) }} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index b8c4f6033..4191cf30c 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -9867,7 +9867,7 @@ Element 3 assembly.edit.status - Stav + Stav sestavy @@ -13852,6 +13852,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz %value% (Součást) + + + part.table.name.value.for_assembly + %value% (Sestava) + + part.table.name.value.for_project @@ -13888,6 +13894,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Sestavy + + + assembly.referencedAssembly.labelp + Odkazované sestavy + + assembly.edit @@ -14056,6 +14068,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Sestavit + + + assembly.build.form.referencedAssembly + Sestava "%name%" + + assembly.builds.no_stocked_builds @@ -14110,6 +14128,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Projekt + + + assembly.bom.referencedAssembly + Sestava + + assembly.bom.name @@ -14146,10 +14170,10 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Importovat součásti do sestavy - + - assembly.bom.partOrProject - Součást + assembly.bom.partOrAssembly + Součást nebo sestava diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 4cec94054..c68acdd4b 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -9893,7 +9893,7 @@ Element 3 assembly.edit.status - Status + Samlingens status @@ -12569,6 +12569,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver %value% (Del) + + + part.table.name.value.for_assembly + %value% (Samling) + + part.table.name.value.for_project @@ -12605,6 +12611,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Samlinger + + + assembly.referencedAssembly.labelp + Refererede samlinger + + assembly.edit @@ -12773,6 +12785,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Byg + + + assembly.build.form.referencedAssembly + Samling "%name%" + + assembly.builds.no_stocked_builds @@ -12827,6 +12845,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Projekt + + + assembly.bom.referencedAssembly + Sammenstilling + + assembly.bom.name @@ -12863,10 +12887,10 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Importer dele til samling - + - assembly.bom.partOrProject - Del + assembly.bom.partOrAssembly + Del eller samling diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 3140a1368..bee171d41 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -4746,6 +4746,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr %value% (Bauteil) + + + part.table.name.value.for_assembly + %value% (Baugruppe) + + part.table.name.value.for_project @@ -9881,7 +9887,7 @@ Element 1 -> Element 1.2 assembly.edit.status - Status + Status Baugruppe @@ -13260,6 +13266,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Baugruppen + + + assembly.referencedAssembly.labelp + Referenzierte Baugruppen + + assembly.edit @@ -13428,6 +13440,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Bauen + + + asssembly.build.form.referencedAssembly + Baugruppe "%name%" + + assembly.builds.no_stocked_builds @@ -13482,6 +13500,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Projekt + + + assembly.bom.referencedAssembly + Baugruppe + + assembly.bom.name @@ -13518,10 +13542,10 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Importiere Parts für Baugruppe - + - assembly.bom.partOrProject - Bauteil oder Projekt + assembly.bom.partOrAssembly + Bauteil oder Baugruppe diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index fc03f08be..97b27f53f 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1541,6 +1541,12 @@ %value% (Μέρος) + + + part.table.name.value.for_assembly + %value% (Συναρμολόγηση) + + part.table.name.value.for_project @@ -1550,7 +1556,7 @@ assembly.edit.status - Κατάσταση + Κατάσταση συναρμολόγησης @@ -1613,6 +1619,12 @@ Συναρμολογήσεις + + + assembly.referencedAssembly.labelp + Αναφερόμενες συναρμολογήσεις + + assembly.edit @@ -1781,6 +1793,12 @@ Κατασκευή + + + assembly.build.form.referencedAssembly + Συναρμολόγηση "%name%" + + assembly.builds.no_stocked_builds @@ -1835,6 +1853,12 @@ έργο + + + assembly.bom.referencedAssembly + Συναρμολόγηση + + assembly.bom.name @@ -1871,10 +1895,10 @@ Εισαγωγή εξαρτημάτων συναρμολόγησης - + - assembly.bom.partOrProject - Εξάρτημα + assembly.bom.partOrAssembly + Μέρος ή συναρμολόγηση diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 18c124ff7..2100548d3 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -4747,6 +4747,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can %value% (Part) + + + part.table.name.value.for_assembly + %value% (Assembly) + + part.table.name.value.for_project @@ -9882,7 +9888,7 @@ Element 1 -> Element 1.2 assembly.edit.status - Project status + Assembly status @@ -13261,6 +13267,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Assemblies + + + assembly.referencedAssembly.labelp + Referenced assemblies + + assembly.edit @@ -13429,6 +13441,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Build + + + assembly.build.form.referencedAssembly + Assembly "%name%" + + assembly.builds.no_stocked_builds @@ -13483,6 +13501,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Project + + + assembly.bom.referencedAssembly + Assembly + + assembly.bom.name @@ -13519,10 +13543,10 @@ Please note, that you can not impersonate a disabled user. If you try you will g Import part list for assembly - + - assembly.bom.partOrProject - Part + assembly.bom.partOrAssembly + Part or assembly diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index edef6d968..d4ece66ea 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -4746,6 +4746,12 @@ Subelementos serán desplazados hacia arriba. %value% (Componente) + + + part.table.name.value.for_assembly + %value% (Ensamblaje) + + part.table.name.value.for_project @@ -9897,7 +9903,7 @@ Elemento 3 assembly.edit.status - Estatus + Estado del ensamblaje @@ -12777,6 +12783,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Ensamblajes + + + assembly.referencedAssembly.labelp + Conjuntos referenciados + + assembly.edit @@ -12945,6 +12957,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Construir + + + assembly.build.form.referencedAssembly + Ensamblaje "%name%" + + assembly.builds.no_stocked_builds @@ -12999,6 +13017,12 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Proyecto + + + assembly.bom.referencedAssembly + Ensamblaje + + assembly.bom.name @@ -13035,10 +13059,10 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Importar piezas para ensamblaje - + - assembly.bom.partOrProject - Pieza + assembly.bom.partOrAssembly + Parte o conjunto diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index f5c7dc97d..a75db3a3a 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -4709,6 +4709,12 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia %value% (Componente) + + + part.table.name.value.for_assembly + %value% (Assemblage) + + part.table.name.value.for_project @@ -9112,7 +9118,7 @@ exemple de ville assembly.edit.status - Statut + Statut de l'assemblage @@ -9175,6 +9181,12 @@ exemple de ville Assemblages + + + assembly.referencedAssembly.labelp + Assemblages référencés + + assembly.edit @@ -9343,6 +9355,12 @@ exemple de ville Construire + + + assembly.build.form.referencedAssembly + Assemblage "%name%" + + assembly.builds.no_stocked_builds @@ -9397,6 +9415,12 @@ exemple de ville Projet + + + assembly.bom.referencedAssembly + Assemblage + + assembly.bom.name @@ -9433,10 +9457,10 @@ exemple de ville Importer des pièces pour l'assemblage - + - assembly.bom.partOrProject - Pièce + assembly.bom.partOrAssembly + Pièce ou assemblage diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index e7628984a..12e20ca2c 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -4748,6 +4748,12 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi %value% (Componente) + + + part.table.name.value.for_assembly + %value% (Assemblaggio) + + part.table.name.value.for_project @@ -9899,7 +9905,7 @@ Element 3 assembly.edit.status - Stato + Stato dell'assemblaggio @@ -12755,6 +12761,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Assemblaggi + + + assembly.referencedAssembly.labelp + Assembly referenziati + + assembly.edit @@ -12923,6 +12935,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Costruire + + + assembly.build.form.referencedAssembly + Gruppo "%name%" + + assembly.builds.no_stocked_builds @@ -12977,6 +12995,12 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Progetto + + + assembly.bom.referencedAssembly + Assemblaggio + + assembly.bom.name @@ -13013,10 +13037,10 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Importa componenti per il gruppo - + - assembly.bom.partOrProject - Componente + assembly.bom.partOrAssembly + Parte o assieme diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index b61b585f3..b7be7490d 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -4709,6 +4709,12 @@ %value%(部品) + + + part.table.name.value.for_assembly + %value%(アセンブリ) + + part.table.name.value.for_project @@ -8849,7 +8855,7 @@ Exampletown assembly.edit.status - ステータス + アセンブリのステータス @@ -8912,6 +8918,12 @@ Exampletown アセンブリ一覧 + + + assembly.referencedAssembly.labelp + 参照されたアセンブリ + + assembly.edit @@ -9080,6 +9092,12 @@ Exampletown ビルド + + + assembly.build.form.referencedAssembly + アセンブリ「%name%」 + + assembly.builds.no_stocked_builds @@ -9134,6 +9152,12 @@ Exampletown プロジェクト + + + assembly.bom.referencedAssembly + アセンブリ + + assembly.bom.name diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index c52a4733a..fe17c3aca 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -730,6 +730,12 @@ %value% (Onderdeel) + + + part.table.name.value.for_assembly + %value% (Assemblage) + + part.table.name.value.for_project @@ -739,7 +745,7 @@ assembly.edit.status - Κατάσταση + Montagestatus @@ -802,6 +808,12 @@ Assemblages + + + assembly.referencedAssembly.labelp + Gerefereerde assemblages + + assembly.edit @@ -970,6 +982,12 @@ Bouwen + + + assembly.build.form.referencedAssembly + Assemblage "%name%" + + assembly.builds.no_stocked_builds @@ -1024,6 +1042,12 @@ Project + + + assembly.bom.referencedAssembly + Assemblage + + assembly.bom.name @@ -1060,10 +1084,10 @@ Importeer onderdelen voor assemblage - + - assembly.bom.partOrProject - Onderdeel + assembly.bom.partOrAssembly + Onderdeel of samenstelling diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index fc6eba18d..73b6e4fcc 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -4751,6 +4751,12 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo %value%(部品) + + + part.table.name.value.for_assembly + %value% (Złożenie) + + part.table.name.value.for_project @@ -9902,7 +9908,7 @@ Element 3 assembly.edit.status - Status + Status montażu @@ -12632,6 +12638,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Zespoły + + + assembly.referencedAssembly.labelp + Odwołane zestawy + + assembly.edit @@ -12800,6 +12812,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Zbuduj + + + assembly.build.form.referencedAssembly + Zespół "%name%" + + assembly.builds.no_stocked_builds @@ -12854,6 +12872,12 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Projekt + + + assembly.bom.referencedAssembly + Złożenie + + assembly.bom.name @@ -12890,10 +12914,10 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Importuj części dla zespołu - + - assembly.bom.partOrProject - Część + assembly.bom.partOrAssembly + Część lub zespół diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 252b857bd..751ff35f1 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -4757,6 +4757,12 @@ %value% (Часть) + + + part.table.name.value.for_assembly + %value% (Сборка) + + part.table.name.value.for_project @@ -9906,7 +9912,7 @@ assembly.edit.status - Статус + Статус сборки @@ -12732,6 +12738,12 @@ Сборки + + + assembly.referencedAssembly.labelp + Ссылочные сборки + + assembly.edit @@ -12900,6 +12912,12 @@ Собрать + + + assembly.build.form.referencedAssembly + Сборка "%name%" + + assembly.builds.no_stocked_builds @@ -12954,6 +12972,12 @@ Проект + + + assembly.bom.referencedAssembly + Сборка + + assembly.bom.name @@ -12990,10 +13014,10 @@ Импортировать детали для сборки - + - assembly.bom.partOrProject - Компонент + assembly.bom.partOrAssembly + Часть или сборка diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 9c0b5a76f..396feae3e 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -4755,6 +4755,12 @@ %value%(部件) + + + part.table.name.value.for_assembly + %value%(装配) + + part.table.name.value.for_project @@ -9905,7 +9911,7 @@ Element 3 assembly.edit.status - 状态 + 装配状态 @@ -12617,6 +12623,12 @@ Element 3 装配列表 + + + assembly.referencedAssembly.labelp + 引用的程序集 + + assembly.edit @@ -12785,6 +12797,12 @@ Element 3 构建 + + + assembly.build.form.referencedAssembly + 组件“%name%” + + assembly.builds.no_stocked_builds @@ -12839,6 +12857,12 @@ Element 3 项目 + + + assembly.bom.referencedAssembly + 组件 + + assembly.bom.name @@ -12875,10 +12899,10 @@ Element 3 导入组件的零件 - + - assembly.bom.partOrProject - 零件 + assembly.bom.partOrAssembly + 部件或组件 diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 607b8a3b6..1731c90cb 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -251,12 +251,6 @@ Musíte vybrat díl pro položku BOM dílu nebo nastavit název pro položku BOM bez dílu. - - - validator.project.bom_entry.only_part_or_assembly_allowed - Je povoleno vybrat pouze jednu součástku nebo sestavu. Upravit prosím svůj výběr! - - project.bom_entry.name_already_in_bom @@ -395,6 +389,12 @@ Tato součást již existuje ve skupině! + + + assembly.bom_entry.assembly_already_in_bom + Tato sestava již existuje jako položka v seznamu materiálů! + + assembly.bom_entry.project_already_in_bom @@ -413,6 +413,12 @@ Musíte vybrat součást nebo nastavit název pro nesoučást! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Je povoleno vybrat pouze jednu součástku nebo sestavu. Upravit prosím svůj výběr! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index f25712719..239e3572a 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -251,12 +251,6 @@ Du skal vælge en komponent eller angive et navn til en ikke-komponent styklistepost! - - - validator.project.bom_entry.only_part_or_assembly_allowed - Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! - - project.bom_entry.name_already_in_bom @@ -371,6 +365,12 @@ Denne del eksisterer allerede i gruppen! + + + assembly.bom_entry.assembly_already_in_bom + Denne samling findes allerede som en post! + + assembly.bom_entry.project_already_in_bom @@ -389,6 +389,12 @@ Du skal vælge en del eller sætte et navn for en ikke-del! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index abd191d9e..86b31e583 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -251,12 +251,6 @@ Sie müssen ein Bauteil bzw. eine Baugruppe auswählen, oder einen Namen für ein nicht-Bauteil BOM-Eintrag setzen! - - - validator.project.bom_entry.only_part_or_assembly_allowed - Es darf nur ein Bauteil oder eine Baugruppe ausgewählt werden. Bitte passen Sie Ihre Auswahl an! - - project.bom_entry.name_already_in_bom @@ -395,6 +389,12 @@ Dieses Bauteil existiert bereits in der Gruppe! + + + assembly.bom_entry.assembly_already_in_bom + Diese Baugruppe existiert bereits als Eintrag! + + assembly.bom_entry.project_already_in_bom @@ -413,6 +413,12 @@ Sie müssen ein Bauteil auswählen, oder einen Namen für den Eintrag setzen! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Es darf nur ein Bauteil oder eine Baugruppe ausgewählt werden. Bitte passen Sie Ihre Auswahl an! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index 580c91275..4e4278daf 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -13,12 +13,6 @@ Ο εσωτερικός αριθμός εξαρτήματος πρέπει να είναι μοναδικός. {{ value }} χρησιμοποιείται ήδη! - - - validator.project.bom_entry.only_part_or_assembly_allowed - Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! - - validator.bom_importer.invalid_import_type @@ -37,6 +31,12 @@ Αυτό το εξάρτημα υπάρχει ήδη στην ομάδα! + + + assembly.bom_entry.assembly_already_in_bom + Αυτή η συναρμολόγηση υπάρχει ήδη ως εγγραφή! + + assembly.bom_entry.project_already_in_bom @@ -55,6 +55,12 @@ Πρέπει να επιλέξετε ένα εξάρτημα ή να βάλετε ένα όνομα για ένα μη εξάρτημα! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index e20e7ff8c..59cabf552 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -251,12 +251,6 @@ You have to select a part or assembly, or set a name for a non-component Bom entry! - - - validator.project.bom_entry.only_part_or_assembly_allowed - Only one part or assembly may be selected. Please modify your selection! - - project.bom_entry.name_already_in_bom @@ -395,6 +389,12 @@ This part already exists in the list! + + + assembly.bom_entry.assembly_already_in_bom + This assembly already exists as an entry! + + assembly.bom_entry.project_already_in_bom @@ -413,6 +413,12 @@ You must select a part or set a name for the entry! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Only one part or assembly may be selected. Please modify your selection! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index e603bdaf8..aff68a185 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -203,12 +203,6 @@ L'emplacement de stockage a été marqué comme "Composant seul", par conséquent aucun nouveau composant ne peut être ajouté. - - - validator.project.bom_entry.only_part_or_assembly_allowed - Seule une pièce ou un assemblage peut être sélectionné. Veuillez ajuster votre sélection! - - validator.bom_importer.invalid_import_type @@ -227,6 +221,12 @@ Cette pièce existe déjà dans le groupe! + + + assembly.bom_entry.assembly_already_in_bom + Cet assemblage existe déjà en tant qu'entrée ! + + assembly.bom_entry.project_already_in_bom @@ -245,6 +245,12 @@ Vous devez sélectionner une pièce ou attribuer un nom pour un non-élément! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Seule une pièce ou un assemblage peut être sélectionné. Veuillez ajuster votre sélection! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index c0af03354..1ee5c06fe 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -251,12 +251,6 @@ Morate odabrati dio za unos u BOM ili postaviti naziv za unos koji nije dio. - - - validator.project.bom_entry.only_part_or_assembly_allowed - Dozvoljeno je odabrati samo jednu komponentu ili sklop. Molimo prilagodite svoj odabir! - - project.bom_entry.name_already_in_bom @@ -389,6 +383,12 @@ Ovaj dio već postoji u grupi! + + + assembly.bom_entry.assembly_already_in_bom + Ova se montaža već nalazi kao zapis! + + assembly.bom_entry.project_already_in_bom @@ -407,6 +407,12 @@ Morate odabrati dio ili unijeti naziv za nedio! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Dozvoljeno je odabrati samo jednu komponentu ili sklop. Molimo prilagodite svoj odabir! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index 88369640f..ac57a2cc4 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -251,12 +251,6 @@ È necessario selezionare un componente o assegnare un nome ad una voce BOM che non indica un componente! - - - validator.project.bom_entry.only_part_or_assembly_allowed - È consentito selezionare solo una parte o un assieme. Si prega di modificare la selezione! - - project.bom_entry.name_already_in_bom @@ -389,6 +383,12 @@ Questa parte è già presente nel gruppo! + + + assembly.bom_entry.assembly_already_in_bom + Questo assemblaggio è già presente come voce! + + assembly.bom_entry.project_already_in_bom @@ -407,6 +407,12 @@ È necessario selezionare una parte o inserire un nome per un non-parte! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + È consentito selezionare solo una parte o un assieme. Si prega di modificare la selezione! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index 070281ccc..a316707ad 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -203,12 +203,6 @@ 新しい部品を追加できません。保管場所は「1つの部品のみ」とマークされています。 - - - validator.project.bom_entry.only_part_or_assembly_allowed - 部品またはアセンブリのみ選択可能です。選択内容を調整してください! - - validator.bom_importer.invalid_import_type @@ -227,6 +221,12 @@ この部品はすでにグループに存在します! + + + assembly.bom_entry.assembly_already_in_bom + このアセンブリはすでにエントリとして存在します! + + assembly.bom_entry.project_already_in_bom @@ -245,6 +245,12 @@ 部品を選択するか、非部品の名前を入力する必要があります! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + 部品またはアセンブリのみ選択可能です。選択内容を調整してください! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 60713fa02..95c44ab4d 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -251,12 +251,6 @@ Należy wybrać część dla wpisu BOM części lub ustawić nazwę dla wpisu BOM niebędącego częścią. - - - validator.project.bom_entry.only_part_or_assembly_allowed - Można wybrać tylko jedną część lub zespół. Proszę dostosować swój wybór! - - project.bom_entry.name_already_in_bom @@ -389,6 +383,12 @@ Ten element już istnieje w grupie! + + + assembly.bom_entry.assembly_already_in_bom + To zestawienie jest już dodane jako wpis! + + assembly.bom_entry.project_already_in_bom @@ -407,6 +407,12 @@ Musisz wybrać element lub przypisać nazwę dla elementu niestandardowego! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Można wybrać tylko jedną część lub zespół. Proszę dostosować swój wybór! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 32540f8c7..425ede5f1 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -251,12 +251,6 @@ Вам необходимо выбрать компонент или задать имя для BOM, не относящейся к компоненту! - - - validator.project.bom_entry.only_part_or_assembly_allowed - Можно выбрать только деталь или сборку. Пожалуйста, измените ваш выбор! - - project.bom_entry.name_already_in_bom @@ -389,6 +383,12 @@ Эта деталь уже существует в группе! + + + assembly.bom_entry.assembly_already_in_bom + Этот сборочный узел уже добавлен как запись! + + assembly.bom_entry.project_already_in_bom @@ -407,6 +407,12 @@ Необходимо выбрать деталь или ввести название для недетали! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Можно выбрать только деталь или сборку. Пожалуйста, измените ваш выбор! + + validator.bom_importer.json_csv.quantity.required diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 4a0ec79e8..4a02523bb 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -251,12 +251,6 @@ 您必须为 BOM 条目选择部件,或为非部件 BOM 条目设置名称。 - - - validator.project.bom_entry.only_part_or_assembly_allowed - 只能选择一个零件或组件。请修改您的选择! - - project.bom_entry.name_already_in_bom @@ -377,6 +371,12 @@ 此零件已存在于组中! + + + assembly.bom_entry.assembly_already_in_bom + 此装配已经作为条目存在! + + assembly.bom_entry.project_already_in_bom @@ -395,6 +395,12 @@ 必须选择零件或为非零件指定名称! + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + 只能选择一个零件或组件。请修改您的选择! + + validator.bom_importer.json_csv.quantity.required From 47f2801f5616e6e01a4d2bbdd13e4b5ca75b24ae Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 3 Jul 2025 13:43:55 +0200 Subject: [PATCH 50/83] AssemblyBomEntriesDataTable anpassen --- .../AssemblyBomEntriesDataTable.php | 82 ++++++------------- 1 file changed, 23 insertions(+), 59 deletions(-) diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php index b31c72bc0..fed6850f4 100644 --- a/src/DataTables/AssemblyBomEntriesDataTable.php +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -22,12 +22,13 @@ */ namespace App\DataTables; -use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Helpers\AssemblyDataTableHelper; use App\DataTables\Helpers\ProjectDataTableHelper; use App\DataTables\Helpers\ColumnSortHelper; use App\DataTables\Helpers\PartDataTableHelper; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; use App\Entity\AssemblySystem\AssemblyBOMEntry; @@ -45,20 +46,20 @@ class AssemblyBomEntriesDataTable implements DataTableTypeInterface { public function __construct( - protected TranslatorInterface $translator, - protected PartDataTableHelper $partDataTableHelper, - protected ProjectDataTableHelper $projectDataTableHelper, - protected EntityURLGenerator $entityURLGenerator, - protected AmountFormatter $amountFormatter, - private string $visible_columns, - private ColumnSortHelper $csh + protected TranslatorInterface $translator, + protected PartDataTableHelper $partDataTableHelper, + protected ProjectDataTableHelper $projectDataTableHelper, + protected AssemblyDataTableHelper $assemblyDataTableHelper, + protected EntityURLGenerator $entityURLGenerator, + protected AmountFormatter $amountFormatter, + private string $visible_columns, + private ColumnSortHelper $csh ){ } public function configure(DataTable $dataTable, array $options): void { $this->csh - //->add('select', SelectColumn::class) ->add('picture', TextColumn::class, [ 'label' => '', 'className' => 'no-colvis', @@ -89,7 +90,7 @@ public function configure(DataTable $dataTable, array $options): void 'label' => $this->translator->trans('part.table.name'), 'orderField' => 'NATSORT(part.name)', 'render' => function ($value, AssemblyBOMEntry $context) { - if(!$context->getPart() instanceof Part && !$context->getProject() instanceof Project) { + if(!$context->getPart() instanceof Part && !$context->getReferencedAssembly() instanceof Assembly && !$context->getProject() instanceof Project) { return htmlspecialchars((string) $context->getName()); } @@ -97,6 +98,13 @@ public function configure(DataTable $dataTable, array $options): void $tmp = $this->partDataTableHelper->renderName($context->getPart()); $tmp = $this->translator->trans('part.table.name.value.for_part', ['%value%' => $tmp]); + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
    '.htmlspecialchars($context->getName()).''; + } + } elseif ($context->getReferencedAssembly() !== null) { + $tmp = $this->assemblyDataTableHelper->renderName($context->getReferencedAssembly()); + $tmp = $this->translator->trans('part.table.name.value.for_assembly', ['%value%' => $tmp]); + if($context->getName() !== null && $context->getName() !== '') { $tmp .= '
    '.htmlspecialchars($context->getName()).''; } @@ -127,59 +135,15 @@ public function configure(DataTable $dataTable, array $options): void ->add('description', MarkdownColumn::class, [ 'label' => $this->translator->trans('part.table.description'), 'data' => function (AssemblyBOMEntry $context) { - if($context->getPart() instanceof Part) { + if ($context->getPart() instanceof Part) { return $context->getPart()->getDescription(); + } elseif ($context->getReferencedAssembly() instanceof Assembly) { + return $context->getReferencedAssembly()->getDescription(); } //For non-part BOM entries show the comment field return $context->getComment(); }, ]) - ->add('category', EntityColumn::class, [ - 'label' => $this->translator->trans('part.table.category'), - 'property' => 'part.category', - 'orderField' => 'NATSORT(category.name)', - ]) - ->add('footprint', EntityColumn::class, [ - 'property' => 'part.footprint', - 'label' => $this->translator->trans('part.table.footprint'), - 'orderField' => 'NATSORT(footprint.name)', - ]) - ->add('manufacturer', EntityColumn::class, [ - 'property' => 'part.manufacturer', - 'label' => $this->translator->trans('part.table.manufacturer'), - 'orderField' => 'NATSORT(manufacturer.name)', - ]) - ->add('mountnames', TextColumn::class, [ - 'label' => 'assembly.bom.mountnames', - 'render' => function ($value, AssemblyBOMEntry $context) { - $html = ''; - - foreach (explode(',', $context->getMountnames()) as $mountname) { - $html .= sprintf('%s ', htmlspecialchars($mountname)); - } - return $html; - }, - ]) - ->add('instockAmount', TextColumn::class, [ - 'label' => 'assembly.bom.instockAmount', - 'render' => function ($value, AssemblyBOMEntry $context) { - if ($context->getPart() !== null) { - return $this->partDataTableHelper->renderAmount($context->getPart()); - } - - return ''; - } - ]) - ->add('storageLocations', TextColumn::class, [ - 'label' => 'part.table.storeLocations', - 'render' => function ($value, AssemblyBOMEntry $context) { - if ($context->getPart() !== null) { - return $this->partDataTableHelper->renderStorageLocations($context->getPart()); - } - - return ''; - } - ]) ->add('addedDate', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.addedDate'), ]) @@ -189,8 +153,7 @@ public function configure(DataTable $dataTable, array $options): void ; //Apply the user configured order and visibility and add the columns to the table - $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->visible_columns, - "TABLE_ASSEMBLIES_DEFAULT_COLUMNS"); + $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->visible_columns,"TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS"); $dataTable->addOrderBy('name'); @@ -214,6 +177,7 @@ private function getQuery(QueryBuilder $builder, array $options): void ->addSelect('part') ->from(AssemblyBOMEntry::class, 'bom_entry') ->leftJoin('bom_entry.part', 'part') + ->leftJoin('bom_entry.referencedAssembly', 'referencedAssembly') ->leftJoin('part.category', 'category') ->leftJoin('part.footprint', 'footprint') ->leftJoin('part.manufacturer', 'manufacturer') From 195f1903f45fbfe7c7648fab9068bc025d4b433c Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 3 Jul 2025 13:47:20 +0200 Subject: [PATCH 51/83] =?UTF-8?q?Assembly=20Listen=C3=BCbersicht=20umsetze?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/app/images.css | 5 + config/parameters.yaml | 6 - docs/configuration.md | 3 +- src/Controller/AssemblyController.php | 74 ++++++ src/DataTables/AssemblyDataTable.php | 249 ++++++++++++++++++ src/DataTables/Filters/AssemblyFilter.php | 68 +++++ .../Filters/AssemblySearchFilter.php | 183 +++++++++++++ .../Helpers/AssemblyDataTableHelper.php | 77 ++++++ .../Helpers/ProjectDataTableHelper.php | 2 +- src/Form/Filters/AssemblyFilterType.php | 114 ++++++++ .../assemblies/lists/_action_bar.html.twig | 6 + templates/assemblies/lists/_filter.html.twig | 62 +++++ templates/assemblies/lists/all_list.html.twig | 30 +++ templates/assemblies/lists/data.html.twig | 3 + translations/messages.cs.xlf | 126 +++++++++ translations/messages.da.xlf | 126 +++++++++ translations/messages.de.xlf | 126 +++++++++ translations/messages.el.xlf | 126 +++++++++ translations/messages.en.xlf | 126 +++++++++ translations/messages.es.xlf | 126 +++++++++ translations/messages.fr.xlf | 126 +++++++++ translations/messages.it.xlf | 126 +++++++++ translations/messages.ja.xlf | 126 +++++++++ translations/messages.nl.xlf | 126 +++++++++ translations/messages.pl.xlf | 126 +++++++++ translations/messages.ru.xlf | 126 +++++++++ translations/messages.zh.xlf | 126 +++++++++ 27 files changed, 2511 insertions(+), 9 deletions(-) create mode 100644 src/DataTables/AssemblyDataTable.php create mode 100644 src/DataTables/Filters/AssemblyFilter.php create mode 100644 src/DataTables/Filters/AssemblySearchFilter.php create mode 100644 src/DataTables/Helpers/AssemblyDataTableHelper.php create mode 100644 src/Form/Filters/AssemblyFilterType.php create mode 100644 templates/assemblies/lists/_action_bar.html.twig create mode 100644 templates/assemblies/lists/_filter.html.twig create mode 100644 templates/assemblies/lists/all_list.html.twig create mode 100644 templates/assemblies/lists/data.html.twig diff --git a/assets/css/app/images.css b/assets/css/app/images.css index 0212a85b7..132cab99b 100644 --- a/assets/css/app/images.css +++ b/assets/css/app/images.css @@ -61,3 +61,8 @@ .object-fit-cover { object-fit: cover; } + +.assembly-table-image { + max-height: 40px; + object-fit: contain; +} diff --git a/config/parameters.yaml b/config/parameters.yaml index 8c45b95a9..599fbe595 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -48,14 +48,8 @@ parameters: ###################################################################################################################### # Table settings ###################################################################################################################### -<<<<<<< HEAD partdb.table.assemblies.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_DEFAULT_COLUMNS)%' # The default columns in assembly tables and their order -======= - partdb.table.default_page_size: '%env(int:TABLE_DEFAULT_PAGE_SIZE)%' # The default number of entries shown per page in tables - partdb.table.parts.default_columns: '%env(trim:string:TABLE_PARTS_DEFAULT_COLUMNS)%' # The default columns in part tables and their order - partdb.table.assemblies.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_DEFAULT_COLUMNS)%' # The default columns in assembly tables and their order partdb.table.assemblies_bom.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS)%' # The default columns in assembly bom tables and their order ->>>>>>> 2779c55a (Baugruppen Stückliste um referenzierte Baugruppe erweitern) ###################################################################################################################### # Miscellaneous diff --git a/docs/configuration.md b/docs/configuration.md index 498308b00..242164bf5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -137,8 +137,7 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`. -* `TABLE_ASSEMBLIES_DEFAULT_COLUMNS`: The columns in assemblies tables, which are visible by default (when loading table for first - time). +* `TABLE_ASSEMBLIES_DEFAULT_COLUMNS`: The columns in assemblies tables, which are visible by default (when loading table for first time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns are: `name`, `id`, `ipn`, `description`, `referencedAssemblies`, `edit`, `addedDate`, `lastModified`. * `TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS`: The columns in assemblies bom tables, which are visible by default (when loading table for first time). diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php index 9106f6775..a1ba7fa65 100644 --- a/src/Controller/AssemblyController.php +++ b/src/Controller/AssemblyController.php @@ -23,15 +23,22 @@ namespace App\Controller; use App\DataTables\AssemblyBomEntriesDataTable; +use App\DataTables\AssemblyDataTable; +use App\DataTables\ErrorDataTable; +use App\DataTables\Filters\AssemblyFilter; use App\Entity\AssemblySystem\Assembly; use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Parts\Part; +use App\Exceptions\InvalidRegexException; use App\Form\AssemblySystem\AssemblyAddPartsType; use App\Form\AssemblySystem\AssemblyBuildType; +use App\Form\Filters\AssemblyFilterType; use App\Helpers\Assemblies\AssemblyBuildRequest; use App\Services\ImportExportSystem\BOMImporter; use App\Services\AssemblySystem\AssemblyBuildHelper; +use App\Services\Trees\NodesListBuilder; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\DBAL\Exception\DriverException; use Doctrine\ORM\EntityManagerInterface; use League\Csv\SyntaxError; use Omines\DataTablesBundle\DataTableFactory; @@ -54,9 +61,76 @@ class AssemblyController extends AbstractController public function __construct( private readonly DataTableFactory $dataTableFactory, private readonly TranslatorInterface $translator, + private readonly NodesListBuilder $nodesListBuilder ) { } + #[Route(path: '/list', name: 'assemblies_list')] + public function showAll(Request $request): Response + { + return $this->showListWithFilter($request,'assemblies/lists/all_list.html.twig'); + } + + /** + * Common implementation for the part list pages. + * @param Request $request The request to parse + * @param string $template The template that should be rendered + * @param callable|null $filter_changer A function that is called with the filter object as parameter. This function can be used to customize the filter + * @param callable|null $form_changer A function that is called with the form object as parameter. This function can be used to customize the form + * @param array $additonal_template_vars Any additional template variables that should be passed to the template + * @param array $additional_table_vars Any additional variables that should be passed to the table creation + */ + protected function showListWithFilter(Request $request, string $template, ?callable $filter_changer = null, ?callable $form_changer = null, array $additonal_template_vars = [], array $additional_table_vars = []): Response + { + $this->denyAccessUnlessGranted('@assemblies.read'); + + $formRequest = clone $request; + $formRequest->setMethod('GET'); + $filter = new AssemblyFilter($this->nodesListBuilder); + if($filter_changer !== null){ + $filter_changer($filter); + } + + $filterForm = $this->createForm(AssemblyFilterType::class, $filter, ['method' => 'GET']); + if($form_changer !== null) { + $form_changer($filterForm); + } + + $filterForm->handleRequest($formRequest); + + $table = $this->dataTableFactory->createFromType( + AssemblyDataTable::class, + array_merge(['filter' => $filter], $additional_table_vars), + ['lengthMenu' => AssemblyDataTable::LENGTH_MENU] + ) + ->handleRequest($request); + + if ($table->isCallback()) { + try { + try { + return $table->getResponse(); + } catch (DriverException $driverException) { + if ($driverException->getCode() === 1139) { + //Convert the driver exception to InvalidRegexException so it has the same handler as for SQLite + throw InvalidRegexException::fromDriverException($driverException); + } else { + throw $driverException; + } + } + } catch (InvalidRegexException $exception) { + $errors = $this->translator->trans('assembly.table.invalid_regex').': '.$exception->getReason(); + $request->request->set('order', []); + + return ErrorDataTable::errorTable($this->dataTableFactory, $request, $errors); + } + } + + return $this->render($template, array_merge([ + 'datatable' => $table, + 'filterForm' => $filterForm->createView(), + ], $additonal_template_vars)); + } + #[Route(path: '/{id}/info', name: 'assembly_info', requirements: ['id' => '\d+'])] public function info(Assembly $assembly, Request $request, AssemblyBuildHelper $buildHelper): Response { diff --git a/src/DataTables/AssemblyDataTable.php b/src/DataTables/AssemblyDataTable.php new file mode 100644 index 000000000..f3854ebca --- /dev/null +++ b/src/DataTables/AssemblyDataTable.php @@ -0,0 +1,249 @@ +. + */ + +declare(strict_types=1); + +namespace App\DataTables; + +use App\DataTables\Adapters\TwoStepORMAdapter; +use App\DataTables\Column\IconLinkColumn; +use App\DataTables\Column\LocaleDateTimeColumn; +use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Column\SelectColumn; +use App\DataTables\Filters\AssemblyFilter; +use App\DataTables\Filters\AssemblySearchFilter; +use App\DataTables\Helpers\AssemblyDataTableHelper; +use App\DataTables\Helpers\ColumnSortHelper; +use App\Doctrine\Helpers\FieldHelper; +use App\Entity\AssemblySystem\Assembly; +use App\Services\EntityURLGenerator; +use Doctrine\ORM\AbstractQuery; +use Doctrine\ORM\QueryBuilder; +use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; +use Omines\DataTablesBundle\Column\TextColumn; +use Omines\DataTablesBundle\DataTable; +use Omines\DataTablesBundle\DataTableTypeInterface; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; + +final class AssemblyDataTable implements DataTableTypeInterface +{ + const LENGTH_MENU = [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]; + + public function __construct( + private readonly EntityURLGenerator $urlGenerator, + private readonly TranslatorInterface $translator, + private readonly AssemblyDataTableHelper $assemblyDataTableHelper, + private readonly Security $security, + private readonly string $visible_columns, + private readonly ColumnSortHelper $csh, + ) { + } + + public function configureOptions(OptionsResolver $optionsResolver): void + { + $optionsResolver->setDefaults([ + 'filter' => null, + 'search' => null + ]); + + $optionsResolver->setAllowedTypes('filter', [AssemblyFilter::class, 'null']); + $optionsResolver->setAllowedTypes('search', [AssemblySearchFilter::class, 'null']); + } + + public function configure(DataTable $dataTable, array $options): void + { + $resolver = new OptionsResolver(); + $this->configureOptions($resolver); + $options = $resolver->resolve($options); + + $this->csh + ->add('select', SelectColumn::class, visibility_configurable: false) + ->add('picture', TextColumn::class, [ + 'label' => '', + 'className' => 'no-colvis', + 'render' => fn($value, Assembly $context) => $this->assemblyDataTableHelper->renderPicture($context), + ], visibility_configurable: false) + ->add('name', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.table.name'), + 'render' => fn($value, Assembly $context) => $this->assemblyDataTableHelper->renderName($context), + 'orderField' => 'NATSORT(assembly.name)' + ]) + ->add('id', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.table.id'), + ]) + ->add('ipn', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.table.ipn'), + 'orderField' => 'NATSORT(assembly.ipn)' + ]) + ->add('description', MarkdownColumn::class, [ + 'label' => $this->translator->trans('assembly.table.description'), + ]) + ->add('addedDate', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('assembly.table.addedDate'), + ]) + ->add('lastModified', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('assembly.table.lastModified'), + ]); + + //Add a assembly column to list where the assembly is used as referenced assembly as bom-entry, when the user has the permission to see the assemblies + if ($this->security->isGranted('read', Assembly::class)) { + $this->csh->add('referencedAssemblies', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.referencedAssembly.labelp'), + 'render' => function ($value, Assembly $context): string { + $assemblies = $context->getReferencedAssemblies(); + + $max = 5; + $tmp = ""; + + for ($i = 0; $i < min($max, count($assemblies)); $i++) { + $url = $this->urlGenerator->infoURL($assemblies[$i]); + $tmp .= sprintf('%s', $url, htmlspecialchars($assemblies[$i]->getName())); + if ($i < count($assemblies) - 1) { + $tmp .= ", "; + } + } + + if (count($assemblies) > $max) { + $tmp .= ", + ".(count($assemblies) - $max); + } + + return $tmp; + } + ]); + } + + $this->csh + ->add('edit', IconLinkColumn::class, [ + 'label' => $this->translator->trans('assembly.table.edit'), + 'href' => fn($value, Assembly $context) => $this->urlGenerator->editURL($context), + 'disabled' => fn($value, Assembly $context) => !$this->security->isGranted('edit', $context), + 'title' => $this->translator->trans('assembly.table.edit.title'), + ]); + + //Apply the user configured order and visibility and add the columns to the table + $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->visible_columns, "TABLE_ASSEMBLIES_DEFAULT_COLUMNS"); + + $dataTable->addOrderBy('name') + ->createAdapter(TwoStepORMAdapter::class, [ + 'filter_query' => $this->getFilterQuery(...), + 'detail_query' => $this->getDetailQuery(...), + 'entity' => Assembly::class, + 'hydrate' => AbstractQuery::HYDRATE_OBJECT, + //Use the simple total query, as we just want to get the total number of assemblies without any conditions + //For this the normal query would be pretty slow + 'simple_total_query' => true, + 'criteria' => [ + function (QueryBuilder $builder) use ($options): void { + $this->buildCriteria($builder, $options); + }, + new SearchCriteriaProvider(), + ], + 'query_modifier' => $this->addJoins(...), + ]); + } + + + private function getFilterQuery(QueryBuilder $builder): void + { + /* In the filter query we only select the IDs. The fetching of the full entities is done in the detail query. + * We only need to join the entities here, so we can filter by them. + * The filter conditions are added to this QB in the buildCriteria method. + * + * The amountSum field and the joins are dynamically added by the addJoins method, if the fields are used in the query. + * This improves the performance, as we do not need to join all tables, if we do not need them. + */ + $builder + ->select('assembly.id') + ->from(Assembly::class, 'assembly') + + //The other group by fields, are dynamically added by the addJoins method + ->addGroupBy('assembly'); + } + + private function getDetailQuery(QueryBuilder $builder, array $filter_results): void + { + $ids = array_map(static fn($row) => $row['id'], $filter_results); + + /* + * In this query we take the IDs which were filtered, paginated and sorted in the filter query, and fetch the + * full entities. + * We can do complex fetch joins, as we do not need to filter or sort here (which would kill the performance). + * The only condition should be for the IDs. + * It is important that elements are ordered the same way, as the IDs are passed, or ordering will be wrong. + * + * We do not require the subqueries like amountSum here, as it is not used to render the table (and only for sorting) + */ + $builder + ->select('assembly') + ->addSelect('master_picture_attachment') + ->addSelect('attachments') + ->from(Assembly::class, 'assembly') + ->leftJoin('assembly.master_picture_attachment', 'master_picture_attachment') + ->leftJoin('assembly.attachments', 'attachments') + ->where('assembly.id IN (:ids)') + ->setParameter('ids', $ids) + ->addGroupBy('assembly') + ->addGroupBy('master_picture_attachment') + ->addGroupBy('attachments'); + + //Get the results in the same order as the IDs were passed + FieldHelper::addOrderByFieldParam($builder, 'assembly.id', 'ids'); + } + + /** + * This function is called right before the filter query is executed. + * We use it to dynamically add joins to the query, if the fields are used in the query. + * @param QueryBuilder $builder + * @return QueryBuilder + */ + private function addJoins(QueryBuilder $builder): QueryBuilder + { + //Check if the query contains certain conditions, for which we need to add additional joins + //The join fields get prefixed with an underscore, so we can check if they are used in the query easy without confusing them for a assembly subfield + $dql = $builder->getDQL(); + + if (str_contains($dql, '_master_picture_attachment')) { + $builder->leftJoin('assembly.master_picture_attachment', '_master_picture_attachment'); + $builder->addGroupBy('_master_picture_attachment'); + } + if (str_contains($dql, '_attachments')) { + $builder->leftJoin('assembly.attachments', '_attachments'); + } + + return $builder; + } + + private function buildCriteria(QueryBuilder $builder, array $options): void + { + //Apply the search criterias first + if ($options['search'] instanceof AssemblySearchFilter) { + $search = $options['search']; + $search->apply($builder); + } + + //We do the most stuff here in the filter class + if ($options['filter'] instanceof AssemblyFilter) { + $filter = $options['filter']; + $filter->apply($builder); + } + } +} diff --git a/src/DataTables/Filters/AssemblyFilter.php b/src/DataTables/Filters/AssemblyFilter.php new file mode 100644 index 000000000..d8d07a1ec --- /dev/null +++ b/src/DataTables/Filters/AssemblyFilter.php @@ -0,0 +1,68 @@ +. + */ +namespace App\DataTables\Filters; + +use App\DataTables\Filters\Constraints\DateTimeConstraint; +use App\DataTables\Filters\Constraints\EntityConstraint; +use App\DataTables\Filters\Constraints\IntConstraint; +use App\DataTables\Filters\Constraints\TextConstraint; +use App\Entity\Attachments\AttachmentType; +use App\Services\Trees\NodesListBuilder; +use Doctrine\ORM\QueryBuilder; + +class AssemblyFilter implements FilterInterface +{ + + use CompoundFilterTrait; + + public readonly IntConstraint $dbId; + public readonly TextConstraint $ipn; + public readonly TextConstraint $name; + public readonly TextConstraint $description; + public readonly TextConstraint $comment; + public readonly DateTimeConstraint $lastModified; + public readonly DateTimeConstraint $addedDate; + + public readonly IntConstraint $attachmentsCount; + public readonly EntityConstraint $attachmentType; + public readonly TextConstraint $attachmentName; + + public function __construct(NodesListBuilder $nodesListBuilder) + { + $this->name = new TextConstraint('assembly.name'); + $this->description = new TextConstraint('assembly.description'); + $this->comment = new TextConstraint('assembly.comment'); + $this->dbId = new IntConstraint('assembly.id'); + $this->ipn = new TextConstraint('assembly.ipn'); + $this->addedDate = new DateTimeConstraint('assembly.addedDate'); + $this->lastModified = new DateTimeConstraint('assembly.lastModified'); + $this->attachmentsCount = new IntConstraint('COUNT(_attachments)'); + $this->attachmentType = new EntityConstraint($nodesListBuilder, AttachmentType::class, '_attachments.attachment_type'); + $this->attachmentName = new TextConstraint('_attachments.name'); + } + + public function apply(QueryBuilder $queryBuilder): void + { + $this->applyAllChildFilters($queryBuilder); + } +} diff --git a/src/DataTables/Filters/AssemblySearchFilter.php b/src/DataTables/Filters/AssemblySearchFilter.php new file mode 100644 index 000000000..1627cc612 --- /dev/null +++ b/src/DataTables/Filters/AssemblySearchFilter.php @@ -0,0 +1,183 @@ +. + */ +namespace App\DataTables\Filters; +use Doctrine\ORM\QueryBuilder; + +class AssemblySearchFilter implements FilterInterface +{ + + /** @var boolean Whether to use regex for searching */ + protected bool $regex = false; + + /** @var bool Use name field for searching */ + protected bool $name = true; + + /** @var bool Use description for searching */ + protected bool $description = true; + + /** @var bool Use comment field for searching */ + protected bool $comment = true; + + /** @var bool Use ordernr for searching */ + protected bool $ordernr = true; + + /** @var bool Use Internal part number for searching */ + protected bool $ipn = true; + + public function __construct( + /** @var string The string to query for */ + protected string $keyword + ) + { + } + + protected function getFieldsToSearch(): array + { + $fields_to_search = []; + + if($this->name) { + $fields_to_search[] = 'assembly.name'; + } + if($this->description) { + $fields_to_search[] = 'assembly.description'; + } + if ($this->comment) { + $fields_to_search[] = 'assembly.comment'; + } + if ($this->ipn) { + $fields_to_search[] = 'assembly.ipn'; + } + + return $fields_to_search; + } + + public function apply(QueryBuilder $queryBuilder): void + { + $fields_to_search = $this->getFieldsToSearch(); + + //If we have nothing to search for, do nothing + if ($fields_to_search === [] || $this->keyword === '') { + return; + } + + //Convert the fields to search to a list of expressions + $expressions = array_map(function (string $field): string { + if ($this->regex) { + return sprintf("REGEXP(%s, :search_query) = TRUE", $field); + } + + return sprintf("ILIKE(%s, :search_query) = TRUE", $field); + }, $fields_to_search); + + //Add Or concatenation of the expressions to our query + $queryBuilder->andWhere( + $queryBuilder->expr()->orX(...$expressions) + ); + + //For regex, we pass the query as is, for like we add % to the start and end as wildcards + if ($this->regex) { + $queryBuilder->setParameter('search_query', $this->keyword); + } else { + $queryBuilder->setParameter('search_query', '%' . $this->keyword . '%'); + } + } + + public function getKeyword(): string + { + return $this->keyword; + } + + public function setKeyword(string $keyword): AssemblySearchFilter + { + $this->keyword = $keyword; + return $this; + } + + public function isRegex(): bool + { + return $this->regex; + } + + public function setRegex(bool $regex): AssemblySearchFilter + { + $this->regex = $regex; + return $this; + } + + public function isName(): bool + { + return $this->name; + } + + public function setName(bool $name): AssemblySearchFilter + { + $this->name = $name; + return $this; + } + + public function isCategory(): bool + { + return $this->category; + } + + public function setCategory(bool $category): AssemblySearchFilter + { + $this->category = $category; + return $this; + } + + public function isDescription(): bool + { + return $this->description; + } + + public function setDescription(bool $description): AssemblySearchFilter + { + $this->description = $description; + return $this; + } + + public function isIPN(): bool + { + return $this->ipn; + } + + public function setIPN(bool $ipn): AssemblySearchFilter + { + $this->ipn = $ipn; + return $this; + } + + public function isComment(): bool + { + return $this->comment; + } + + public function setComment(bool $comment): AssemblySearchFilter + { + $this->comment = $comment; + return $this; + } + + +} diff --git a/src/DataTables/Helpers/AssemblyDataTableHelper.php b/src/DataTables/Helpers/AssemblyDataTableHelper.php new file mode 100644 index 000000000..dda563ea4 --- /dev/null +++ b/src/DataTables/Helpers/AssemblyDataTableHelper.php @@ -0,0 +1,77 @@ +. + */ + +namespace App\DataTables\Helpers; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\Attachment; +use App\Services\Attachments\AssemblyPreviewGenerator; +use App\Services\Attachments\AttachmentURLGenerator; +use App\Services\EntityURLGenerator; + +/** + * A helper service which contains common code to render columns for assembly related tables + */ +class AssemblyDataTableHelper +{ + public function __construct( + private readonly EntityURLGenerator $entityURLGenerator, + private readonly AssemblyPreviewGenerator $previewGenerator, + private readonly AttachmentURLGenerator $attachmentURLGenerator + ) { + } + + public function renderName(Assembly $context): string + { + $icon = ''; + + return sprintf( + '%s%s', + $this->entityURLGenerator->infoURL($context), + $icon, + htmlspecialchars($context->getName()) + ); + } + + public function renderPicture(Assembly $context): string + { + $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($context); + if (!$preview_attachment instanceof Attachment) { + return ''; + } + + $title = htmlspecialchars($preview_attachment->getName()); + if ($preview_attachment->getFilename()) { + $title .= ' ('.htmlspecialchars($preview_attachment->getFilename()).')'; + } + + return sprintf( + '%s', + 'Assembly image', + $this->attachmentURLGenerator->getThumbnailURL($preview_attachment), + $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_md'), + 'hoverpic assembly-table-image', + $title + ); + } +} diff --git a/src/DataTables/Helpers/ProjectDataTableHelper.php b/src/DataTables/Helpers/ProjectDataTableHelper.php index 0118d5d56..baa0e24e1 100644 --- a/src/DataTables/Helpers/ProjectDataTableHelper.php +++ b/src/DataTables/Helpers/ProjectDataTableHelper.php @@ -27,7 +27,7 @@ use App\Services\EntityURLGenerator; /** - * A helper service which contains common code to render columns for assembly related tables + * A helper service which contains common code to render columns for project related tables */ class ProjectDataTableHelper { diff --git a/src/Form/Filters/AssemblyFilterType.php b/src/Form/Filters/AssemblyFilterType.php new file mode 100644 index 000000000..acfbb1a8e --- /dev/null +++ b/src/Form/Filters/AssemblyFilterType.php @@ -0,0 +1,114 @@ +. + */ +namespace App\Form\Filters; + +use App\DataTables\Filters\AssemblyFilter; +use App\Entity\Attachments\AttachmentType; +use App\Form\Filters\Constraints\DateTimeConstraintType; +use App\Form\Filters\Constraints\NumberConstraintType; +use App\Form\Filters\Constraints\StructuralEntityConstraintType; +use App\Form\Filters\Constraints\TextConstraintType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ResetType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class AssemblyFilterType extends AbstractType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => AssemblyFilter::class, + 'csrf_protection' => false, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + /* + * Common tab + */ + + $builder->add('name', TextConstraintType::class, [ + 'label' => 'assembly.filter.name', + ]); + + $builder->add('description', TextConstraintType::class, [ + 'label' => 'assembly.filter.description', + ]); + + $builder->add('comment', TextConstraintType::class, [ + 'label' => 'assembly.filter.comment' + ]); + + /* + * Advanced tab + */ + + $builder->add('dbId', NumberConstraintType::class, [ + 'label' => 'assembly.filter.dbId', + 'min' => 1, + 'step' => 1, + ]); + + $builder->add('ipn', TextConstraintType::class, [ + 'label' => 'assembly.filter.ipn', + ]); + + $builder->add('lastModified', DateTimeConstraintType::class, [ + 'label' => 'lastModified' + ]); + + $builder->add('addedDate', DateTimeConstraintType::class, [ + 'label' => 'createdAt' + ]); + + /** + * Attachments count + */ + $builder->add('attachmentsCount', NumberConstraintType::class, [ + 'label' => 'assembly.filter.attachments_count', + 'step' => 1, + 'min' => 0, + ]); + + $builder->add('attachmentType', StructuralEntityConstraintType::class, [ + 'label' => 'attachment.attachment_type', + 'entity_class' => AttachmentType::class + ]); + + $builder->add('attachmentName', TextConstraintType::class, [ + 'label' => 'assembly.filter.attachmentName', + ]); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'filter.submit', + ]); + + $builder->add('discard', ResetType::class, [ + 'label' => 'filter.discard', + ]); + } +} diff --git a/templates/assemblies/lists/_action_bar.html.twig b/templates/assemblies/lists/_action_bar.html.twig new file mode 100644 index 000000000..37289812a --- /dev/null +++ b/templates/assemblies/lists/_action_bar.html.twig @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/templates/assemblies/lists/_filter.html.twig b/templates/assemblies/lists/_filter.html.twig new file mode 100644 index 000000000..11be7bc24 --- /dev/null +++ b/templates/assemblies/lists/_filter.html.twig @@ -0,0 +1,62 @@ +
    +
    + +
    +
    +
    + + + {{ form_start(filterForm, {"attr": {"data-controller": "helpers--form-cleanup", "data-action": "helpers--form-cleanup#submit"}}) }} + +
    +
    + {{ form_row(filterForm.name) }} + {{ form_row(filterForm.description) }} + {{ form_row(filterForm.comment) }} +
    + +
    + {{ form_row(filterForm.dbId) }} + {{ form_row(filterForm.ipn) }} + {{ form_row(filterForm.lastModified) }} + {{ form_row(filterForm.addedDate) }} +
    + +
    + {{ form_row(filterForm.attachmentsCount) }} + {{ form_row(filterForm.attachmentType) }} + {{ form_row(filterForm.attachmentName) }} +
    +
    + + {{ form_row(filterForm.submit) }} + {{ form_row(filterForm.discard) }} + +
    +
    + +
    +
    + + {# Retain the query parameters of the search form if it is existing #} + {% if searchFilter is defined %} + {% for property, value in searchFilter|to_array %} + + {% endfor %} + + {% endif %} + + {{ form_end(filterForm) }} +
    +
    +
    \ No newline at end of file diff --git a/templates/assemblies/lists/all_list.html.twig b/templates/assemblies/lists/all_list.html.twig new file mode 100644 index 000000000..70d75ad40 --- /dev/null +++ b/templates/assemblies/lists/all_list.html.twig @@ -0,0 +1,30 @@ +{% extends "base.html.twig" %} + +{% block title %} + {% trans %}assembly_list.all.title{% endtrans %} +{% endblock %} + +{% block content %} + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + + {% include "assemblies/lists/_filter.html.twig" %} +
    + + {% include "assemblies/lists/_action_bar.html.twig" with {'url_options': {}} %} + {% include "assemblies/lists/data.html.twig" %} + +{% endblock %} diff --git a/templates/assemblies/lists/data.html.twig b/templates/assemblies/lists/data.html.twig new file mode 100644 index 000000000..69e13e4f5 --- /dev/null +++ b/templates/assemblies/lists/data.html.twig @@ -0,0 +1,3 @@ +{% import "components/datatables.macro.html.twig" as datatables %} + +{{ datatables.partsDatatableWithForm(datatable) }} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 4191cf30c..34e21fa34 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -14619,5 +14619,131 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz
    + + + assembly_list.all.title + Všechny sestavy + + + + + assembly.edit.tab.common + Obecné + + + + + assembly.edit.tab.advanced + Pokročilé možnosti + + + + + assembly.edit.tab.attachments + Přílohy + + + + + assembly.filter.dbId + ID databáze + + + + + assembly.filter.ipn + Interní číslo dílu (IPN) + + + + + assembly.filter.name + Název + + + + + assembly.filter.description + Popis + + + + + assembly.filter.comment + Poznámky + + + + + assembly.filter.attachments_count + Počet příloh + + + + + assembly.filter.attachmentName + Název přílohy + + + + + assemblies.create.btn + Vytvořit novou sestavu + + + + + assembly.table.id + ID + + + + + assembly.table.name + Název + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Popis + + + + + assembly.table.addedDate + Přidáno + + + + + assembly.table.lastModified + Naposledy upraveno + + + + + assembly.table.edit + Upravit + + + + + assembly.table.edit.title + Upravit sestavu + + + + + assembly.table.invalid_regex + Neplatný regulární výraz (regex) + + diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index c68acdd4b..5901e9f5a 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -13336,5 +13336,131 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
    + + + assembly_list.all.title + Alle samlinger + + + + + assembly.edit.tab.common + Generelt + + + + + assembly.edit.tab.advanced + Avancerede indstillinger + + + + + assembly.edit.tab.attachments + Vedhæftede filer + + + + + assembly.filter.dbId + Database-ID + + + + + assembly.filter.ipn + Internt delnummer (IPN) + + + + + assembly.filter.name + Navn + + + + + assembly.filter.description + Beskrivelse + + + + + assembly.filter.comment + Kommentarer + + + + + assembly.filter.attachments_count + Antal vedhæftninger + + + + + assembly.filter.attachmentName + Vedhæftningens navn + + + + + assemblies.create.btn + Opret ny samling + + + + + assembly.table.id + ID + + + + + assembly.table.name + Navn + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Beskrivelse + + + + + assembly.table.addedDate + Tilføjet + + + + + assembly.table.lastModified + Sidst ændret + + + + + assembly.table.edit + Rediger + + + + + assembly.table.edit.title + Rediger samling + + + + + assembly.table.invalid_regex + Ugyldigt regulært udtryk (regex) + + diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index bee171d41..a7ece41a9 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -14621,5 +14621,131 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Min. Vorschaubilde-Breite (px)
    + + + assembly_list.all.title + Alle Baugruppen + + + + + assembly.edit.tab.common + Allgemein + + + + + assembly.edit.tab.advanced + Erweiterte Optionen + + + + + assembly.edit.tab.attachments + Dateianhänge + + + + + assembly.filter.dbId + Datenbank ID + + + + + assembly.filter.ipn + Internal Part Number (IPN) + + + + + assembly.filter.name + Name + + + + + assembly.filter.description + Beschreibung + + + + + assembly.filter.comment + Notizen + + + + + assembly.filter.attachments_count + Anzahl der Anhänge + + + + + assembly.filter.attachmentName + Name des Anhangs + + + + + assemblies.create.btn + Neue Baugruppe anlegen + + + + + assembly.table.id + ID + + + + + assembly.table.name + Name + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Beschreibung + + + + + assembly.table.addedDate + Hinzugefügt + + + + + assembly.table.lastModified + Zuletzt bearbeitet + + + + + assembly.table.edit + Ändern + + + + + assembly.table.edit.title + Baugruppe ändern + + + + + assembly.table.invalid_regex + Ungültiger regulärer Ausdruck (regex) + + diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 97b27f53f..fd5114539 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -2344,5 +2344,131 @@
    + + + assembly_list.all.title + Όλες οι συναρμολογήσεις + + + + + assembly.edit.tab.common + Γενικά + + + + + assembly.edit.tab.advanced + Προηγμένες επιλογές + + + + + assembly.edit.tab.attachments + Συνημμένα + + + + + assembly.filter.dbId + Αναγνωριστικό βάσης δεδομένων + + + + + assembly.filter.ipn + Εσωτερικός αριθμός εξαρτήματος (IPN) + + + + + assembly.filter.name + Όνομα + + + + + assembly.filter.description + Περιγραφή + + + + + assembly.filter.comment + Σχόλια + + + + + assembly.filter.attachments_count + Αριθμός συνημμένων + + + + + assembly.filter.attachmentName + Όνομα συνημμένου + + + + + assemblies.create.btn + Δημιουργία νέας συναρμολόγησης + + + + + assembly.table.id + Αναγνωριστικό + + + + + assembly.table.name + Όνομα + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Περιγραφή + + + + + assembly.table.addedDate + Προστέθηκε + + + + + assembly.table.lastModified + Τελευταία επεξεργασία + + + + + assembly.table.edit + Επεξεργασία + + + + + assembly.table.edit.title + Επεξεργασία συναρμολόγησης + + + + + assembly.table.invalid_regex + Μη έγκυρη κανονική έκφραση (regex) + + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 2100548d3..a1003f3f5 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -14622,5 +14622,131 @@ Please note, that you can not impersonate a disabled user. If you try you will g Preview image min width (px)
    + + + assembly_list.all.title + All assemblies + + + + + assembly.edit.tab.common + General + + + + + assembly.edit.tab.advanced + Advanced options + + + + + assembly.edit.tab.attachments + Attachments + + + + + assembly.filter.dbId + Database ID + + + + + assembly.filter.ipn + Internal Part Number (IPN) + + + + + assembly.filter.name + Name + + + + + assembly.filter.description + Description + + + + + assembly.filter.comment + Comments + + + + + assembly.filter.attachments_count + Number of attachments + + + + + assembly.filter.attachmentName + Attachment name + + + + + assemblies.create.btn + Create new assembly + + + + + assembly.table.id + ID + + + + + assembly.table.name + Name + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Description + + + + + assembly.table.addedDate + Added + + + + + assembly.table.lastModified + Last modified + + + + + assembly.table.edit + Edit + + + + + assembly.table.edit.title + Edit assembly + + + + + assembly.table.invalid_regex + Invalid regular expression (regex) + + diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index d4ece66ea..5854628c1 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -13508,5 +13508,131 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S
    + + + assembly_list.all.title + Todas las ensamblajes + + + + + assembly.edit.tab.common + General + + + + + assembly.edit.tab.advanced + Opciones avanzadas + + + + + assembly.edit.tab.attachments + Archivos adjuntos + + + + + assembly.filter.dbId + ID de la base de datos + + + + + assembly.filter.ipn + Número interno de pieza (IPN) + + + + + assembly.filter.name + Nombre + + + + + assembly.filter.description + Descripción + + + + + assembly.filter.comment + Comentarios + + + + + assembly.filter.attachments_count + Cantidad de adjuntos + + + + + assembly.filter.attachmentName + Nombre del adjunto + + + + + assemblies.create.btn + Crear una nueva ensamblaje + + + + + assembly.table.id + ID + + + + + assembly.table.name + Nombre + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Descripción + + + + + assembly.table.addedDate + Añadido + + + + + assembly.table.lastModified + Última modificación + + + + + assembly.table.edit + Editar + + + + + assembly.table.edit.title + Editar ensamblaje + + + + + assembly.table.invalid_regex + Expresión regular no válida (regex) + + diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index a75db3a3a..2b4341ca2 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -9918,5 +9918,131 @@ exemple de ville
    + + + assembly_list.all.title + Toutes les assemblages + + + + + assembly.edit.tab.common + Général + + + + + assembly.edit.tab.advanced + Options avancées + + + + + assembly.edit.tab.attachments + Pièces jointes + + + + + assembly.filter.dbId + ID de la base de données + + + + + assembly.filter.ipn + Numéro de pièce interne (IPN) + + + + + assembly.filter.name + Nom + + + + + assembly.filter.description + Description + + + + + assembly.filter.comment + Commentaires + + + + + assembly.filter.attachments_count + Nombre de pièces jointes + + + + + assembly.filter.attachmentName + Nom de la pièce jointe + + + + + assemblies.create.btn + Créer un nouvel assemblage + + + + + assembly.table.id + ID + + + + + assembly.table.name + Nom + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Description + + + + + assembly.table.addedDate + Ajouté + + + + + assembly.table.lastModified + Dernière modification + + + + + assembly.table.edit + Modifier + + + + + assembly.table.edit.title + Modifier l'assemblage + + + + + assembly.table.invalid_regex + Expression régulière invalide (regex) + + diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 12e20ca2c..c727eef82 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -13510,5 +13510,131 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Questo componente contiene più di uno stock. Cambia manualmente la posizione per selezionare quale stock scegliere.
    + + + assembly_list.all.title + Tutti gli assiemi + + + + + assembly.edit.tab.common + Generale + + + + + assembly.edit.tab.advanced + Opzioni avanzate + + + + + assembly.edit.tab.attachments + Allegati + + + + + assembly.filter.dbId + ID del database + + + + + assembly.filter.ipn + Numero interno di parte (IPN) + + + + + assembly.filter.name + Nome + + + + + assembly.filter.description + Descrizione + + + + + assembly.filter.comment + Commenti + + + + + assembly.filter.attachments_count + Numero di allegati + + + + + assembly.filter.attachmentName + Nome dell'allegato + + + + + assemblies.create.btn + Crea un nuovo assieme + + + + + assembly.table.id + ID + + + + + assembly.table.name + Nome + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Descrizione + + + + + assembly.table.addedDate + Aggiunto + + + + + assembly.table.lastModified + Ultima modifica + + + + + assembly.table.edit + Modifica + + + + + assembly.table.edit.title + Modifica l'assieme + + + + + assembly.table.invalid_regex + Espressione regolare non valida (regex) + + diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index b7be7490d..157b1cf26 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -9631,5 +9631,131 @@ Exampletown
    + + + assembly_list.all.title + すべてのアセンブリ + + + + + assembly.edit.tab.common + 一般 + + + + + assembly.edit.tab.advanced + 詳細オプション + + + + + assembly.edit.tab.attachments + 添付ファイル + + + + + assembly.filter.dbId + データベースID + + + + + assembly.filter.ipn + 内部部品番号(IPN) + + + + + assembly.filter.name + 名前 + + + + + assembly.filter.description + 説明 + + + + + assembly.filter.comment + コメント + + + + + assembly.filter.attachments_count + 添付ファイルの数 + + + + + assembly.filter.attachmentName + 添付ファイル名 + + + + + assemblies.create.btn + 新しいアセンブリを作成 + + + + + assembly.table.id + ID + + + + + assembly.table.name + 名前 + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + 説明 + + + + + assembly.table.addedDate + 追加日 + + + + + assembly.table.lastModified + 最終変更 + + + + + assembly.table.edit + 編集 + + + + + assembly.table.edit.title + アセンブリを編集 + + + + + assembly.table.invalid_regex + 無効な正規表現(regex) + + diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index fe17c3aca..44a48dcb3 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1569,5 +1569,131 @@
    + + + assembly_list.all.title + Alle assemblages + + + + + assembly.edit.tab.common + Algemeen + + + + + assembly.edit.tab.advanced + Geavanceerde opties + + + + + assembly.edit.tab.attachments + Bijlagen + + + + + assembly.filter.dbId + Database-ID + + + + + assembly.filter.ipn + Intern partnummer (IPN) + + + + + assembly.filter.name + Naam + + + + + assembly.filter.description + Beschrijving + + + + + assembly.filter.comment + Opmerkingen + + + + + assembly.filter.attachments_count + Aantal bijlagen + + + + + assembly.filter.attachmentName + Naam van de bijlage + + + + + assemblies.create.btn + Nieuwe assemblage aanmaken + + + + + assembly.table.id + ID + + + + + assembly.table.name + Naam + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Beschrijving + + + + + assembly.table.addedDate + Toegevoegd + + + + + assembly.table.lastModified + Laatst gewijzigd + + + + + assembly.table.edit + Bewerken + + + + + assembly.table.edit.title + Assemblage bewerken + + + + + assembly.table.invalid_regex + Ongeldige reguliere expressie (regex) + + diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 73b6e4fcc..171c585d6 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -13363,5 +13363,131 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli
    + + + assembly_list.all.title + Wszystkie zespoły + + + + + assembly.edit.tab.common + Ogólne + + + + + assembly.edit.tab.advanced + Zaawansowane + + + + + assembly.edit.tab.attachments + Załączniki + + + + + assembly.filter.dbId + ID bazy danych + + + + + assembly.filter.ipn + Wewnętrzny numer części (IPN) + + + + + assembly.filter.name + Nazwa + + + + + assembly.filter.description + Opis + + + + + assembly.filter.comment + Komentarze + + + + + assembly.filter.attachments_count + Liczba załączników + + + + + assembly.filter.attachmentName + Nazwa załącznika + + + + + assemblies.create.btn + Utwórz nowy zespół + + + + + assembly.table.id + ID + + + + + assembly.table.name + Nazwa + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Opis + + + + + assembly.table.addedDate + Dodano + + + + + assembly.table.lastModified + Ostatnia modyfikacja + + + + + assembly.table.edit + Edytuj + + + + + assembly.table.edit.title + Edytuj zespół + + + + + assembly.table.invalid_regex + Nieprawidłowe wyrażenie regularne (regex) + + diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 751ff35f1..7bd0f3bc4 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -13463,5 +13463,131 @@
    + + + assembly_list.all.title + Все сборки + + + + + assembly.edit.tab.common + Общие + + + + + assembly.edit.tab.advanced + Дополнительные параметры + + + + + assembly.edit.tab.attachments + Вложения + + + + + assembly.filter.dbId + ID базы данных + + + + + assembly.filter.ipn + Внутренний номер детали (IPN) + + + + + assembly.filter.name + Название + + + + + assembly.filter.description + Описание + + + + + assembly.filter.comment + Комментарии + + + + + assembly.filter.attachments_count + Количество вложений + + + + + assembly.filter.attachmentName + Имя вложения + + + + + assemblies.create.btn + Создать новую сборку + + + + + assembly.table.id + ID + + + + + assembly.table.name + Название + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Описание + + + + + assembly.table.addedDate + Добавлено + + + + + assembly.table.lastModified + Последнее изменение + + + + + assembly.table.edit + Редактировать + + + + + assembly.table.edit.title + Редактировать сборку + + + + + assembly.table.invalid_regex + Неверное регулярное выражение (regex) + + diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 396feae3e..9e8d8aa1f 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -13348,5 +13348,131 @@ Element 3
    + + + assembly_list.all.title + 所有组件 + + + + + assembly.edit.tab.common + 通用 + + + + + assembly.edit.tab.advanced + 高级选项 + + + + + assembly.edit.tab.attachments + 附件 + + + + + assembly.filter.dbId + 数据库ID + + + + + assembly.filter.ipn + 内部零件编号(IPN) + + + + + assembly.filter.name + 名称 + + + + + assembly.filter.description + 描述 + + + + + assembly.filter.comment + 评论 + + + + + assembly.filter.attachments_count + 附件数量 + + + + + assembly.filter.attachmentName + 附件名称 + + + + + assemblies.create.btn + 创建新组件 + + + + + assembly.table.id + ID + + + + + assembly.table.name + 名称 + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + 描述 + + + + + assembly.table.addedDate + 添加日期 + + + + + assembly.table.lastModified + 最后修改 + + + + + assembly.table.edit + 编辑 + + + + + assembly.table.edit.title + 编辑组件 + + + + + assembly.table.invalid_regex + 无效的正则表达式(regex) + + From 669ad371dfc0a7f46955b0be025c90a28a372fe9 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 3 Jul 2025 13:50:33 +0200 Subject: [PATCH 52/83] =?UTF-8?q?Assembly=20Konstanten=20in=20.env=20einf?= =?UTF-8?q?=C3=BCgen=20bzw.=20anpassen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.env b/.env index cfa3bb053..8d89896a6 100644 --- a/.env +++ b/.env @@ -35,6 +35,9 @@ DATABASE_EMULATE_NATURAL_SORT=0 # This must end with a slash! DEFAULT_URI="https://partdb.changeme.invalid/" +# Use an %%ipn%% placeholder in the name of a assembly. Placeholder is replaced with the ipn input while saving. +CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME=0 + ################################################################################### # Email settings ################################################################################### @@ -67,7 +70,8 @@ ERROR_PAGE_SHOW_HELP=1 # Configure which columns will be visible by default in the specific table (and in which order). # This is a comma separated list of column names. See documentation for available values. TABLE_PARTS_DEFAULT_COLUMNS=name,description,category,footprint,manufacturer,storage_location,amount -TABLE_ASSEMBLIES_DEFAULT_COLUMNS=quantity,manufacturer,name,description,category +TABLE_ASSEMBLIES_DEFAULT_COLUMNS=id,ipn,name,description,referencedAssemblies,edit +TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS=quantity,id,ipn,name,description ################################################################################### From e1c6b42b500616d9ed0c4f81b97b6eed5c222772 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 3 Jul 2025 14:47:19 +0200 Subject: [PATCH 53/83] Assembly getReferencedAssemblies korrigieren --- src/Entity/AssemblySystem/Assembly.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 5991b9e1d..9593dbb53 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -400,7 +400,7 @@ public function getReferencedAssemblies(): array $assemblies = []; foreach($this->bom_entries as $entry) { - if ($entry->getAssembly() !== null) { + if ($entry->getReferencedAssembly() !== null) { $assemblies[] = $entry->getReferencedAssembly(); } } From 186105105794c52a02b834acec0d19ece04065a1 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 7 Jul 2025 10:08:19 +0200 Subject: [PATCH 54/83] =?UTF-8?q?F=C3=BCge=20Unterst=C3=BCtzung=20f=C3=BCr?= =?UTF-8?q?=20Datenquellen-Synonyme=20hinzu.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ermöglicht benutzerdefinierte Synonyme für Datenquellen basierend auf Locale. Synonyme werden in verschiedenen Bereichen wie Bäumen, Übersetzungen und Vorlagen genutzt, um anpassbare Namen anzuzeigen. --- config/parameters.yaml | 14 ++++++ config/services.yaml | 19 ++++++++ src/Controller/AssemblyController.php | 1 + src/Services/Trees/ToolsTreeBuilder.php | 43 +++++++++++++++---- src/Services/Trees/TreeViewGenerator.php | 36 +++++++++++++--- src/Twig/DataSourceNameExtension.php | 42 ++++++++++++++++++ templates/admin/assembly_admin.html.twig | 4 +- templates/admin/category_admin.html.twig | 4 +- templates/admin/footprint_admin.html.twig | 4 +- templates/admin/manufacturer_admin.html.twig | 4 +- templates/admin/project_admin.html.twig | 4 +- templates/admin/storelocation_admin.html.twig | 4 +- templates/admin/supplier_admin.html.twig | 4 +- templates/components/tree_macros.html.twig | 24 ++++++----- templates/form/permission_layout.html.twig | 23 +++++++++- translations/messages.cs.xlf | 6 +++ translations/messages.da.xlf | 6 +++ translations/messages.de.xlf | 6 +++ translations/messages.el.xlf | 6 +++ translations/messages.en.xlf | 6 +++ translations/messages.es.xlf | 6 +++ translations/messages.fr.xlf | 6 +++ translations/messages.it.xlf | 6 +++ translations/messages.ja.xlf | 6 +++ translations/messages.nl.xlf | 6 +++ translations/messages.pl.xlf | 6 +++ translations/messages.ru.xlf | 6 +++ translations/messages.zh.xlf | 6 +++ 28 files changed, 272 insertions(+), 36 deletions(-) create mode 100644 src/Twig/DataSourceNameExtension.php diff --git a/config/parameters.yaml b/config/parameters.yaml index 599fbe595..0c0c9c81e 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -16,6 +16,20 @@ parameters: partdb.create_assembly_use_ipn_placeholder_in_name: '%env(bool:CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME)%' # Use an %%ipn%% placeholder in the name of an assembly. Placeholder is replaced with the ipn input while saving. + partdb.data_sources.synonyms: # Define your own synonyms for the given data sources + # Possible datasources: category, storagelocation, footprint, manufacturer, supplier, project, assembly + # Possible locales like the ones in 'partdb.locale_menu': en, de, it, fr, ru, ja, cs, da, zh, pl + #category: + #de: 'Bauteil Kategorien' + #en: 'Part categories' + #project: + #de: 'Geräte' + #en: 'Devices' + #assembly: + #de: 'Zusammengestellte Baugruppe' + #en: 'Combined assembly' + + ###################################################################################################################### # Users and Privacy ###################################################################################################################### diff --git a/config/services.yaml b/config/services.yaml index ff8ce2ec2..8256daa1f 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -198,6 +198,25 @@ services: $fontDirectory: '%kernel.project_dir%/var/dompdf/fonts/' $tmpDirectory: '%kernel.project_dir%/var/dompdf/tmp/' + #################################################################################################################### + # Trees + #################################################################################################################### + App\Services\Trees\TreeViewGenerator: + arguments: + $dataSourceSynonyms: '%partdb.data_sources.synonyms%' + App\Services\Trees\ToolsTreeBuilder: + arguments: + $dataSourceSynonyms: '%partdb.data_sources.synonyms%' + + #################################################################################################################### + # Twig Extensions + #################################################################################################################### + + App\Twig\DataSourceNameExtension: + arguments: + $dataSourceSynonyms: '%partdb.data_sources.synonyms%' + tags: [ 'twig.extension' ] + #################################################################################################################### # Part info provider system #################################################################################################################### diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php index a1ba7fa65..94e129643 100644 --- a/src/Controller/AssemblyController.php +++ b/src/Controller/AssemblyController.php @@ -29,6 +29,7 @@ use App\Entity\AssemblySystem\Assembly; use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Parts\Part; +use App\Entity\UserSystem\User; use App\Exceptions\InvalidRegexException; use App\Form\AssemblySystem\AssemblyAddPartsType; use App\Form\AssemblySystem\AssemblyBuildType; diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index 5f08b8183..a2c86b7e8 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -50,8 +50,15 @@ */ class ToolsTreeBuilder { - public function __construct(protected TranslatorInterface $translator, protected UrlGeneratorInterface $urlGenerator, protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator, protected Security $security) - { + public function __construct( + protected TranslatorInterface $translator, + protected UrlGeneratorInterface $urlGenerator, + protected TagAwareCacheInterface $cache, + protected UserCacheKeyGenerator $keyGenerator, + protected Security $security, + protected ?array $dataSourceSynonyms = [], + ) { + $this->dataSourceSynonyms = $dataSourceSynonyms ?? []; } /** @@ -161,43 +168,43 @@ protected function getEditNodes(): array } if ($this->security->isGranted('read', new Category())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.categories'), + $this->getTranslatedDataSourceOrSynonym('category', 'tree.tools.edit.categories', $this->translator->getLocale()), $this->urlGenerator->generate('category_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-tags'); } if ($this->security->isGranted('read', new Project())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.projects'), + $this->getTranslatedDataSourceOrSynonym('project', 'tree.tools.edit.projects', $this->translator->getLocale()), $this->urlGenerator->generate('project_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-archive'); } if ($this->security->isGranted('read', new Assembly())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.assemblies'), + $this->getTranslatedDataSourceOrSynonym('assembly', 'tree.tools.edit.assemblies', $this->translator->getLocale()), $this->urlGenerator->generate('assembly_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-list'); } if ($this->security->isGranted('read', new Supplier())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.suppliers'), + $this->getTranslatedDataSourceOrSynonym('supplier', 'tree.tools.edit.suppliers', $this->translator->getLocale()), $this->urlGenerator->generate('supplier_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-truck'); } if ($this->security->isGranted('read', new Manufacturer())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.manufacturer'), + $this->getTranslatedDataSourceOrSynonym('manufacturer', 'tree.tools.edit.manufacturer', $this->translator->getLocale()), $this->urlGenerator->generate('manufacturer_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-industry'); } if ($this->security->isGranted('read', new StorageLocation())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.storelocation'), + $this->getTranslatedDataSourceOrSynonym('storagelocation', 'tree.tools.edit.storelocation', $this->translator->getLocale()), $this->urlGenerator->generate('store_location_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-cube'); } if ($this->security->isGranted('read', new Footprint())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.footprint'), + $this->getTranslatedDataSourceOrSynonym('footprint', 'tree.tools.edit.footprint', $this->translator->getLocale()), $this->urlGenerator->generate('footprint_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-microchip'); } @@ -305,4 +312,22 @@ protected function getSystemNodes(): array return $nodes; } + + protected function getTranslatedDataSourceOrSynonym(string $dataSource, string $translationKey, string $locale): string + { + $currentTranslation = $this->translator->trans($translationKey); + + // Call alternatives from DataSourcesynonyms (if available) + if (!empty($this->dataSourceSynonyms[$dataSource][$locale])) { + $alternativeTranslation = $this->dataSourceSynonyms[$dataSource][$locale]; + + // Use alternative translation when it deviates from the standard translation + if ($alternativeTranslation !== $currentTranslation) { + return $alternativeTranslation; + } + } + + // Otherwise return the standard translation + return $currentTranslation; + } } diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index 3a0979028..d5358bfa2 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -68,9 +68,11 @@ public function __construct( protected TranslatorInterface $translator, private readonly UrlGeneratorInterface $router, private readonly SidebarSettings $sidebarSettings, + protected ?array $dataSourceSynonyms = [], ) { $this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled; $this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded; + $this->dataSourceSynonyms = $dataSourceSynonyms ?? []; } /** @@ -226,14 +228,16 @@ protected function entityClassToRootNodeHref(string $class): ?string protected function entityClassToRootNodeString(string $class): string { + $locale = $this->translator->getLocale(); + return match ($class) { - Category::class => $this->translator->trans('category.labelp'), - StorageLocation::class => $this->translator->trans('storelocation.labelp'), - Footprint::class => $this->translator->trans('footprint.labelp'), - Manufacturer::class => $this->translator->trans('manufacturer.labelp'), - Supplier::class => $this->translator->trans('supplier.labelp'), - Project::class => $this->translator->trans('project.labelp'), - Assembly::class => $this->translator->trans('assembly.labelp'), + Category::class => $this->getTranslatedOrSynonym('category', $locale), + StorageLocation::class => $this->getTranslatedOrSynonym('storelocation', $locale), + Footprint::class => $this->getTranslatedOrSynonym('footprint', $locale), + Manufacturer::class => $this->getTranslatedOrSynonym('manufacturer', $locale), + Supplier::class => $this->getTranslatedOrSynonym('supplier', $locale), + Project::class => $this->getTranslatedOrSynonym('project', $locale), + Assembly::class => $this->getTranslatedOrSynonym('assembly', $locale), default => $this->translator->trans('tree.root_node.text'), }; } @@ -290,4 +294,22 @@ public function getGenericTree(string $class, ?AbstractStructuralDBElement $pare return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line }); } + + protected function getTranslatedOrSynonym(string $key, string $locale): string + { + $currentTranslation = $this->translator->trans($key . '.labelp'); + + // Call alternatives from DataSourcesynonyms (if available) + if (!empty($this->dataSourceSynonyms[$key][$locale])) { + $alternativeTranslation = $this->dataSourceSynonyms[$key][$locale]; + + // Use alternative translation when it deviates from the standard translation + if ($alternativeTranslation !== $currentTranslation) { + return $alternativeTranslation; + } + } + + // Otherwise return the standard translation + return $currentTranslation; + } } diff --git a/src/Twig/DataSourceNameExtension.php b/src/Twig/DataSourceNameExtension.php new file mode 100644 index 000000000..1c02243f7 --- /dev/null +++ b/src/Twig/DataSourceNameExtension.php @@ -0,0 +1,42 @@ +translator = $translator; + $this->dataSourceSynonyms = $dataSourceSynonyms ?? []; + } + + public function getFunctions(): array + { + return [ + new TwigFunction('get_data_source_name', [$this, 'getDataSourceName']), + ]; + } + + /** + * Based on the locale and data source names, gives the right synonym value back or the default translator value. + */ + public function getDataSourceName(string $dataSourceName, string $defaultKey): string + { + $locale = $this->translator->getLocale(); + + // Use alternative dataSource synonym (if available) + if (isset($this->dataSourceSynonyms[$dataSourceName][$locale])) { + return $this->dataSourceSynonyms[$dataSourceName][$locale]; + } + + // Otherwise return the standard translation + return $this->translator->trans($defaultKey); + } +} \ No newline at end of file diff --git a/templates/admin/assembly_admin.html.twig b/templates/admin/assembly_admin.html.twig index e6a90dc09..def4eeb2e 100644 --- a/templates/admin/assembly_admin.html.twig +++ b/templates/admin/assembly_admin.html.twig @@ -3,7 +3,9 @@ {# @var entity App\Entity\AssemblySystem\Assembly #} {% block card_title %} - {% trans %}assembly.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('assembly', 'assembly.caption') %} + {% set translatedSource = 'assembly.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block edit_title %} diff --git a/templates/admin/category_admin.html.twig b/templates/admin/category_admin.html.twig index 5811640b9..f1fe7663a 100644 --- a/templates/admin/category_admin.html.twig +++ b/templates/admin/category_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}category.labelp{% endtrans %} + {% set dataSourceName = get_data_source_name('category', 'category.labelp') %} + {% set translatedSource = 'category.labelp'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block additional_pills %} diff --git a/templates/admin/footprint_admin.html.twig b/templates/admin/footprint_admin.html.twig index a2c3e4afd..a6acbe84e 100644 --- a/templates/admin/footprint_admin.html.twig +++ b/templates/admin/footprint_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}footprint.labelp{% endtrans %} + {% set dataSourceName = get_data_source_name('footprint', 'footprint.labelp') %} + {% set translatedSource = 'footprint.labelp'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block master_picture_block %} diff --git a/templates/admin/manufacturer_admin.html.twig b/templates/admin/manufacturer_admin.html.twig index 5db892c04..3ce9a124c 100644 --- a/templates/admin/manufacturer_admin.html.twig +++ b/templates/admin/manufacturer_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - {% trans %}manufacturer.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('manufacturer', 'manufacturer.caption') %} + {% set translatedSource = 'manufacturer.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block edit_title %} diff --git a/templates/admin/project_admin.html.twig b/templates/admin/project_admin.html.twig index dcf8c64cf..401be7cf7 100644 --- a/templates/admin/project_admin.html.twig +++ b/templates/admin/project_admin.html.twig @@ -3,7 +3,9 @@ {# @var entity App\Entity\ProjectSystem\Project #} {% block card_title %} - {% trans %}project.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('project', 'project.caption') %} + {% set translatedSource = 'project.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block edit_title %} diff --git a/templates/admin/storelocation_admin.html.twig b/templates/admin/storelocation_admin.html.twig index c93339dc1..1e60eeea2 100644 --- a/templates/admin/storelocation_admin.html.twig +++ b/templates/admin/storelocation_admin.html.twig @@ -2,7 +2,9 @@ {% import "label_system/dropdown_macro.html.twig" as dropdown %} {% block card_title %} - {% trans %}storelocation.labelp{% endtrans %} + {% set dataSourceName = get_data_source_name('storagelocation', 'storelocation.labelp') %} + {% set translatedSource = 'storelocation.labelp'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block additional_controls %} diff --git a/templates/admin/supplier_admin.html.twig b/templates/admin/supplier_admin.html.twig index ce38a5ca4..b5cf7b236 100644 --- a/templates/admin/supplier_admin.html.twig +++ b/templates/admin/supplier_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - {% trans %}supplier.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('supplier', 'supplier.caption') %} + {% set translatedSource = 'supplier.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block additional_panes %} diff --git a/templates/components/tree_macros.html.twig b/templates/components/tree_macros.html.twig index 2e55147a1..210a00633 100644 --- a/templates/components/tree_macros.html.twig +++ b/templates/components/tree_macros.html.twig @@ -1,14 +1,16 @@ {% macro sidebar_dropdown() %} + {% set currentLocale = app.request.locale %} + {# Format is [mode, route, label, show_condition] #} {% set data_sources = [ - ['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read')], - ['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read')], - ['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read')], - ['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')], - ['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')], - ['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')], - ['assembly', path('tree_assembly_root'), 'assembly.labelp', is_granted('@assemblies.read')], - ['tools', path('tree_tools'), 'tools.label', true], + ['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read'), 'category'], + ['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read'), 'storagelocation'], + ['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read'), 'footprint'], + ['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read'), 'manufacturer'], + ['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read'), 'supplier'], + ['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read'), 'project'], + ['assembly', path('tree_assembly_root'), 'assembly.labelp', is_granted('@assemblies.read'), 'assembly'], + ['tools', path('tree_tools'), 'tools.label', true, 'tool'], ] %} @@ -19,9 +21,9 @@ {% for source in data_sources %} {% if source[3] %} {# show_condition #} -
  • + >{{ get_data_source_name(source[4], source[2]) }} {% endif %} {% endfor %} {% endmacro %} @@ -62,4 +64,4 @@
    -{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/templates/form/permission_layout.html.twig b/templates/form/permission_layout.html.twig index 166147b4c..896a2defa 100644 --- a/templates/form/permission_layout.html.twig +++ b/templates/form/permission_layout.html.twig @@ -6,12 +6,31 @@
    {% else %} - {{ form.vars.label | trans }} + def{{ form.vars.label | trans }} {% endif %} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 34e21fa34..baecebb90 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -14745,5 +14745,11 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Neplatný regulární výraz (regex) + + + datasource.synonym + %name% (Váš synonymum: %synonym%) + + diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 5901e9f5a..07f6c1b37 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -13462,5 +13462,11 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Ugyldigt regulært udtryk (regex) + + + datasource.synonym + %name% (Dit synonym: %synonym%) + + diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index a7ece41a9..7e2c84385 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -14747,5 +14747,11 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Ungültiger regulärer Ausdruck (regex) + + + datasource.synonym + %name% (Ihr Synonym: %synonym%) + + diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index fd5114539..07143e5b7 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -2470,5 +2470,11 @@ Μη έγκυρη κανονική έκφραση (regex) + + + datasource.synonym + %name% (Το συνώνυμό σας: %synonym%) + + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index a1003f3f5..66292192f 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -14748,5 +14748,11 @@ Please note, that you can not impersonate a disabled user. If you try you will g Invalid regular expression (regex) + + + datasource.synonym + %name% (Your synonym: %synonym%) + + diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 5854628c1..d635333ff 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -13634,5 +13634,11 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Expresión regular no válida (regex) + + + datasource.synonym + %name% (Tu sinónimo: %synonym%) + + diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 2b4341ca2..34ebcd1f1 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -10044,5 +10044,11 @@ exemple de ville Expression régulière invalide (regex) + + + datasource.synonym + %name% (Votre synonyme : %synonym%) + + diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index c727eef82..b018468bb 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -13636,5 +13636,11 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Espressione regolare non valida (regex) + + + datasource.synonym + %name% (Il tuo sinonimo: %synonym%) + + diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 157b1cf26..4d1c82edf 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -9757,5 +9757,11 @@ Exampletown 無効な正規表現(regex) + + + datasource.synonym + %name% (あなたの同義語: %synonym%) + + diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 44a48dcb3..822191818 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1695,5 +1695,11 @@ Ongeldige reguliere expressie (regex) + + + datasource.synonym + %name% (Uw synoniem: %synonym%) + + diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 171c585d6..6a7e5d591 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -13489,5 +13489,11 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Nieprawidłowe wyrażenie regularne (regex) + + + datasource.synonym + %name% (Twój synonim: %synonym%) + + diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 7bd0f3bc4..2add6347a 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -13589,5 +13589,11 @@ Неверное регулярное выражение (regex) + + + datasource.synonym + %name% (Ваш синоним: %synonym%) + + diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 9e8d8aa1f..cd6b34b2c 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -13474,5 +13474,11 @@ Element 3 无效的正则表达式(regex) + + + datasource.synonym + %name% (您的同义词: %synonym%) + + From 9bf1d2bab7ed4be5076b4a50e8b60656c7473d8a Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 8 Jul 2025 09:05:53 +0200 Subject: [PATCH 55/83] =?UTF-8?q?F=C3=BCge=20Validierung=20f=C3=BCr=20zykl?= =?UTF-8?q?ische=20Baugruppenreferenzen=20hinzu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eine neue Validierung wurde implementiert, um zyklische Referenzen in Baugruppen zu erkennen. Entsprechende Fehlertexte wurden in allen unterstützten Sprachen hinzugefügt. Zudem wurde der Validator in die Entität AssemblyBOMEntry integriert. --- config/services.yaml | 3 + src/Entity/AssemblySystem/Assembly.php | 2 + .../AssemblySystem/AssemblyBOMEntry.php | 2 + .../AssemblySystem/AssemblyCycle.php | 39 +++++ .../AssemblySystem/AssemblyCycleValidator.php | 139 ++++++++++++++++++ translations/validators.cs.xlf | 6 + translations/validators.da.xlf | 6 + translations/validators.de.xlf | 6 + translations/validators.el.xlf | 6 + translations/validators.en.xlf | 6 + translations/validators.fr.xlf | 6 + translations/validators.hr.xlf | 6 + translations/validators.it.xlf | 6 + translations/validators.ja.xlf | 6 + translations/validators.pl.xlf | 6 + translations/validators.ru.xlf | 6 + translations/validators.zh.xlf | 6 + 17 files changed, 257 insertions(+) create mode 100644 src/Validator/Constraints/AssemblySystem/AssemblyCycle.php create mode 100644 src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php diff --git a/config/services.yaml b/config/services.yaml index 8256daa1f..659098ff3 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -168,6 +168,9 @@ services: arguments: $useAssemblyIpnPlaceholder: '%partdb.create_assembly_use_ipn_placeholder_in_name%' + App\Validator\Constraints\AssemblySystem\AssemblyCycleValidator: + tags: [ 'validator.constraint_validator' ] + #################################################################################################################### # Table settings #################################################################################################################### diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 9593dbb53..5ce060d8e 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -23,6 +23,7 @@ namespace App\Entity\AssemblySystem; use App\Repository\AssemblyRepository; +use App\Validator\Constraints\AssemblySystem\AssemblyCycle; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; @@ -113,6 +114,7 @@ class Assembly extends AbstractStructuralDBElement #[Groups(['extended', 'full', 'import'])] #[ORM\OneToMany(targetEntity: AssemblyBOMEntry::class, mappedBy: 'assembly', cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])] + #[AssemblyCycle] #[UniqueReferencedAssembly] #[UniqueObjectCollection(message: 'assembly.bom_entry.project_already_in_bom', fields: ['project'])] #[UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name'])] diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 820fc2f5c..6a3e82d3c 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -38,6 +38,7 @@ use App\Entity\Contracts\TimeStampableInterface; use App\Entity\ProjectSystem\Project; use App\Repository\DBElementRepository; +use App\Validator\Constraints\AssemblySystem\AssemblyCycle; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; @@ -140,6 +141,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt '(this.getPart() === null or this.getReferencedAssembly() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))', message: 'validator.assembly.bom_entry.only_part_or_assembly_allowed' )] + #[AssemblyCycle] #[ORM\ManyToOne(targetEntity: Assembly::class)] #[ORM\JoinColumn(name: 'id_referenced_assembly', nullable: true, onDelete: 'SET NULL')] #[Groups(['bom_entry:read', 'bom_entry:write', ])] diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyCycle.php b/src/Validator/Constraints/AssemblySystem/AssemblyCycle.php new file mode 100644 index 000000000..9d79b879c --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/AssemblyCycle.php @@ -0,0 +1,39 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use Symfony\Component\Validator\Constraint; + +/** + * This constraint checks that there is no cycle in bom configuration of the assembly + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class AssemblyCycle extends Constraint +{ + public string $message = 'assembly.bom_entry.assembly_cycle'; + + public function validatedBy(): string + { + return AssemblyCycleValidator::class; + } +} \ No newline at end of file diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php new file mode 100644 index 000000000..3483f94a1 --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php @@ -0,0 +1,139 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; +use ReflectionClass; + +/** + * Validator class to check for cycles in assemblies based on BOM entries. + * + * This validator ensures that the structure of assemblies does not contain circular dependencies + * by validating each entry in the Bill of Materials (BOM) of the given assembly. Additionally, + * it can handle form-submitted BOM entries to include these in the validation process. + */ +class AssemblyCycleValidator extends ConstraintValidator +{ + public function validate($value, Constraint $constraint): void + { + if (!$constraint instanceof AssemblyCycle) { + throw new UnexpectedTypeException($constraint, AssemblyCycle::class); + } + + if (!$value instanceof Assembly) { + return; + } + + $bomEntries = $value->getBomEntries()->toArray(); + + // Consider additional entries from the form + if ($this->context->getRoot()->has('bom_entries')) { + $formBomEntries = $this->context->getRoot()->get('bom_entries')->getData(); + if ($formBomEntries) { + $given = is_array($formBomEntries) ? $formBomEntries : iterator_to_array($formBomEntries); + foreach ($given as $givenIdx => $entry) { + if (in_array($entry, $bomEntries, true)) { + continue; + } else { + $bomEntries[$givenIdx] = $entry; + } + } + } + } + + $visitedAssemblies = []; + foreach ($bomEntries as $bomEntry) { + if ($this->hasCycle($bomEntry->getReferencedAssembly(), $value, $visitedAssemblies)) { + $this->addViolation($value, $constraint); + } + } + } + + private function hasCycle(?Assembly $currentAssembly, Assembly $originalAssembly, array &$visitedAssemblies): bool + { + if ($currentAssembly === null) { + return false; + } + + if (in_array($currentAssembly, $visitedAssemblies, true)) { + return true; + } + + $visitedAssemblies[] = $currentAssembly; + + foreach ($currentAssembly->getBomEntries() as $bomEntry) { + if ($this->hasCycle($bomEntry->getReferencedAssembly(), $originalAssembly, $visitedAssemblies)) { + return true; + } + } + + return false; + } + + /** + * Adds a violation to the current context if it hasn’t already been added. + * + * This method checks whether a violation with the same property path as the current violation + * already exists in the context. If such a violation is found, the current violation is not added again. + * The process involves reflection to access private or protected properties of violation objects. + * + * @param mixed $value The value that triggered the violation. + * @param Constraint $constraint The constraint containing the validation details. + * + */ + private function addViolation($value, Constraint $constraint): void + { + /** @var ConstraintViolationBuilder $buildViolation */ + $buildViolation = $this->context->buildViolation($constraint->message) + ->setParameter('%name%', $value->getName()); + + $alreadyAdded = false; + + try { + $reflectionClass = new ReflectionClass($buildViolation); + $property = $reflectionClass->getProperty('propertyPath'); + $propertyPath = $property->getValue($buildViolation); + + $availableViolations = $this->context->getViolations(); + + foreach ($availableViolations as $tmpViolation) { + $tmpReflectionClass = new ReflectionClass($tmpViolation); + $tmpProperty = $tmpReflectionClass->getProperty('propertyPath'); + $tmpPropertyPath = $tmpProperty->getValue($tmpViolation); + + if ($tmpPropertyPath === $propertyPath) { + $alreadyAdded = true; + } + } + } catch (\ReflectionException) { + } + + if (!$alreadyAdded) { + $buildViolation->addViolation(); + } + } +} \ No newline at end of file diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 1731c90cb..7ee171b26 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -395,6 +395,12 @@ Tato sestava již existuje jako položka v seznamu materiálů! + + + assembly.bom_entry.assembly_cycle + Byl zjištěn cyklus: Sestava "%name%" nepřímo odkazuje sama na sebe. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 239e3572a..24fa330af 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -371,6 +371,12 @@ Denne samling findes allerede som en post! + + + assembly.bom_entry.assembly_cycle + En cyklus blev opdaget: Samlingen "%name%" refererer indirekte til sig selv. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 86b31e583..fa84354b9 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -395,6 +395,12 @@ Diese Baugruppe existiert bereits als Eintrag! + + + assembly.bom_entry.assembly_cycle + Ein Zyklus wurde entdeckt: Die Baugruppe "%name%" referenziert sich indirekt selbst. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index 4e4278daf..bc9b0947e 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -37,6 +37,12 @@ Αυτή η συναρμολόγηση υπάρχει ήδη ως εγγραφή! + + + assembly.bom_entry.assembly_cycle + Εντοπίστηκε κύκλος: Η συναρμολόγηση "%name%" αναφέρεται έμμεσα στον εαυτό της. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 59cabf552..7d9beb4ec 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -395,6 +395,12 @@ This assembly already exists as an entry! + + + assembly.bom_entry.assembly_cycle + A cycle was detected: the assembly "%name%" indirectly references itself. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index aff68a185..1c9c53029 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -227,6 +227,12 @@ Cet assemblage existe déjà en tant qu'entrée ! + + + assembly.bom_entry.assembly_cycle + Un cycle a été détecté : L'assemblage "%name%" se réfère indirectement à lui-même. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 1ee5c06fe..89d470e7e 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -389,6 +389,12 @@ Ova se montaža već nalazi kao zapis! + + + assembly.bom_entry.assembly_cycle + Otkriven je ciklus: Sklop "%name%" neizravno referencira samog sebe. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index ac57a2cc4..e9b528bb7 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -389,6 +389,12 @@ Questo assemblaggio è già presente come voce! + + + assembly.bom_entry.assembly_cycle + È stato rilevato un ciclo: L'assemblaggio "%name%" fa riferimento indirettamente a sé stesso. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index a316707ad..80ec65ff2 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -227,6 +227,12 @@ このアセンブリはすでにエントリとして存在します! + + + assembly.bom_entry.assembly_cycle + 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 95c44ab4d..5df01cd6a 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -389,6 +389,12 @@ To zestawienie jest już dodane jako wpis! + + + assembly.bom_entry.assembly_cycle + 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 425ede5f1..8bf08ab3c 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -389,6 +389,12 @@ Этот сборочный узел уже добавлен как запись! + + + assembly.bom_entry.assembly_cycle + Обнаружен цикл: Сборка «%name%» косвенно ссылается на саму себя. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 4a02523bb..87c507c19 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -377,6 +377,12 @@ 此装配已经作为条目存在! + + + assembly.bom_entry.assembly_cycle + 检测到循环:装配体“%name%”间接引用了其自身。 + + assembly.bom_entry.project_already_in_bom From 3d4f55d9eee5e39b203de0d05c8f3b2af8a5358f Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 1 Apr 2025 16:10:10 +0200 Subject: [PATCH 56/83] =?UTF-8?q?Erweiterungst=C3=A4tigkeiten=20zur=20IPN-?= =?UTF-8?q?Vorschlagsliste=20anhand=20von=20Pr=C3=A4fixen=20aus=20den=20Ka?= =?UTF-8?q?tegorien?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 18 ++ .../elements/ipn_suggestion_controller.js | 220 ++++++++++++++++++ config/parameters.yaml | 3 + config/services.yaml | 24 ++ docs/configuration.md | 6 + migrations/Version20250325073036.php | 23 ++ src/Controller/PartController.php | 16 +- src/Controller/TypeaheadController.php | 27 ++- src/Entity/Parts/Category.php | 17 ++ src/Entity/Parts/Part.php | 2 - .../PartTraits/AdvancedPropertyTrait.php | 4 +- .../UserSystem/PartUniqueIpnSubscriber.php | 73 ++++++ src/Form/AdminPages/CategoryAdminForm.php | 11 + src/Form/Part/PartBaseType.php | 8 + src/Repository/PartRepository.php | 119 ++++++++++ .../Constraints/UniquePartIpnConstraint.php | 20 ++ .../Constraints/UniquePartIpnValidator.php | 38 +++ templates/admin/category_admin.html.twig | 1 + templates/parts/edit/_advanced.html.twig | 11 +- translations/messages.cs.xlf | 66 ++++++ translations/messages.da.xlf | 66 ++++++ translations/messages.de.xlf | 66 ++++++ translations/messages.el.xlf | 66 ++++++ translations/messages.en.xlf | 66 ++++++ translations/messages.es.xlf | 66 ++++++ translations/messages.fr.xlf | 66 ++++++ translations/messages.it.xlf | 66 ++++++ translations/messages.ja.xlf | 66 ++++++ translations/messages.nl.xlf | 66 ++++++ translations/messages.pl.xlf | 66 ++++++ translations/messages.ru.xlf | 66 ++++++ translations/messages.zh.xlf | 66 ++++++ 32 files changed, 1489 insertions(+), 10 deletions(-) create mode 100644 assets/controllers/elements/ipn_suggestion_controller.js create mode 100644 migrations/Version20250325073036.php create mode 100644 src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php create mode 100644 src/Validator/Constraints/UniquePartIpnConstraint.php create mode 100644 src/Validator/Constraints/UniquePartIpnValidator.php diff --git a/.env b/.env index 8d89896a6..2b98255f1 100644 --- a/.env +++ b/.env @@ -53,6 +53,8 @@ EMAIL_SENDER_EMAIL=noreply@partdb.changeme EMAIL_SENDER_NAME="Part-DB Mailer" # Set this to 1 to allow reset of a password per email ALLOW_EMAIL_PW_RESET=0 +# Set this to 0 to allow to enter already available IPN. In this case a unique increment is appended to the user input. +ENFORCE_UNIQUE_IPN=1 ################################################################################### # Error pages settings @@ -126,6 +128,22 @@ NO_URL_REWRITE_AVAILABLE=0 # Set to 1, if Part-DB should redirect all HTTP requests to HTTPS. You dont need to configure this, if your webserver already does this. REDIRECT_TO_HTTPS=0 +# If you want to use fixer.io for currency conversion, you have to set this to your API key +FIXER_API_KEY=CHANGEME + +# Override value if you want to show to show a given text on homepage. +# When this is empty the content of config/banner.md is used as banner +BANNER="" + +# Enable the part image overlay which shows name and filename of the picture +SHOW_PART_IMAGE_OVERLAY=1 + +# Define the number of digits used for the incremental numbering of parts in the IPN (Internal Part Number) autocomplete system. +AUTOCOMPLETE_PART_DIGITS=4 + +APP_ENV=prod +APP_SECRET=a03498528f5a5fc089273ec9ae5b2849 + # Set this to zero, if you want to disable the year 2038 bug check on 32-bit systems (it will cause errors with current 32-bit PHP versions) DISABLE_YEAR2038_BUG_CHECK=0 diff --git a/assets/controllers/elements/ipn_suggestion_controller.js b/assets/controllers/elements/ipn_suggestion_controller.js new file mode 100644 index 000000000..088c07b33 --- /dev/null +++ b/assets/controllers/elements/ipn_suggestion_controller.js @@ -0,0 +1,220 @@ +import { Controller } from "@hotwired/stimulus"; +import "../../css/components/autocomplete_bootstrap_theme.css"; + +export default class extends Controller { + static targets = ["input"]; + static values = { + partId: Number, + partCategoryId: Number, + suggestions: Object, + commonSectionHeader: String, // Dynamic header for common Prefixes + partIncrementHeader: String, // Dynamic header for new possible part increment + suggestUrl: String, + }; + + connect() { + this.configureAutocomplete(); + this.watchCategoryChanges(); + } + + templates = { + commonSectionHeader({ title, html }) { + return html` +
    +
    + ${title} +
    +
    +
    + `; + }, + partIncrementHeader({ title, html }) { + return html` +
    +
    + ${title} +
    +
    +
    + `; + }, + list({ html }) { + return html` +
      + `; + }, + item({ suggestion, description, html }) { + return html` +
    • +
      +
      +
      + + + +
      +
      +
      ${suggestion}
      +
      ${description}
      +
      +
      +
      +
    • + `; + }, + }; + + configureAutocomplete() { + const inputField = this.inputTarget; + const commonPrefixes = this.suggestionsValue.commonPrefixes || []; + const prefixesPartIncrement = this.suggestionsValue.prefixesPartIncrement || []; + const commonHeader = this.commonSectionHeaderValue; + const partIncrementHeader = this.partIncrementHeaderValue; + + if (!inputField || (!commonPrefixes.length && !prefixesPartIncrement.length)) return; + + // Check whether the panel should be created at the update + if (this.isPanelInitialized) { + const existingPanel = inputField.parentNode.querySelector(".aa-Panel"); + if (existingPanel) { + // Only remove the panel in the update phase + + existingPanel.remove(); + } + } + + // Create panel + const panel = document.createElement("div"); + panel.classList.add("aa-Panel"); + panel.style.display = "none"; + + // Create panel layout + const panelLayout = document.createElement("div"); + panelLayout.classList.add("aa-PanelLayout", "aa-Panel--scrollable"); + + // Section for common prefixes + if (commonPrefixes.length) { + const commonSection = document.createElement("section"); + commonSection.classList.add("aa-Source"); + + const commonSectionHeader = this.templates.commonSectionHeader({ + title: commonHeader, + html: String.raw, + }); + commonSection.innerHTML += commonSectionHeader; + + const commonList = document.createElement("ul"); + commonList.classList.add("aa-List"); + commonList.setAttribute("role", "listbox"); + + commonPrefixes.forEach((prefix) => { + const itemHTML = this.templates.item({ + suggestion: prefix.title, + description: prefix.description, + html: String.raw, + }); + commonList.innerHTML += itemHTML; + }); + + commonSection.appendChild(commonList); + panelLayout.appendChild(commonSection); + } + + // Section for prefixes part increment + if (prefixesPartIncrement.length) { + const partIncrementSection = document.createElement("section"); + partIncrementSection.classList.add("aa-Source"); + + const partIncrementHeaderHtml = this.templates.partIncrementHeader({ + title: partIncrementHeader, + html: String.raw, + }); + partIncrementSection.innerHTML += partIncrementHeaderHtml; + + const partIncrementList = document.createElement("ul"); + partIncrementList.classList.add("aa-List"); + partIncrementList.setAttribute("role", "listbox"); + + prefixesPartIncrement.forEach((prefix) => { + const itemHTML = this.templates.item({ + suggestion: prefix.title, + description: prefix.description, + html: String.raw, + }); + partIncrementList.innerHTML += itemHTML; + }); + + partIncrementSection.appendChild(partIncrementList); + panelLayout.appendChild(partIncrementSection); + } + + panel.appendChild(panelLayout); + inputField.parentNode.appendChild(panel); + + inputField.addEventListener("focus", () => { + panel.style.display = "block"; + }); + + inputField.addEventListener("blur", () => { + setTimeout(() => { + panel.style.display = "none"; + }, 100); + }); + + // Selection of an item + panelLayout.addEventListener("mousedown", (event) => { + const target = event.target.closest("li"); + + if (target) { + inputField.value = target.dataset.suggestion; + panel.style.display = "none"; + } + }); + + this.isPanelInitialized = true; + }; + + watchCategoryChanges() { + const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]'); + this.previousCategoryId = Number(this.partCategoryIdValue); + + if (categoryField) { + categoryField.addEventListener("change", () => { + const categoryId = Number(categoryField.value); + + // Check whether the category has changed compared to the previous ID + if (categoryId !== this.previousCategoryId) { + this.fetchNewSuggestions(categoryId); + this.previousCategoryId = categoryId; + } + }); + } + } + + fetchNewSuggestions(categoryId) { + const baseUrl = this.suggestUrlValue; + const partId = this.partIdValue; + const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}`; + + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error(`Error when calling up the IPN-suggestions: ${response.status}`); + } + return response.json(); + }) + .then((data) => { + this.suggestionsValue = data; + this.configureAutocomplete(); + }) + .catch((error) => { + console.error("Errors when loading the new IPN-suggestions:", error); + }); + }; +} \ No newline at end of file diff --git a/config/parameters.yaml b/config/parameters.yaml index 0c0c9c81e..6b585542a 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -9,6 +9,8 @@ parameters: # This is used as workaround for places where we can not access the settings directly (like the 2FA application names) partdb.title: '%env(string:settings:customization:instanceName)%' # The title shown inside of Part-DB (e.g. in the navbar and on homepage) partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl'] # The languages that are shown in user drop down menu + partdb.enforce_change_comments_for: '%env(csv:ENFORCE_CHANGE_COMMENTS_FOR)%' # The actions for which a change comment is required (e.g. "part_edit", "part_create", etc.). If this is empty, change comments are not required at all. + partdb.autocomplete_part_digits: '%env(trim:string:AUTOCOMPLETE_PART_DIGITS)%' # The number of digits used for the incremental numbering of parts in the IPN (Internal Part Number) autocomplete system. partdb.default_uri: '%env(string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails @@ -35,6 +37,7 @@ parameters: ###################################################################################################################### partdb.gdpr_compliance: true # If this option is activated, IP addresses are anonymized to be GDPR compliant partdb.users.email_pw_reset: '%env(bool:ALLOW_EMAIL_PW_RESET)%' # Config if users are able, to reset their password by email. By default this enabled, when a mail server is configured. + partdb.users.enforce_unique_ipn: '%env(bool:ENFORCE_UNIQUE_IPN)%' # Config if users are able, to enter an already available IPN. In this case a unique increment is appended to the user input. ###################################################################################################################### # Mail settings diff --git a/config/services.yaml b/config/services.yaml index 659098ff3..cd80230b8 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -263,6 +263,30 @@ services: tags: - { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' } + App\Controller\PartController: + bind: + $autocompletePartDigits: '%partdb.autocomplete_part_digits%' + + App\Controller\TypeaheadController: + bind: + $autocompletePartDigits: '%partdb.autocomplete_part_digits%' + + App\Repository\PartRepository: + arguments: + $translator: '@translator' + tags: ['doctrine.repository_service'] + + App\EventSubscriber\UserSystem\PartUniqueIpnSubscriber: + arguments: + $enforceUniqueIpn: '%partdb.users.enforce_unique_ipn%' + tags: + - { name: doctrine.event_subscriber } + + App\Validator\Constraints\UniquePartIpnValidator: + arguments: + $enforceUniqueIpn: '%partdb.users.enforce_unique_ipn%' + tags: [ 'validator.constraint_validator' ] + # We are needing this service inside a migration, where only the container is injected. So we need to define it as public, to access it from the container. App\Services\UserSystem\PermissionPresetsHelper: public: true diff --git a/docs/configuration.md b/docs/configuration.md index 242164bf5..41fbcd6bd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -116,6 +116,10 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept value should be handled as confidential data and not shared publicly. * `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the part image gallery +* `AUTOCOMPLETE_PART_DIGITS`: Defines the fixed number of digits used as the increment at the end of an IPN (Internal Part Number). + IPN prefixes, maintained within part categories and their hierarchy, form the foundation for suggesting complete IPNs. + These suggestions become accessible during IPN input of a part. The constant specifies the digits used to calculate and assign + unique increments for parts within a category hierarchy, ensuring consistency and uniqueness in IPN generation. ### E-Mail settings (all env only) @@ -128,6 +132,8 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept sent from. * `ALLOW_EMAIL_PW_RESET`: Set this value to true, if you want to allow users to reset their password via an email notification. You have to configure the mail provider first before via the MAILER_DSN setting. +* `ENFORCE_UNIQUE_IPN`: Set this value to false, if you want to allow users to enter a already available IPN for a part entry. + In this case a unique increment is appended to the user input. ### Table related settings diff --git a/migrations/Version20250325073036.php b/migrations/Version20250325073036.php new file mode 100644 index 000000000..8c346b6b2 --- /dev/null +++ b/migrations/Version20250325073036.php @@ -0,0 +1,23 @@ +addSql('ALTER TABLE categories ADD part_ipn_prefix VARCHAR(255) NOT NULL AFTER partname_regex'); + $this->addSql('DROP INDEX UNIQ_6940A7FE3D721C14 ON parts'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE `categories` DROP part_ipn_prefix'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON `parts` (ipn)'); + } +} diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 246dbbb22..ce54f7e41 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -68,12 +68,16 @@ #[Route(path: '/part')] class PartController extends AbstractController { - public function __construct(protected PricedetailHelper $pricedetailHelper, + public function __construct( + protected PricedetailHelper $pricedetailHelper, protected PartPreviewGenerator $partPreviewGenerator, private readonly TranslatorInterface $translator, - private readonly AttachmentSubmitHandler $attachmentSubmitHandler, private readonly EntityManagerInterface $em, - protected EventCommentHelper $commentHelper, private readonly PartInfoSettings $partInfoSettings) - { + private readonly AttachmentSubmitHandler $attachmentSubmitHandler, + private readonly EntityManagerInterface $em, + protected EventCommentHelper $commentHelper, + private readonly PartInfoSettings $partInfoSettings, + private readonly int $autocompletePartDigits + ) { } /** @@ -385,16 +389,18 @@ private function renderPartForm(string $mode, Request $request, Part $data, arra $template = 'parts/edit/update_from_ip.html.twig'; } + $partRepository = $this->em->getRepository(Part::class); + return $this->render($template, [ 'part' => $new_part, + 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, $this->autocompletePartDigits), 'form' => $form, 'merge_old_name' => $merge_infos['tname_before'] ?? null, 'merge_other' => $merge_infos['other_part'] ?? null ]); } - #[Route(path: '/{id}/add_withdraw', name: 'part_add_withdraw', methods: ['POST'])] public function withdrawAddHandler(Part $part, Request $request, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper): Response { diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index ca6ed8639..d4c3f4e7c 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -69,7 +69,8 @@ class TypeaheadController extends AbstractController public function __construct( protected AttachmentURLGenerator $urlGenerator, protected Packages $assets, - protected TranslatorInterface $translator + protected TranslatorInterface $translator, + protected int $autocompletePartDigits, ) { } @@ -271,4 +272,28 @@ public function tags(string $query, TagFinder $finder): JsonResponse return new JsonResponse($data, Response::HTTP_OK, [], true); } + + #[Route(path: '/parts/ipn-suggestions', name: 'ipn_suggestions', methods: ['GET'])] + public function ipnSuggestions( + Request $request, + EntityManagerInterface $entityManager + ): JsonResponse { + $partId = $request->query->get('partId'); + if ($partId === '0' || $partId === 'undefined' || $partId === 'null') { + $partId = null; + } + $categoryId = $request->query->getInt('categoryId'); + + /** @var Part $part */ + $part = $partId !== null ? $entityManager->getRepository(Part::class)->find($partId) : new Part(); + $category = $entityManager->getRepository(Category::class)->find($categoryId); + + $clonedPart = clone $part; + $clonedPart->setCategory($category); + + $partRepository = $entityManager->getRepository(Part::class); + $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $this->autocompletePartDigits); + + return new JsonResponse($ipnSuggestions); + } } diff --git a/src/Entity/Parts/Category.php b/src/Entity/Parts/Category.php index 99ed3c6d0..7d2e0d1ef 100644 --- a/src/Entity/Parts/Category.php +++ b/src/Entity/Parts/Category.php @@ -118,6 +118,13 @@ class Category extends AbstractPartsContainingDBElement #[ORM\Column(type: Types::TEXT)] protected string $partname_regex = ''; + /** + * @var string The prefix for ipn generation for created parts in this category. + */ + #[Groups(['full', 'import', 'category:read', 'category:write'])] + #[ORM\Column(type: Types::STRING, length: 255, nullable: false)] + protected string $part_ipn_prefix = ''; + /** * @var bool Set to true, if the footprints should be disabled for parts this category (not implemented yet). */ @@ -225,6 +232,16 @@ public function setPartnameRegex(string $partname_regex): self return $this; } + public function getPartIpnPrefix(): string + { + return $this->part_ipn_prefix; + } + + public function setPartIpnPrefix(string $part_ipn_prefix): void + { + $this->part_ipn_prefix = $part_ipn_prefix; + } + public function isDisableFootprints(): bool { return $this->disable_footprints; diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index 74bd3c3a4..3530d15df 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -61,7 +61,6 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -75,7 +74,6 @@ * @extends AttachmentContainingDBElement * @template-use ParametersTrait */ -#[UniqueEntity(fields: ['ipn'], message: 'part.ipn.must_be_unique')] #[ORM\Entity(repositoryClass: PartRepository::class)] #[ORM\EntityListeners([TreeCacheInvalidationListener::class])] #[ORM\Table('`parts`')] diff --git a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php index 230ba7b76..5605ef59a 100644 --- a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php @@ -29,6 +29,7 @@ use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints\Length; +use App\Validator\Constraints\UniquePartIpnConstraint; /** * Advanced properties of a part, not related to a more specific group. @@ -62,8 +63,9 @@ trait AdvancedPropertyTrait */ #[Assert\Length(max: 100)] #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] - #[ORM\Column(type: Types::STRING, length: 100, unique: true, nullable: true)] + #[ORM\Column(type: Types::STRING, length: 100, nullable: true)] #[Length(max: 100)] + #[UniquePartIpnConstraint] protected ?string $ipn = null; /** diff --git a/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php new file mode 100644 index 000000000..9cff3166d --- /dev/null +++ b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php @@ -0,0 +1,73 @@ +getObject(); + + if ($entity instanceof Part) { + $this->ensureUniqueIpn($entity); + } + } + + public function preUpdate(LifecycleEventArgs $args): void + { + $entity = $args->getObject(); + + if ($entity instanceof Part) { + $this->ensureUniqueIpn($entity); + } + } + + private function ensureUniqueIpn(Part $part): void + { + if ($part->getIpn() === null || $part->getIpn() === '') { + return; + } + + $existingPart = $this->entityManager + ->getRepository(Part::class) + ->findOneBy(['ipn' => $part->getIpn()]); + + if ($existingPart && $existingPart->getId() !== $part->getId()) { + if ($this->enforceUniqueIpn) { + return; + } + + // Anhang eines Inkrements bis ein einzigartiger Wert gefunden wird + $increment = 1; + $originalIpn = $part->getIpn(); + + while ($this->entityManager + ->getRepository(Part::class) + ->findOneBy(['ipn' => $originalIpn . "_$increment"])) { + $increment++; + } + + $part->setIpn($originalIpn . "_$increment"); + } + } +} \ No newline at end of file diff --git a/src/Form/AdminPages/CategoryAdminForm.php b/src/Form/AdminPages/CategoryAdminForm.php index 44c1dede7..489649ede 100644 --- a/src/Form/AdminPages/CategoryAdminForm.php +++ b/src/Form/AdminPages/CategoryAdminForm.php @@ -84,6 +84,17 @@ protected function additionalFormElements(FormBuilderInterface $builder, array $ 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); + $builder->add('part_ipn_prefix', TextType::class, [ + 'required' => false, + 'empty_data' => '', + 'label' => 'category.edit.part_ipn_prefix', + 'help' => 'category.edit.part_ipn_prefix.help', + 'attr' => [ + 'placeholder' => 'category.edit.part_ipn_prefix.placeholder', + ], + 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), + ]); + $builder->add('default_description', RichTextEditorType::class, [ 'required' => false, 'empty_data' => '', diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index 0bd3d0e3f..06639bf36 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -104,6 +104,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'disable_not_selectable' => true, //Do not require category for new parts, so that the user must select the category by hand and cannot forget it (the requirement is handled by the constraint in the entity) 'required' => !$new_part, + 'attr' => [ + 'data-ipn-suggestion' => 'categoryField', + ] ]) ->add('footprint', StructuralEntityType::class, [ 'class' => Footprint::class, @@ -175,6 +178,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'empty_data' => null, 'label' => 'part.edit.ipn', + 'attr' => [ + 'class' => 'ipn-suggestion-field', + 'data-elements--ipn-suggestion-target' => 'input', + 'autocomplete' => 'off', + ] ]); //Comment section diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index edccd74ba..cdba4f777 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -22,17 +22,31 @@ namespace App\Repository; +use App\Entity\Parts\Category; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; +use Symfony\Contracts\Translation\TranslatorInterface; +use Doctrine\ORM\EntityManagerInterface; /** * @extends NamedDBElementRepository */ class PartRepository extends NamedDBElementRepository { + private TranslatorInterface $translator; + + public function __construct( + EntityManagerInterface $em, + TranslatorInterface $translator + ) { + parent::__construct($em, $em->getClassMetadata(Part::class)); + + $this->translator = $translator; + } + /** * Gets the summed up instock of all parts (only parts without a measurement unit). * @@ -94,4 +108,109 @@ public function autocompleteSearch(string $query, int $max_limits = 50): array return $qb->getQuery()->getResult(); } + + public function autoCompleteIpn(Part $part, int $autocompletePartDigits): array + { + $category = $part->getCategory(); + $ipnSuggestions = ['commonPrefixes' => [], 'prefixesPartIncrement' => []]; + + // Validate the category and ensure it's an instance of Category + if ($category instanceof Category) { + $currentPath = $category->getPartIpnPrefix(); + $directIpnPrefixEmpty = $category->getPartIpnPrefix() === ''; + $currentPath = $currentPath === '' ? 'n.a.' : $currentPath; + + $increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $autocompletePartDigits); + + $ipnSuggestions['commonPrefixes'][] = [ + 'title' => $currentPath . '-', + 'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category') + ]; + + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $currentPath . '-' . $increment, + 'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category.increment') + ]; + + // Process parent categories + $parentCategory = $category->getParent(); + + while ($parentCategory instanceof Category) { + // Prepend the parent category's prefix to the current path + $currentPath = $parentCategory->getPartIpnPrefix() . '-' . $currentPath; + $currentPath = $parentCategory->getPartIpnPrefix() === '' ? 'n.a.-' . $currentPath : $currentPath; + + $ipnSuggestions['commonPrefixes'][] = [ + 'title' => $currentPath . '-', + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment') + ]; + + $increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $autocompletePartDigits); + + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $currentPath . '-' . $increment, + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.hierarchical.increment') + ]; + + // Move to the next parent category + $parentCategory = $parentCategory->getParent(); + } + } elseif ($part->getID() === null) { + $ipnSuggestions['commonPrefixes'][] = [ + 'title' => 'n.a.', + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.not_saved') + ]; + } + + return $ipnSuggestions; + } + + public function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $autocompletePartDigits): string + { + $qb = $this->createQueryBuilder('part'); + + $expectedLength = strlen($currentPath) + 1 + $autocompletePartDigits; // Path + '-' + $autocompletePartDigits digits + + // Fetch all parts in the given category, sorted by their ID in ascending order + $qb->select('part') + ->where('part.ipn LIKE :ipnPattern') + ->andWhere('LENGTH(part.ipn) = :expectedLength') + ->setParameter('ipnPattern', $currentPath . '%') + ->setParameter('expectedLength', $expectedLength) + ->orderBy('part.id', 'ASC'); + + $parts = $qb->getQuery()->getResult(); + + // Collect all used increments in the category + $usedIncrements = []; + foreach ($parts as $part) { + if ($part->getIpn() === null || $part->getIpn() === '') { + continue; + } + + if ($part->getId() === $currentPart->getId()) { + // Extract and return the current part's increment directly + $incrementPart = substr($part->getIpn(), -$autocompletePartDigits); + if (is_numeric($incrementPart)) { + return str_pad((string) $incrementPart, $autocompletePartDigits, '0', STR_PAD_LEFT); + } + } + + // Extract last $autocompletePartDigits digits for possible available part increment + $incrementPart = substr($part->getIpn(), -$autocompletePartDigits); + if (is_numeric($incrementPart)) { + $usedIncrements[] = (int) $incrementPart; + } + + } + + // Generate the next free $autocompletePartDigits-digit increment + $nextIncrement = 1; // Start at the beginning + + while (in_array($nextIncrement, $usedIncrements)) { + $nextIncrement++; + } + + return str_pad((string) $nextIncrement, $autocompletePartDigits, '0', STR_PAD_LEFT); + } } diff --git a/src/Validator/Constraints/UniquePartIpnConstraint.php b/src/Validator/Constraints/UniquePartIpnConstraint.php new file mode 100644 index 000000000..13fd0330f --- /dev/null +++ b/src/Validator/Constraints/UniquePartIpnConstraint.php @@ -0,0 +1,20 @@ +entityManager = $entityManager; + $this->enforceUniqueIpn = $enforceUniqueIpn; + } + + public function validate($value, Constraint $constraint) + { + if (null === $value || '' === $value) { + return; + } + + $repository = $this->entityManager->getRepository(Part::class); + $existingPart = $repository->findOneBy(['ipn' => $value]); + + if ($existingPart) { + if ($this->enforceUniqueIpn) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $value) + ->addViolation(); + } + } + } +} \ No newline at end of file diff --git a/templates/admin/category_admin.html.twig b/templates/admin/category_admin.html.twig index f1fe7663a..82089a283 100644 --- a/templates/admin/category_admin.html.twig +++ b/templates/admin/category_admin.html.twig @@ -33,6 +33,7 @@
      {{ form_row(form.partname_regex) }} {{ form_row(form.partname_hint) }} + {{ form_row(form.part_ipn_prefix) }}
      {{ form_row(form.default_description) }} {{ form_row(form.default_comment) }} diff --git a/templates/parts/edit/_advanced.html.twig b/templates/parts/edit/_advanced.html.twig index 12b546abc..4dd91dd16 100644 --- a/templates/parts/edit/_advanced.html.twig +++ b/templates/parts/edit/_advanced.html.twig @@ -1,5 +1,14 @@ {{ form_row(form.needsReview) }} {{ form_row(form.favorite) }} {{ form_row(form.mass) }} -{{ form_row(form.ipn) }} +
      + {{ form_row(form.ipn) }} +
      {{ form_row(form.partUnit) }} \ No newline at end of file diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index baecebb90..64eda2069 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -1842,6 +1842,54 @@ Související prvky budou přesunuty nahoru. Pokročilé
      + + + part.edit.tab.advanced.ipn.commonSectionHeader + Návrhy bez přírůstku části + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Návrhy s číselnými přírůstky částí + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + IPN předpona přímé kategorie je prázdná, zadejte ji v kategorii „%name%“ + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN prefix přímé kategorie + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN prefix přímé kategorie a specifického přírůstku pro část + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN prefixy s hierarchickým pořadím kategorií rodičovských prefixů + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN prefixy s hierarchickým pořadím kategorií rodičovských prefixů a specifickým přírůstkem pro část + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Nejprve vytvořte součást a přiřaďte ji do kategorie: s dostupnými kategoriemi a jejich vlastními IPN prefixy lze automaticky navrhnout IPN označení pro danou součást + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6959,6 +7007,12 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Filtr názvů + + + category.edit.part_ipn_prefix + Předpona součásti IPN + + obsolete @@ -10296,12 +10350,24 @@ Element 3 např. "/Kondenzátor \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + např. "B12A" + + category.edit.partname_regex.help Regulární výraz kompatibilní s PCRE, kterému musí název dílu odpovídat. + + + category.edit.part_ipn_prefix.help + Předpona navrhovaná při zadávání IPN části. + + entity.select.add_hint diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 07f6c1b37..d943f69fa 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -1850,6 +1850,54 @@ Underelementer vil blive flyttet opad. Advanceret + + + part.edit.tab.advanced.ipn.commonSectionHeader + Forslag uden del-inkrement + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Forslag med numeriske deleforøgelser + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + IPN-præfikset for den direkte kategori er tomt, angiv det i kategorien "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN-præfiks for direkte kategori + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN-præfiks for den direkte kategori og en delspecifik inkrement + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN-præfikser med hierarkisk rækkefølge af overordnede præfikser + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN-præfikser med hierarkisk rækkefølge af overordnede præfikser og en del-specifik inkrement + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Opret først en komponent, og tildel den en kategori: med eksisterende kategorier og deres egne IPN-præfikser kan IPN-betegnelsen for komponenten foreslås automatisk + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6966,6 +7014,12 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Navnefilter + + + category.edit.part_ipn_prefix + IPN-komponentförstavelse + + obsolete @@ -10322,12 +10376,24 @@ Element 3 f.eks. "/Kondensator \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + f.eks. "B12A" + + category.edit.partname_regex.help Et PCRE-kompatibelt regulært udtryk, som delnavnet skal opfylde. + + + category.edit.part_ipn_prefix.help + Et prefix foreslået, når IPN for en del indtastes. + + entity.select.add_hint diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 7e2c84385..d200f5a60 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1841,6 +1841,54 @@ Subelemente werden beim Löschen nach oben verschoben. Erweiterte Optionen + + + part.edit.tab.advanced.ipn.commonSectionHeader + Vorschläge ohne Teil-Inkrement + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Vorschläge mit numerischen Teil-Inkrement + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + IPN-Präfix der direkten Kategorie leer, geben Sie einen Präfix in Kategorie "%name%" an + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN-Präfix der direkten Kategorie + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN-Präfix der direkten Kategorie und eines teilspezifischen Inkrements + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN-Präfixe mit hierarchischer Kategorienreihenfolge der Elternpräfixe + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN-Präfixe mit hierarchischer Kategorienreihenfolge der Elternpräfixe und ein teilsspezifisches Inkrement + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Bitte erstellen Sie zuerst ein Bauteil und weisen Sie dieses einer Kategorie zu: mit vorhandenen Kategorien und derene eigenen IPN-Präfix kann die IPN-Angabe für das jeweilige Teil automatisch vorgeschlagen werden + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6976,6 +7024,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr Namensfilter + + + category.edit.part_ipn_prefix + Bauteil IPN-Präfix + + obsolete @@ -10316,12 +10370,24 @@ Element 1 -> Element 1.2 z.B. "/Kondensator \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + z.B. "B12A" + + category.edit.partname_regex.help Ein PCRE-kompatibler regulärer Ausdruck, den der Bauteilename erfüllen muss. + + + category.edit.part_ipn_prefix.help + Ein Präfix, der bei der IPN-Eingabe eines Bauteils vorgeschlagen wird. + + entity.select.add_hint diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 07143e5b7..1d2ce3cbd 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -2476,5 +2476,71 @@ %name% (Το συνώνυμό σας: %synonym%) + + + category.edit.part_ipn_prefix + Πρόθεμα εξαρτήματος IPN + + + + + category.edit.part_ipn_prefix.placeholder + π.χ. "B12A" + + + + + category.edit.part_ipn_prefix.help + Μια προτεινόμενη πρόθεμα κατά την εισαγωγή του IPN ενός τμήματος. + + + + + part.edit.tab.advanced.ipn.commonSectionHeader + Προτάσεις χωρίς αύξηση μέρους + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Προτάσεις με αριθμητικές αυξήσεις μερών + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Το IPN πρόθεμα της άμεσης κατηγορίας είναι κενό, καθορίστε το στην κατηγορία "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Πρόθεμα IPN για την άμεση κατηγορία + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Πρόθεμα IPN της άμεσης κατηγορίας και μιας ειδικής για μέρος αύξησης + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Προθέματα IPN με ιεραρχική σειρά κατηγοριών των προθέτων γονέων + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Προθέματα IPN με ιεραρχική σειρά κατηγοριών των προθέτων γονέων και συγκεκριμένη αύξηση για το μέρος + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Δημιουργήστε πρώτα ένα εξάρτημα και αντιστοιχίστε το σε μια κατηγορία: με τις υπάρχουσες κατηγορίες και τα δικά τους προθέματα IPN, η ονομασία IPN για το εξάρτημα μπορεί να προταθεί αυτόματα + + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 66292192f..790533408 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -1842,6 +1842,54 @@ Sub elements will be moved upwards. Advanced + + + part.edit.tab.advanced.ipn.commonSectionHeader + Suggestions without part increment + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Suggestions with numeric part increment + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + IPN prefix of direct category empty, specify one in category "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN prefix of direct category + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN prefix of direct category and part-specific increment + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN prefixes with hierarchical category order of parent-prefix(es) + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN prefixes with hierarchical category order of parent-prefix(es) and part-specific increment + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Please create part at first and assign it to a category: with existing categories and their own IPN prefix, the IPN for the part can be suggested automatically + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6977,6 +7025,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Name filter + + + category.edit.part_ipn_prefix + Part IPN Prefix + + obsolete @@ -10317,12 +10371,24 @@ Element 1 -> Element 1.2 e.g "/Capacitor \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + e.g "B12A" + + category.edit.partname_regex.help A PCRE-compatible regular expression, which a part name have to match. + + + category.edit.part_ipn_prefix.help + A prefix suggested when entering the IPN of a part. + + entity.select.add_hint diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index d635333ff..59c947e16 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -1842,6 +1842,54 @@ Subelementos serán desplazados hacia arriba. Avanzado + + + part.edit.tab.advanced.ipn.commonSectionHeader + Sugerencias sin incremento de parte + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Sugerencias con incrementos numéricos de partes + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + El prefijo IPN de la categoría directa está vacío, especifíquelo en la categoría "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Prefijo IPN de la categoría directa + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Prefijo IPN de la categoría directa y un incremento específico de la pieza + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Prefijos IPN con orden jerárquico de categorías de prefijos principales + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Prefijos IPN con orden jerárquico de categorías de prefijos principales y un incremento específico para la parte + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Primero cree un componente y asígnele una categoría: con las categorías existentes y sus propios prefijos IPN, el identificador IPN para el componente puede ser sugerido automáticamente + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6976,6 +7024,12 @@ Subelementos serán desplazados hacia arriba. Filtro de nombre + + + category.edit.part_ipn_prefix + Prefijo de IPN de la pieza + + obsolete @@ -10332,12 +10386,24 @@ Elemento 3 p.ej. "/Condensador \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + p.ej. "B12A" + + category.edit.partname_regex.help Una expresión regular compatible con PCRE, la cual debe coincidir con el nombre de un componente. + + + category.edit.part_ipn_prefix.help + Un prefijo sugerido al ingresar el IPN de una parte. + + entity.select.add_hint diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 34ebcd1f1..248a59614 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -1820,6 +1820,54 @@ Show/Hide sidebar Avancé + + + part.edit.tab.advanced.ipn.commonSectionHeader + Suggestions sans incrément de partie + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Propositions avec incréments numériques de parties + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Le préfixe IPN de la catégorie directe est vide, veuillez le spécifier dans la catégorie "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Préfixe IPN de la catégorie directe + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Préfixe IPN de la catégorie directe et d'un incrément spécifique à la partie + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Préfixes IPN avec un ordre hiérarchique des catégories des préfixes parents + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Préfixes IPN avec un ordre hiérarchique des catégories des préfixes parents et un incrément spécifique à la pièce + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Créez d'abord une pièce et assignez-la à une catégorie : avec les catégories existantes et leurs propres préfixes IPN, l'identifiant IPN pour la pièce peut être proposé automatiquement + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6918,6 +6966,12 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia Filtre de nom + + + category.edit.part_ipn_prefix + Préfixe de pièce IPN + + obsolete @@ -10050,5 +10104,17 @@ exemple de ville %name% (Votre synonyme : %synonym%) + + + category.edit.part_ipn_prefix.placeholder + par ex. "B12A" + + + + + category.edit.part_ipn_prefix.help + Un préfixe suggéré lors de la saisie de l'IPN d'une pièce. + + diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index b018468bb..d6373cae5 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -1842,6 +1842,54 @@ I sub elementi saranno spostati verso l'alto. Avanzate + + + part.edit.tab.advanced.ipn.commonSectionHeader + Suggerimenti senza incremento di parte + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Suggerimenti con incrementi numerici delle parti + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Il prefisso IPN della categoria diretta è vuoto, specificarlo nella categoria "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Prefisso IPN della categoria diretta + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Prefisso IPN della categoria diretta e di un incremento specifico della parte + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Prefissi IPN con ordine gerarchico delle categorie dei prefissi padre + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Prefissi IPN con ordine gerarchico delle categorie dei prefissi padre e un incremento specifico per il pezzo + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Crea prima un componente e assegnagli una categoria: con le categorie esistenti e i loro propri prefissi IPN, l'identificativo IPN per il componente può essere suggerito automaticamente + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6978,6 +7026,12 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi Filtro nome + + + category.edit.part_ipn_prefix + Prefisso parte IPN + + obsolete @@ -10334,12 +10388,24 @@ Element 3 es. "/Condensatore \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + es. "B12A" + + category.edit.partname_regex.help Un'espressione regolare compatibile con PCRE che il nome del componente deve soddisfare. + + + category.edit.part_ipn_prefix.help + Un prefisso suggerito durante l'inserimento dell'IPN di una parte. + + entity.select.add_hint diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 4d1c82edf..90a813df5 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -1820,6 +1820,54 @@ 詳細 + + + part.edit.tab.advanced.ipn.commonSectionHeader + 部品の増加なしの提案。 + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + パーツの数値インクリメントを含む提案 + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + 直接カテゴリの IPN プレフィックスが空です。「%name%」カテゴリで指定してください + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + 直接カテゴリのIPNプレフィックス + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + 直接カテゴリのIPNプレフィックスと部品特有のインクリメント + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + 親プレフィックスの階層カテゴリ順のIPNプレフィックス + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + 親プレフィックスの階層カテゴリ順とパーツ固有の増分のIPNプレフィックス + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + まずはコンポーネントを作成し、それをカテゴリに割り当ててください:既存のカテゴリとそれぞれのIPNプレフィックスを基に、コンポーネントのIPNを自動的に提案できます + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6919,6 +6967,12 @@ 名前のフィルター + + + category.edit.part_ipn_prefix + 部品 IPN 接頭辞 + + obsolete @@ -9763,5 +9817,17 @@ Exampletown %name% (あなたの同義語: %synonym%) + + + category.edit.part_ipn_prefix.placeholder + 例: "B12A" + + + + + category.edit.part_ipn_prefix.help + 部品のIPN入力時に提案される接頭辞。 + + diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 822191818..a57fe6182 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1701,5 +1701,71 @@ %name% (Uw synoniem: %synonym%) + + + category.edit.part_ipn_prefix + IPN-voorvoegsel van onderdeel + + + + + category.edit.part_ipn_prefix.placeholder + bijv. "B12A" + + + + + category.edit.part_ipn_prefix.help + Een voorgesteld voorvoegsel bij het invoeren van de IPN van een onderdeel. + + + + + part.edit.tab.advanced.ipn.commonSectionHeader + Suggesties zonder toename van onderdelen + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Suggesties met numerieke verhogingen van onderdelen + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Het IPN-prefix van de directe categorie is leeg, geef het op in de categorie "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN-prefix van de directe categorie + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN-voorvoegsel van de directe categorie en een onderdeel specifiek increment + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN-prefixen met een hiërarchische volgorde van hoofdcategorieën + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN-prefixen met een hiërarchische volgorde van hoofdcategorieën en een specifieke toename voor het onderdeel + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Maak eerst een component en wijs het toe aan een categorie: met de bestaande categorieën en hun eigen IPN-prefixen kan de IPN voor het component automatisch worden voorgesteld + + diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 6a7e5d591..8be63348b 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -1847,6 +1847,54 @@ Po usunięciu pod elementy zostaną przeniesione na górę. Zaawansowane + + + part.edit.tab.advanced.ipn.commonSectionHeader + Sugestie bez zwiększenia części + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Propozycje z numerycznymi przyrostami części + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Prefiks IPN kategorii bezpośredniej jest pusty, podaj go w kategorii "%name%". + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Prefiks IPN kategorii bezpośredniej + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Prefiks IPN bezpośredniej kategorii i specyficzny dla części przyrost + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Prefiksy IPN z hierarchiczną kolejnością kategorii prefiksów nadrzędnych + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Prefiksy IPN z hierarchiczną kolejnością kategorii prefiksów nadrzędnych i specyficznym przyrostem dla części + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Najpierw utwórz komponent i przypisz go do kategorii: dzięki istniejącym kategoriom i ich własnym prefiksom IPN identyfikator IPN dla komponentu może być proponowany automatycznie + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6981,6 +7029,12 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo Filtr nazwy + + + category.edit.part_ipn_prefix + Prefiks IPN części + + obsolete @@ -10337,12 +10391,24 @@ Element 3 np. "/Kondensator \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + np. "B12A" + + category.edit.partname_regex.help Wyrażenie regularne zgodne z PCRE, do którego musi pasować nazwa komponentu. + + + category.edit.part_ipn_prefix.help + Een voorgesteld voorvoegsel bij het invoeren van de IPN van een onderdeel. + + entity.select.add_hint diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 2add6347a..1744452c4 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -1850,6 +1850,54 @@ Расширенные + + + part.edit.tab.advanced.ipn.commonSectionHeader + Предложения без увеличения частей. + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Предложения с числовыми приращениями частей + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Префикс IPN для прямой категории пуст, укажите его в категории «%name%». + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Префикс IPN для прямой категории + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Префикс IPN прямой категории и специфическое для части приращение + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN-префиксы с иерархическим порядком категорий родительских префиксов + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN-префиксы с иерархическим порядком категорий родительских префиксов и специфическим увеличением для компонента + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Сначала создайте компонент и назначьте ему категорию: на основе существующих категорий и их собственных IPN-префиксов идентификатор IPN для компонента может быть предложен автоматически + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6988,6 +7036,12 @@ Фильтр по имени + + + category.edit.part_ipn_prefix + Префикс IPN детали + + obsolete @@ -10341,12 +10395,24 @@ e.g "/Конденсатор \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + e.g "B12A" + + category.edit.partname_regex.help PCRE-совместимое регулярное выражение которому должно соответствовать имя компонента. + + + category.edit.part_ipn_prefix.help + Предлагаемый префикс при вводе IPN детали. + + entity.select.add_hint diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index cd6b34b2c..2500c8f36 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -1850,6 +1850,54 @@ 高级 + + + part.edit.tab.advanced.ipn.commonSectionHeader + Sugestie bez zwiększenia części + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + 包含部件数值增量的建议 + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + 直接类别的 IPN 前缀为空,请在类别“%name%”中指定。 + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + 直接类别的IPN前缀 + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + 直接类别的IPN前缀和部件特定的增量 + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + 具有父级前缀层级类别顺序的IPN前缀 + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + 具有父级前缀层级类别顺序和组件特定增量的IPN前缀 + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + 请先创建组件并将其分配到类别:基于现有类别及其专属的IPN前缀,可以自动建议组件的IPN + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6985,6 +7033,12 @@ 名称过滤器 + + + category.edit.part_ipn_prefix + 部件 IPN 前缀 + + obsolete @@ -10340,12 +10394,24 @@ Element 3 + + + category.edit.part_ipn_prefix.placeholder + 例如:"B12A" + + category.edit.partname_regex.help 与PCRE兼容的正则表达式,部分名称必须匹配。 + + + category.edit.part_ipn_prefix.help + 输入零件IPN时建议的前缀。 + + entity.select.add_hint From dee090fa846fa80e6204e214ccdbdfca0c761e65 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 23 Apr 2025 13:59:23 +0200 Subject: [PATCH 57/83] =?UTF-8?q?Umstellung=20Migrationen=20bzgl.=20Multi-?= =?UTF-8?q?Plattform-Support.=20Zun=C3=A4chst=20MySQL,=20SQLite=20Statemen?= =?UTF-8?q?ts=20integrieren.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/Version20250325073036.php | 288 ++++++++++++++++++++++++++- src/Entity/Parts/Category.php | 2 +- 2 files changed, 284 insertions(+), 6 deletions(-) diff --git a/migrations/Version20250325073036.php b/migrations/Version20250325073036.php index 8c346b6b2..1d5bdfd2c 100644 --- a/migrations/Version20250325073036.php +++ b/migrations/Version20250325073036.php @@ -4,20 +4,298 @@ namespace DoctrineMigrations; +use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; -use Doctrine\Migrations\AbstractMigration; -final class Version20250325073036 extends AbstractMigration +final class Version20250325073036 extends AbstractMultiPlatformMigration { - public function up(Schema $schema): void + public function getDescription(): string { - $this->addSql('ALTER TABLE categories ADD part_ipn_prefix VARCHAR(255) NOT NULL AFTER partname_regex'); + return 'Add part_ipn_prefix column to categories table and remove unique constraint from parts table'; + } + + public function mySQLUp(Schema $schema): void + { + $this->addSql('ALTER TABLE categories ADD COLUMN part_ipn_prefix VARCHAR(255) NOT NULL DEFAULT \'\''); $this->addSql('DROP INDEX UNIQ_6940A7FE3D721C14 ON parts'); } - public function down(Schema $schema): void + public function mySQLDown(Schema $schema): void { $this->addSql('ALTER TABLE `categories` DROP part_ipn_prefix'); $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON `parts` (ipn)'); } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__categories AS + SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM categories + SQL); + + $this->addSql('DROP TABLE categories'); + + $this->addSql(<<<'SQL' + CREATE TABLE categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + partname_hint CLOB NOT NULL, + partname_regex CLOB NOT NULL, + part_ipn_prefix VARCHAR(255) DEFAULT '' NOT NULL, + disable_footprints BOOLEAN NOT NULL, + disable_manufacturers BOOLEAN NOT NULL, + disable_autodatasheets BOOLEAN NOT NULL, + disable_properties BOOLEAN NOT NULL, + default_description CLOB NOT NULL, + default_comment CLOB NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + alternative_names CLOB DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO categories ( + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + ) SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM __temp__categories + SQL); + + $this->addSql('DROP TABLE __temp__categories'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668727ACA70 ON categories (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668EA7100A1 ON categories (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_name ON categories (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_parent_name ON categories (parent_id, name) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__categories AS + SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM categories + SQL); + + $this->addSql('DROP TABLE categories'); + + $this->addSql(<<<'SQL' + CREATE TABLE categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + partname_hint CLOB NOT NULL, + partname_regex CLOB NOT NULL, + disable_footprints BOOLEAN NOT NULL, + disable_manufacturers BOOLEAN NOT NULL, + disable_autodatasheets BOOLEAN NOT NULL, + disable_properties BOOLEAN NOT NULL, + default_description CLOB NOT NULL, + default_comment CLOB NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + alternative_names CLOB DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO categories ( + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + ) SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM __temp__categories + SQL); + + $this->addSql('DROP TABLE __temp__categories'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668727ACA70 ON categories (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668EA7100A1 ON categories (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_name ON categories (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_parent_name ON categories (parent_id, name) + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + //Not needed + } + + public function postgreSQLDown(Schema $schema): void + { + //Not needed + } } diff --git a/src/Entity/Parts/Category.php b/src/Entity/Parts/Category.php index 7d2e0d1ef..7fca81bc2 100644 --- a/src/Entity/Parts/Category.php +++ b/src/Entity/Parts/Category.php @@ -122,7 +122,7 @@ class Category extends AbstractPartsContainingDBElement * @var string The prefix for ipn generation for created parts in this category. */ #[Groups(['full', 'import', 'category:read', 'category:write'])] - #[ORM\Column(type: Types::STRING, length: 255, nullable: false)] + #[ORM\Column(type: Types::STRING, length: 255, nullable: false, options: ['default' => ''])] protected string $part_ipn_prefix = ''; /** From 22ae4e5e8befd8101154e3a44c1745817040346c Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 17 Jun 2025 11:47:01 +0200 Subject: [PATCH 58/83] Postgre Statements integrieren --- migrations/Version20250325073036.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/migrations/Version20250325073036.php b/migrations/Version20250325073036.php index 1d5bdfd2c..dae163dc1 100644 --- a/migrations/Version20250325073036.php +++ b/migrations/Version20250325073036.php @@ -291,11 +291,21 @@ public function sqLiteDown(Schema $schema): void public function postgreSQLUp(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + ALTER TABLE categories ADD part_ipn_prefix VARCHAR(255) DEFAULT '' NOT NULL + SQL); + $this->addSql(<<<'SQL' + DROP INDEX uniq_6940a7fe3d721c14 + SQL); } public function postgreSQLDown(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + ALTER TABLE "categories" DROP part_ipn_prefix + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX uniq_6940a7fe3d721c14 ON "parts" (ipn) + SQL); } } From 01915f6957b6162720964bee21fb398e0c4dbcb9 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 17 Jun 2025 13:54:50 +0200 Subject: [PATCH 59/83] SQL-Formatierung in Migration verbessern --- migrations/Version20250325073036.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/migrations/Version20250325073036.php b/migrations/Version20250325073036.php index dae163dc1..a9d3eaaad 100644 --- a/migrations/Version20250325073036.php +++ b/migrations/Version20250325073036.php @@ -16,14 +16,22 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { - $this->addSql('ALTER TABLE categories ADD COLUMN part_ipn_prefix VARCHAR(255) NOT NULL DEFAULT \'\''); - $this->addSql('DROP INDEX UNIQ_6940A7FE3D721C14 ON parts'); + $this->addSql(<<<'SQL' + ALTER TABLE categories ADD COLUMN part_ipn_prefix VARCHAR(255) NOT NULL DEFAULT '' + SQL); + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_6940A7FE3D721C14 ON parts + SQL); } public function mySQLDown(Schema $schema): void { - $this->addSql('ALTER TABLE `categories` DROP part_ipn_prefix'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON `parts` (ipn)'); + $this->addSql(<<<'SQL' + ALTER TABLE categories DROP part_ipn_prefixSQL + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn) + SQL); } public function sqLiteUp(Schema $schema): void From 13840c4dadbbef2889f54d247d7bbd9b76d3c58b Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 9 Jul 2025 09:45:43 +0200 Subject: [PATCH 60/83] Erweitere IPN-Suggest um Bauteilbeschreibung. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Implementierung berücksichtigt nun zusätzlich die Bauteilbeschreibung zu maximal 150 Zeichen Länge für die Generierung von IPN-Vorschlägen und Inkrementen. --- .../elements/ckeditor_controller.js | 9 ++ .../elements/ipn_suggestion_controller.js | 92 ++++++++---- src/Controller/PartController.php | 2 +- src/Controller/TypeaheadController.php | 3 +- src/Form/Part/PartBaseType.php | 1 + src/Repository/PartRepository.php | 132 +++++++++++++++++- templates/parts/edit/_advanced.html.twig | 1 + translations/messages.cs.xlf | 12 ++ translations/messages.da.xlf | 12 ++ translations/messages.de.xlf | 12 ++ translations/messages.el.xlf | 12 ++ translations/messages.en.xlf | 12 ++ translations/messages.es.xlf | 12 ++ translations/messages.fr.xlf | 12 ++ translations/messages.it.xlf | 12 ++ translations/messages.ja.xlf | 12 ++ translations/messages.nl.xlf | 12 ++ translations/messages.pl.xlf | 12 ++ translations/messages.ru.xlf | 12 ++ translations/messages.zh.xlf | 12 ++ 20 files changed, 361 insertions(+), 35 deletions(-) diff --git a/assets/controllers/elements/ckeditor_controller.js b/assets/controllers/elements/ckeditor_controller.js index 62a48b151..7f55dd5ca 100644 --- a/assets/controllers/elements/ckeditor_controller.js +++ b/assets/controllers/elements/ckeditor_controller.js @@ -78,6 +78,15 @@ export default class extends Controller { editor_div.classList.add(...new_classes.split(",")); } + // Automatic synchronization of source input + editor.model.document.on("change:data", () => { + editor.updateSourceElement(); + + // Dispatch the input event for further treatment + const event = new Event("input"); + this.element.dispatchEvent(event); + }); + //This return is important! Otherwise we get mysterious errors in the console //See: https://github.com/ckeditor/ckeditor5/issues/5897#issuecomment-628471302 return editor; diff --git a/assets/controllers/elements/ipn_suggestion_controller.js b/assets/controllers/elements/ipn_suggestion_controller.js index 088c07b33..e7289a91e 100644 --- a/assets/controllers/elements/ipn_suggestion_controller.js +++ b/assets/controllers/elements/ipn_suggestion_controller.js @@ -6,6 +6,7 @@ export default class extends Controller { static values = { partId: Number, partCategoryId: Number, + partDescription: String, suggestions: Object, commonSectionHeader: String, // Dynamic header for common Prefixes partIncrementHeader: String, // Dynamic header for new possible part increment @@ -15,6 +16,7 @@ export default class extends Controller { connect() { this.configureAutocomplete(); this.watchCategoryChanges(); + this.watchDescriptionChanges(); } templates = { @@ -92,34 +94,6 @@ export default class extends Controller { const panelLayout = document.createElement("div"); panelLayout.classList.add("aa-PanelLayout", "aa-Panel--scrollable"); - // Section for common prefixes - if (commonPrefixes.length) { - const commonSection = document.createElement("section"); - commonSection.classList.add("aa-Source"); - - const commonSectionHeader = this.templates.commonSectionHeader({ - title: commonHeader, - html: String.raw, - }); - commonSection.innerHTML += commonSectionHeader; - - const commonList = document.createElement("ul"); - commonList.classList.add("aa-List"); - commonList.setAttribute("role", "listbox"); - - commonPrefixes.forEach((prefix) => { - const itemHTML = this.templates.item({ - suggestion: prefix.title, - description: prefix.description, - html: String.raw, - }); - commonList.innerHTML += itemHTML; - }); - - commonSection.appendChild(commonList); - panelLayout.appendChild(commonSection); - } - // Section for prefixes part increment if (prefixesPartIncrement.length) { const partIncrementSection = document.createElement("section"); @@ -148,6 +122,34 @@ export default class extends Controller { panelLayout.appendChild(partIncrementSection); } + // Section for common prefixes + if (commonPrefixes.length) { + const commonSection = document.createElement("section"); + commonSection.classList.add("aa-Source"); + + const commonSectionHeader = this.templates.commonSectionHeader({ + title: commonHeader, + html: String.raw, + }); + commonSection.innerHTML += commonSectionHeader; + + const commonList = document.createElement("ul"); + commonList.classList.add("aa-List"); + commonList.setAttribute("role", "listbox"); + + commonPrefixes.forEach((prefix) => { + const itemHTML = this.templates.item({ + suggestion: prefix.title, + description: prefix.description, + html: String.raw, + }); + commonList.innerHTML += itemHTML; + }); + + commonSection.appendChild(commonList); + panelLayout.appendChild(commonSection); + } + panel.appendChild(panelLayout); inputField.parentNode.appendChild(panel); @@ -176,25 +178,48 @@ export default class extends Controller { watchCategoryChanges() { const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]'); + const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]'); this.previousCategoryId = Number(this.partCategoryIdValue); if (categoryField) { categoryField.addEventListener("change", () => { const categoryId = Number(categoryField.value); + const description = String(descriptionField.value); // Check whether the category has changed compared to the previous ID if (categoryId !== this.previousCategoryId) { - this.fetchNewSuggestions(categoryId); + this.fetchNewSuggestions(categoryId, description); this.previousCategoryId = categoryId; } }); } } - fetchNewSuggestions(categoryId) { + watchDescriptionChanges() { + const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]'); + const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]'); + this.previousDescription = String(this.partDescriptionValue); + + if (descriptionField) { + descriptionField.addEventListener("input", () => { + const categoryId = Number(categoryField.value); + const description = String(descriptionField.value); + + // Check whether the description has changed compared to the previous one + if (description !== this.previousDescription) { + this.fetchNewSuggestions(categoryId, description); + this.previousDescription = description; + } + }); + } + } + + fetchNewSuggestions(categoryId, description) { const baseUrl = this.suggestUrlValue; const partId = this.partIdValue; - const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}`; + const truncatedDescription = description.length > 150 ? description.substring(0, 150) : description; + const encodedDescription = this.base64EncodeUtf8(truncatedDescription); + const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}&description=${encodedDescription}`; fetch(url, { method: "GET", @@ -217,4 +242,9 @@ export default class extends Controller { console.error("Errors when loading the new IPN-suggestions:", error); }); }; + + base64EncodeUtf8(text) { + const utf8Bytes = new TextEncoder().encode(text); + return btoa(String.fromCharCode(...utf8Bytes)); + }; } \ No newline at end of file diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index ce54f7e41..ba5c2054e 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -394,7 +394,7 @@ private function renderPartForm(string $mode, Request $request, Part $data, arra return $this->render($template, [ 'part' => $new_part, - 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, $this->autocompletePartDigits), + 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, base64_encode($data->getDescription()), $this->autocompletePartDigits), 'form' => $form, 'merge_old_name' => $merge_infos['tname_before'] ?? null, 'merge_other' => $merge_infos['other_part'] ?? null diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index d4c3f4e7c..fd08128ca 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -283,6 +283,7 @@ public function ipnSuggestions( $partId = null; } $categoryId = $request->query->getInt('categoryId'); + $description = $request->query->getString('description'); /** @var Part $part */ $part = $partId !== null ? $entityManager->getRepository(Part::class)->find($partId) : new Part(); @@ -292,7 +293,7 @@ public function ipnSuggestions( $clonedPart->setCategory($category); $partRepository = $entityManager->getRepository(Part::class); - $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $this->autocompletePartDigits); + $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $description, $this->autocompletePartDigits); return new JsonResponse($ipnSuggestions); } diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index 06639bf36..c493f12bb 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -86,6 +86,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'attr' => [ 'placeholder' => 'part.edit.description.placeholder', 'rows' => 2, + 'data-ipn-suggestion' => 'descriptionField', ], ]) ->add('minAmount', SIUnitType::class, [ diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index cdba4f777..693615533 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -109,10 +109,30 @@ public function autocompleteSearch(string $query, int $max_limits = 50): array return $qb->getQuery()->getResult(); } - public function autoCompleteIpn(Part $part, int $autocompletePartDigits): array + /** + * Provides IPN (Internal Part Number) suggestions for a given part based on its category, description, + * and configured autocomplete digit length. + * + * This function generates suggestions for common prefixes and incremented prefixes based on + * the part's current category and its hierarchy. If the part is unsaved, a default "n.a." prefix is returned. + * + * @param Part $part The part for which autocomplete suggestions are generated. + * @param string $description Base64-encoded description to assist in generating suggestions. + * @param int $autocompletePartDigits The number of digits used in autocomplete increments. + * + * @return array An associative array containing the following keys: + * - 'commonPrefixes': List of common prefixes found for the part. + * - 'prefixesPartIncrement': Increments for the generated prefixes, including hierarchical prefixes. + */ + public function autoCompleteIpn(Part $part, string $description, int $autocompletePartDigits): array { $category = $part->getCategory(); $ipnSuggestions = ['commonPrefixes' => [], 'prefixesPartIncrement' => []]; + $description = base64_decode($description); + + if (strlen($description) > 150) { + $description = substr($description, 0, 150); + } // Validate the category and ensure it's an instance of Category if ($category instanceof Category) { @@ -127,6 +147,22 @@ public function autoCompleteIpn(Part $part, int $autocompletePartDigits): array 'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category') ]; + $suggestionByDescription = $this->getIpnSuggestByDescription($description); + + if ($suggestionByDescription !== null && $suggestionByDescription !== $part->getIpn() && $part->getIpn() !== null && $part->getIpn() !== '') { + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $part->getIpn(), + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.description.current-increment') + ]; + } + + if ($suggestionByDescription !== null) { + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $suggestionByDescription, + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.description.increment') + ]; + } + $ipnSuggestions['prefixesPartIncrement'][] = [ 'title' => $currentPath . '-' . $increment, 'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category.increment') @@ -165,7 +201,62 @@ public function autoCompleteIpn(Part $part, int $autocompletePartDigits): array return $ipnSuggestions; } - public function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $autocompletePartDigits): string + /** + * Suggests the next IPN (Internal Part Number) based on the provided part description. + * + * Searches for parts with similar descriptions and retrieves their existing IPNs to calculate the next suggestion. + * Returns null if the description is empty or no suggestion can be generated. + * + * @param string $description The part description to search for. + * + * @return string|null The suggested IPN, or null if no suggestion is possible. + * + * @throws NonUniqueResultException + */ + public function getIpnSuggestByDescription(string $description): ?string + { + if ($description === '') { + return null; + } + + $qb = $this->createQueryBuilder('part'); + + $qb->select('part') + ->where('part.description LIKE :descriptionPattern') + ->setParameter('descriptionPattern', $description.'%') + ->orderBy('part.id', 'ASC'); + + $partsBySameDescription = $qb->getQuery()->getResult(); + $givenIpnsWithSameDescription = []; + + foreach ($partsBySameDescription as $part) { + if ($part->getIpn() === null || $part->getIpn() === '') { + continue; + } + + $givenIpnsWithSameDescription[] = $part->getIpn(); + } + + return $this->getNextIpnSuggestion($givenIpnsWithSameDescription); + } + + /** + * Generates the next possible increment for a part within a given category, while ensuring uniqueness. + * + * This method calculates the next available increment for a part's identifier (`ipn`) based on the current path + * and the number of digits specified for the autocomplete feature. It ensures that the generated identifier + * aligns with the expected length and does not conflict with already existing identifiers in the same category. + * + * @param string $currentPath The base path or prefix for the part's identifier. + * @param Part $currentPart The part entity for which the increment is being generated. + * @param int $autocompletePartDigits The number of digits reserved for the increment. + * + * @return string|null The next possible increment as a zero-padded string, or null if it cannot be generated. + * + * @throws NonUniqueResultException If the query returns non-unique results. + * @throws NoResultException If the query fails to return a result. + */ + private function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $autocompletePartDigits): ?string { $qb = $this->createQueryBuilder('part'); @@ -213,4 +304,41 @@ public function generateNextPossiblePartIncrement(string $currentPath, Part $cur return str_pad((string) $nextIncrement, $autocompletePartDigits, '0', STR_PAD_LEFT); } + + /** + * Generates the next IPN suggestion based on the maximum numeric suffix found in the given IPNs. + * + * The new IPN is constructed using the base format of the first provided IPN, + * incremented by the next free numeric suffix. If no base IPNs are found, + * returns null. + * + * @param array $givenIpns List of IPNs to analyze. + * + * @return string|null The next suggested IPN, or null if no base IPNs can be derived. + */ + private function getNextIpnSuggestion(array $givenIpns): ?string { + $maxSuffix = 0; + + foreach ($givenIpns as $ipn) { + // Check whether the IPN contains a suffix "_ " + if (preg_match('/_(\d+)$/', $ipn, $matches)) { + $suffix = (int)$matches[1]; + if ($suffix > $maxSuffix) { + $maxSuffix = $suffix; // Höchste Nummer speichern + } + } + } + + // Find the basic format (the IPN without suffix) from the first IPN + $baseIpn = $givenIpns[0] ?? ''; + $baseIpn = preg_replace('/_\d+$/', '', $baseIpn); // Entferne vorhandene "_" + + if ($baseIpn === '') { + return null; + } + + // Generate next free possible IPN + return $baseIpn . '_' . ($maxSuffix + 1); + } + } diff --git a/templates/parts/edit/_advanced.html.twig b/templates/parts/edit/_advanced.html.twig index 4dd91dd16..de31786c2 100644 --- a/templates/parts/edit/_advanced.html.twig +++ b/templates/parts/edit/_advanced.html.twig @@ -4,6 +4,7 @@
      Návrhy s číselnými přírůstky částí + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktuální specifikace IPN pro součást + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Další možná specifikace IPN na základě identického popisu součásti + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index d943f69fa..d86eab149 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -1862,6 +1862,18 @@ Underelementer vil blive flyttet opad. Forslag med numeriske deleforøgelser + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktuel IPN-specifikation for delen + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Næste mulige IPN-specifikation baseret på en identisk delebeskrivelse + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index d200f5a60..c24c74fe3 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1853,6 +1853,18 @@ Subelemente werden beim Löschen nach oben verschoben. Vorschläge mit numerischen Teil-Inkrement + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktuelle IPN-Angabe des Bauteils + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Nächstmögliche IPN-Angabe auf Basis der identischen Bauteil-Beschreibung + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 1d2ce3cbd..f2126c5e3 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -2506,6 +2506,18 @@ Προτάσεις με αριθμητικές αυξήσεις μερών + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Τρέχουσα προδιαγραφή IPN του εξαρτήματος + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Επόμενη δυνατή προδιαγραφή IPN βάσει της ίδιας περιγραφής εξαρτήματος + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 790533408..aa140bb55 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -1854,6 +1854,18 @@ Sub elements will be moved upwards. Suggestions with numeric part increment + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Current IPN specification of the part + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Next possible IPN specification based on an identical part description + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 59c947e16..441fb6fc0 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -1854,6 +1854,18 @@ Subelementos serán desplazados hacia arriba. Sugerencias con incrementos numéricos de partes + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Especificación actual de IPN de la pieza + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Siguiente especificación de IPN posible basada en una descripción idéntica de la pieza + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 248a59614..0ed61ac5a 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -1832,6 +1832,18 @@ Show/Hide sidebar Propositions avec incréments numériques de parties + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Spécification IPN actuelle pour la pièce + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Prochaine spécification IPN possible basée sur une description identique de la pièce + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index d6373cae5..4b749523d 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -1854,6 +1854,18 @@ I sub elementi saranno spostati verso l'alto. Suggerimenti con incrementi numerici delle parti + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Specifica IPN attuale per il pezzo + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Prossima specifica IPN possibile basata su una descrizione identica del pezzo + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 90a813df5..abc01949e 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -1832,6 +1832,18 @@ パーツの数値インクリメントを含む提案 + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + 部品の現在のIPN仕様 + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + 同じ部品説明に基づく次の可能なIPN仕様 + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index a57fe6182..14242c3aa 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -1731,6 +1731,18 @@ Suggesties met numerieke verhogingen van onderdelen + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Huidige IPN-specificatie voor het onderdeel + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Volgende mogelijke IPN-specificatie op basis van een identieke onderdeelbeschrijving + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 8be63348b..f56858e3a 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -1859,6 +1859,18 @@ Po usunięciu pod elementy zostaną przeniesione na górę. Propozycje z numerycznymi przyrostami części + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktualna specyfikacja IPN dla części + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Następna możliwa specyfikacja IPN na podstawie identycznego opisu części + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 1744452c4..45d743baf 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -1862,6 +1862,18 @@ Предложения с числовыми приращениями частей + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Текущая спецификация IPN для детали + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Следующая возможная спецификация IPN на основе идентичного описания детали + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 2500c8f36..edb4ec59e 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -1862,6 +1862,18 @@ 包含部件数值增量的建议 + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + 部件的当前IPN规格 + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + 基于相同部件描述的下一个可能的IPN规格 + + part.edit.tab.advanced.ipn.prefix_empty.direct_category From 0c6bbef586eb3c435a48fb302f4990fff6157cd2 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 24 Mar 2025 15:15:15 +0100 Subject: [PATCH 61/83] =?UTF-8?q?Benutzerdefinierten=20Bauteilstatus=20ein?= =?UTF-8?q?f=C3=BChren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/permissions.yaml | 6 +- docs/configuration.md | 2 +- migrations/Version20250321075747.php | 23 ++++ migrations/Version20250321141740.php | 25 ++++ .../Migrations/ImportPartKeeprCommand.php | 5 + .../AdminPages/BaseAdminController.php | 18 +-- src/DataTables/Filters/PartFilter.php | 3 + src/DataTables/PartsDataTable.php | 20 +++ src/Entity/Attachments/Attachment.php | 8 +- .../Attachments/PartCustomStateAttachment.php | 45 +++++++ src/Entity/Base/AbstractDBElement.php | 38 +++++- .../LogSystem/CollectionElementDeleted.php | 5 + src/Entity/LogSystem/LogTargetType.php | 3 + src/Entity/Parameters/AbstractParameter.php | 7 +- .../Parameters/PartCustomStateParameter.php | 65 ++++++++++ src/Entity/Parts/Part.php | 2 +- src/Entity/Parts/PartCustomState.php | 121 ++++++++++++++++++ .../PartTraits/AdvancedPropertyTrait.php | 28 +++- .../AdminPages/PartCustomStateAdminForm.php | 27 ++++ src/Form/Filters/PartFilterType.php | 6 + src/Form/Part/PartBaseType.php | 7 + .../Parts/PartCustomStateRepository.php | 48 +++++++ src/Security/Voter/AttachmentVoter.php | 3 + src/Security/Voter/ParameterVoter.php | 3 + src/Security/Voter/StructureVoter.php | 2 + .../Attachments/PartPreviewGenerator.php | 8 ++ src/Services/EDA/KiCadHelper.php | 4 + src/Services/ElementTypeNameGenerator.php | 2 + .../EntityMergers/Mergers/PartMerger.php | 1 + src/Services/EntityURLGenerator.php | 7 + .../PKDatastructureImporter.php | 21 +++ .../PartKeeprImporter/PKPartImporter.php | 2 + .../LabelSystem/SandboxedTwigFactory.php | 2 +- src/Services/Trees/ToolsTreeBuilder.php | 7 + .../UserSystem/PermissionPresetsHelper.php | 1 + src/Twig/EntityExtension.php | 2 + .../admin/part_custom_state_admin.html.twig | 14 ++ templates/parts/edit/_advanced.html.twig | 3 +- templates/parts/lists/_filter.html.twig | 1 + .../PartCustomStateControllerTest.php | 35 +++++ tests/Entity/Attachments/AttachmentTest.php | 3 + .../EntityMergers/Mergers/PartMergerTest.php | 6 +- tests/assets/partkeepr_import_test.xml | 2 + translations/messages.cs.xlf | 54 ++++++++ translations/messages.da.xlf | 54 ++++++++ translations/messages.de.xlf | 54 ++++++++ translations/messages.el.xlf | 54 ++++++++ translations/messages.en.xlf | 54 ++++++++ translations/messages.es.xlf | 54 ++++++++ translations/messages.fr.xlf | 54 ++++++++ translations/messages.it.xlf | 54 ++++++++ translations/messages.ja.xlf | 54 ++++++++ translations/messages.nl.xlf | 54 ++++++++ translations/messages.pl.xlf | 54 ++++++++ translations/messages.ru.xlf | 54 ++++++++ translations/messages.zh.xlf | 54 ++++++++ 56 files changed, 1312 insertions(+), 31 deletions(-) create mode 100644 migrations/Version20250321075747.php create mode 100644 migrations/Version20250321141740.php create mode 100644 src/Entity/Attachments/PartCustomStateAttachment.php create mode 100644 src/Entity/Parameters/PartCustomStateParameter.php create mode 100644 src/Entity/Parts/PartCustomState.php create mode 100644 src/Form/AdminPages/PartCustomStateAdminForm.php create mode 100644 src/Repository/Parts/PartCustomStateRepository.php create mode 100644 templates/admin/part_custom_state_admin.html.twig create mode 100644 tests/Controller/AdminPages/PartCustomStateControllerTest.php diff --git a/config/permissions.yaml b/config/permissions.yaml index 8709fdb77..2423177da 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -24,7 +24,7 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co label: "perm.read" # If a part can be read by a user, he can also see all the datastructures (except devices) alsoSet: ['storelocations.read', 'footprints.read', 'categories.read', 'suppliers.read', 'manufacturers.read', - 'currencies.read', 'attachment_types.read', 'measurement_units.read'] + 'currencies.read', 'attachment_types.read', 'measurement_units.read', 'part_custom_states.read'] apiTokenRole: ROLE_API_READ_ONLY edit: label: "perm.edit" @@ -137,6 +137,10 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co <<: *PART_CONTAINING label: "perm.measurement_units" + part_custom_states: + <<: *PART_CONTAINING + label: "perm.part_custom_states" + tools: label: "perm.part.tools" operations: diff --git a/docs/configuration.md b/docs/configuration.md index 41fbcd6bd..ea02b227f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -142,7 +142,7 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept * `TABLE_PARTS_DEFAULT_COLUMNS`: The columns in parts tables, which are visible by default (when loading table for first time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns - are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`. + are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `partCustomState`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`. * `TABLE_ASSEMBLIES_DEFAULT_COLUMNS`: The columns in assemblies tables, which are visible by default (when loading table for first time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns are: `name`, `id`, `ipn`, `description`, `referencedAssemblies`, `edit`, `addedDate`, `lastModified`. diff --git a/migrations/Version20250321075747.php b/migrations/Version20250321075747.php new file mode 100644 index 000000000..45b35397e --- /dev/null +++ b/migrations/Version20250321075747.php @@ -0,0 +1,23 @@ +addSql('CREATE TABLE `part_custom_states` (id INT AUTO_INCREMENT NOT NULL, parent_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_F552745D727ACA70 (parent_id), INDEX part_custom_state_name (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE `part_custom_states` ADD CONSTRAINT FK_F552745D727ACA70 FOREIGN KEY (parent_id) REFERENCES `part_custom_states` (id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE `part_custom_states` DROP FOREIGN KEY FK_F552745D727ACA70'); + $this->addSql('DROP TABLE `part_custom_states`'); + } +} diff --git a/migrations/Version20250321141740.php b/migrations/Version20250321141740.php new file mode 100644 index 000000000..cd1aefdc0 --- /dev/null +++ b/migrations/Version20250321141740.php @@ -0,0 +1,25 @@ +addSql('ALTER TABLE parts ADD id_part_custom_state INT DEFAULT NULL AFTER id_part_unit'); + $this->addSql('ALTER TABLE parts ADD CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES `part_custom_states` (id)'); + $this->addSql('CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE `parts` DROP FOREIGN KEY FK_6940A7FEA3ED1215'); + $this->addSql('DROP INDEX IDX_6940A7FEA3ED1215 ON `parts`'); + $this->addSql('ALTER TABLE `parts` DROP id_part_custom_state'); + } +} diff --git a/src/Command/Migrations/ImportPartKeeprCommand.php b/src/Command/Migrations/ImportPartKeeprCommand.php index aee71afe7..429f018d5 100644 --- a/src/Command/Migrations/ImportPartKeeprCommand.php +++ b/src/Command/Migrations/ImportPartKeeprCommand.php @@ -121,6 +121,11 @@ private function doImport(SymfonyStyle $io, array $data): void $count = $this->datastructureImporter->importPartUnits($data); $io->success('Imported '.$count.' measurement units.'); + //Import the custom states + $io->info('Importing custom states...'); + $count = $this->datastructureImporter->importPartCustomStates($data); + $io->success('Imported '.$count.' custom states.'); + //Import manufacturers $io->info('Importing manufacturers...'); $count = $this->datastructureImporter->importManufacturers($data); diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index 039751ea6..ddf19088c 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -233,13 +233,6 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit $repo = $this->entityManager->getRepository($this->entity_class); - $showParameters = true; - if ($this instanceof AssemblyAdminController) { - //currently not needed for assemblies - - $showParameters = false; - } - return $this->render($this->twig_template, [ 'entity' => $entity, 'form' => $form, @@ -249,7 +242,7 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit 'timeTravel' => $timeTravel_timestamp, 'repo' => $repo, 'partsContainingElement' => $repo instanceof PartsContainingRepositoryInterface, - 'showParameters' => $showParameters, + 'showParameters' => !($this instanceof PartCustomStateController), ]); } @@ -403,20 +396,13 @@ protected function _new(Request $request, EntityManagerInterface $em, EntityImpo } } - $showParameters = true; - if ($this instanceof AssemblyAdminController) { - //currently not needed for assemblies - - $showParameters = false; - } - return $this->render($this->twig_template, [ 'entity' => $new_entity, 'form' => $form, 'import_form' => $import_form, 'mass_creation_form' => $mass_creation_form, 'route_base' => $this->route_base, - 'showParameters' => $showParameters, + 'showParameters' => !($this instanceof PartCustomStateController), ]); } diff --git a/src/DataTables/Filters/PartFilter.php b/src/DataTables/Filters/PartFilter.php index 8dcbd6b34..5a4706e33 100644 --- a/src/DataTables/Filters/PartFilter.php +++ b/src/DataTables/Filters/PartFilter.php @@ -38,6 +38,7 @@ use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\PartLot; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; @@ -83,6 +84,7 @@ class PartFilter implements FilterInterface public readonly EntityConstraint $lotOwner; public readonly EntityConstraint $measurementUnit; + public readonly EntityConstraint $partCustomState; public readonly TextConstraint $manufacturer_product_url; public readonly TextConstraint $manufacturer_product_number; public readonly IntConstraint $attachmentsCount; @@ -117,6 +119,7 @@ public function __construct(NodesListBuilder $nodesListBuilder) $this->favorite = new BooleanConstraint('part.favorite'); $this->needsReview = new BooleanConstraint('part.needs_review'); $this->measurementUnit = new EntityConstraint($nodesListBuilder, MeasurementUnit::class, 'part.partUnit'); + $this->partCustomState = new EntityConstraint($nodesListBuilder, PartCustomState::class, 'part.partCustomState'); $this->mass = new NumberConstraint('part.mass'); $this->dbId = new IntConstraint('part.id'); $this->ipn = new TextConstraint('part.ipn'); diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index b98c97629..e628d4f0a 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -173,6 +173,19 @@ public function configure(DataTable $dataTable, array $options): void return $tmp; } ]) + ->add('partCustomState', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.partCustomState'), + 'orderField' => 'NATSORT(_partCustomState.name)', + 'render' => function($value, Part $context): string { + $partCustomState = $context->getPartCustomState(); + + if ($partCustomState === null) { + return ''; + } + + return htmlspecialchars($partCustomState->getName()); + } + ]) ->add('addedDate', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.addedDate'), ]) @@ -336,6 +349,7 @@ private function getDetailQuery(QueryBuilder $builder, array $filter_results): v ->addSelect('footprint') ->addSelect('manufacturer') ->addSelect('partUnit') + ->addSelect('partCustomState') ->addSelect('master_picture_attachment') ->addSelect('footprint_attachment') ->addSelect('partLots') @@ -354,6 +368,7 @@ private function getDetailQuery(QueryBuilder $builder, array $filter_results): v ->leftJoin('orderdetails.supplier', 'suppliers') ->leftJoin('part.attachments', 'attachments') ->leftJoin('part.partUnit', 'partUnit') + ->leftJoin('part.partCustomState', 'partCustomState') ->leftJoin('part.parameters', 'parameters') ->where('part.id IN (:ids)') ->setParameter('ids', $ids) @@ -371,6 +386,7 @@ private function getDetailQuery(QueryBuilder $builder, array $filter_results): v ->addGroupBy('suppliers') ->addGroupBy('attachments') ->addGroupBy('partUnit') + ->addGroupBy('partCustomState') ->addGroupBy('parameters'); //Get the results in the same order as the IDs were passed @@ -442,6 +458,10 @@ private function addJoins(QueryBuilder $builder): QueryBuilder $builder->leftJoin('part.partUnit', '_partUnit'); $builder->addGroupBy('_partUnit'); } + if (str_contains($dql, '_partCustomState')) { + $builder->leftJoin('part.partCustomState', '_partCustomState'); + $builder->addGroupBy('_partCustomState'); + } if (str_contains($dql, '_parameters')) { $builder->leftJoin('part.parameters', '_parameters'); //Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1 diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index 808c60623..70ffbaa01 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -97,7 +97,7 @@ #[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)] abstract class Attachment extends AbstractNamedDBElement { - private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class, 'Assembly' => AssemblyAttachment::class, + private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'PartCustomState' => PartCustomStateAttachment::class, 'Device' => ProjectAttachment::class, 'Assembly' => AssemblyAttachment::class, 'AttachmentType' => AttachmentTypeAttachment::class, 'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class, 'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class, @@ -107,7 +107,7 @@ abstract class Attachment extends AbstractNamedDBElement /* * The discriminator map used for API platform. The key should be the same as the api platform short type (the @type JSONLD field). */ - private const API_DISCRIMINATOR_MAP = ["Part" => PartAttachment::class, "Project" => ProjectAttachment::class, "Assembly" => AssemblyAttachment::class, + private const API_DISCRIMINATOR_MAP = ["Part" => PartAttachment::class, "PartCustomState" => PartCustomStateAttachment::class, "Project" => ProjectAttachment::class, "Assembly" => AssemblyAttachment::class, "AttachmentType" => AttachmentTypeAttachment::class, "Category" => CategoryAttachment::class, "Footprint" => FootprintAttachment::class, "Manufacturer" => ManufacturerAttachment::class, "Currency" => CurrencyAttachment::class, "Group" => GroupAttachment::class, "MeasurementUnit" => MeasurementUnitAttachment::class, @@ -551,8 +551,8 @@ public function setAttachmentType(AttachmentType $attachement_type): self */ #[Groups(['attachment:write'])] #[SerializedName('url')] - #[ApiProperty(description: 'Set the path of the attachment here. - Provide either an external URL, a path to a builtin file (like %FOOTPRINTS%/Active/ICs/IC_DFS.png) or an empty + #[ApiProperty(description: 'Set the path of the attachment here. + Provide either an external URL, a path to a builtin file (like %FOOTPRINTS%/Active/ICs/IC_DFS.png) or an empty string if the attachment has an internal file associated and you\'d like to reset the external source. If you set a new (nonempty) file path any associated internal file will be removed!')] public function setURL(?string $url): self diff --git a/src/Entity/Attachments/PartCustomStateAttachment.php b/src/Entity/Attachments/PartCustomStateAttachment.php new file mode 100644 index 000000000..3a561b136 --- /dev/null +++ b/src/Entity/Attachments/PartCustomStateAttachment.php @@ -0,0 +1,45 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Attachments; + +use App\Entity\Parts\PartCustomState; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; + +/** + * An attachment attached to a part custom state element. + * @extends Attachment + */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] +class PartCustomStateAttachment extends Attachment +{ + final public const ALLOWED_ELEMENT_CLASS = PartCustomState::class; + + #[ORM\ManyToOne(targetEntity: PartCustomState::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AttachmentContainingDBElement $element = null; +} diff --git a/src/Entity/Base/AbstractDBElement.php b/src/Entity/Base/AbstractDBElement.php index ad9c534b0..27d60c865 100644 --- a/src/Entity/Base/AbstractDBElement.php +++ b/src/Entity/Base/AbstractDBElement.php @@ -36,6 +36,7 @@ use App\Entity\Attachments\ManufacturerAttachment; use App\Entity\Attachments\MeasurementUnitAttachment; use App\Entity\Attachments\PartAttachment; +use App\Entity\Attachments\PartCustomStateAttachment; use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\StorageLocationAttachment; use App\Entity\Attachments\SupplierAttachment; @@ -43,6 +44,7 @@ use App\Entity\Parameters\AbstractParameter; use App\Entity\Parts\Category; use App\Entity\PriceInformations\Pricedetail; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Entity\Parts\Footprint; @@ -71,7 +73,41 @@ * Every database table which are managed with this class (or a subclass of it) * must have the table row "id"!! The ID is the unique key to identify the elements. */ -#[DiscriminatorMap(typeProperty: 'type', mapping: ['attachment_type' => AttachmentType::class, 'attachment' => Attachment::class, 'attachment_type_attachment' => AttachmentTypeAttachment::class, 'category_attachment' => CategoryAttachment::class, 'currency_attachment' => CurrencyAttachment::class, 'footprint_attachment' => FootprintAttachment::class, 'group_attachment' => GroupAttachment::class, 'label_attachment' => LabelAttachment::class, 'manufacturer_attachment' => ManufacturerAttachment::class, 'measurement_unit_attachment' => MeasurementUnitAttachment::class, 'part_attachment' => PartAttachment::class, 'project_attachment' => ProjectAttachment::class, 'assembly_attachment' => AssemblyAttachment::class, 'storelocation_attachment' => StorageLocationAttachment::class, 'supplier_attachment' => SupplierAttachment::class, 'user_attachment' => UserAttachment::class, 'category' => Category::class, 'project' => Project::class, 'project_bom_entry' => ProjectBOMEntry::class, 'assembly' => Assembly::class, 'assembly_bom_entry' => AssemblyBOMEntry::class, 'footprint' => Footprint::class, 'group' => Group::class, 'manufacturer' => Manufacturer::class, 'orderdetail' => Orderdetail::class, 'part' => Part::class, 'pricedetail' => Pricedetail::class, 'storelocation' => StorageLocation::class, 'part_lot' => PartLot::class, 'currency' => Currency::class, 'measurement_unit' => MeasurementUnit::class, 'parameter' => AbstractParameter::class, 'supplier' => Supplier::class, 'user' => User::class])] +#[DiscriminatorMap(typeProperty: 'type', mapping: [ + 'attachment_type' => AttachmentType::class, + 'attachment' => Attachment::class, + 'attachment_type_attachment' => AttachmentTypeAttachment::class, + 'category_attachment' => CategoryAttachment::class, + 'currency_attachment' => CurrencyAttachment::class, + 'footprint_attachment' => FootprintAttachment::class, + 'group_attachment' => GroupAttachment::class, + 'label_attachment' => LabelAttachment::class, + 'manufacturer_attachment' => ManufacturerAttachment::class, + 'measurement_unit_attachment' => MeasurementUnitAttachment::class, + 'part_attachment' => PartAttachment::class, + 'part_custom_state_attachment' => PartCustomStateAttachment::class, + 'project_attachment' => ProjectAttachment::class, + 'storelocation_attachment' => StorageLocationAttachment::class, + 'supplier_attachment' => SupplierAttachment::class, + 'user_attachment' => UserAttachment::class, + 'category' => Category::class, + 'project' => Project::class, + 'project_bom_entry' => ProjectBOMEntry::class, + 'footprint' => Footprint::class, + 'group' => Group::class, + 'manufacturer' => Manufacturer::class, + 'orderdetail' => Orderdetail::class, + 'part' => Part::class, + 'part_custom_state' => PartCustomState::class, + 'pricedetail' => Pricedetail::class, + 'storelocation' => StorageLocation::class, + 'part_lot' => PartLot::class, + 'currency' => Currency::class, + 'measurement_unit' => MeasurementUnit::class, + 'parameter' => AbstractParameter::class, + 'supplier' => Supplier::class, + 'user' => User::class] +)] #[ORM\MappedSuperclass(repositoryClass: DBElementRepository::class)] abstract class AbstractDBElement implements JsonSerializable { diff --git a/src/Entity/LogSystem/CollectionElementDeleted.php b/src/Entity/LogSystem/CollectionElementDeleted.php index be19bb0c8..15e0001ed 100644 --- a/src/Entity/LogSystem/CollectionElementDeleted.php +++ b/src/Entity/LogSystem/CollectionElementDeleted.php @@ -48,6 +48,7 @@ use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Attachments\CategoryAttachment; use App\Entity\Attachments\CurrencyAttachment; +use App\Entity\Attachments\PartCustomStateAttachment; use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\FootprintAttachment; use App\Entity\Attachments\GroupAttachment; @@ -60,6 +61,8 @@ use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\LogWithEventUndoInterface; use App\Entity\Contracts\NamedElementInterface; +use App\Entity\Parameters\PartCustomStateParameter; +use App\Entity\Parts\PartCustomState; use App\Entity\Parameters\AssemblyParameter; use App\Entity\ProjectSystem\Project; use App\Entity\Parameters\AbstractParameter; @@ -162,6 +165,7 @@ private function resolveAbstractClassToInstantiableClass(string $abstract_class) Part::class => PartParameter::class, StorageLocation::class => StorageLocationParameter::class, Supplier::class => SupplierParameter::class, + PartCustomState::class => PartCustomStateParameter::class, default => throw new \RuntimeException('Unknown target class for parameter: '.$this->getTargetClass()), }; } @@ -178,6 +182,7 @@ private function resolveAbstractClassToInstantiableClass(string $abstract_class) Manufacturer::class => ManufacturerAttachment::class, MeasurementUnit::class => MeasurementUnitAttachment::class, Part::class => PartAttachment::class, + PartCustomState::class => PartCustomStateAttachment::class, StorageLocation::class => StorageLocationAttachment::class, Supplier::class => SupplierAttachment::class, User::class => UserAttachment::class, diff --git a/src/Entity/LogSystem/LogTargetType.php b/src/Entity/LogSystem/LogTargetType.php index 2bccf14af..e25e35dad 100644 --- a/src/Entity/LogSystem/LogTargetType.php +++ b/src/Entity/LogSystem/LogTargetType.php @@ -34,6 +34,7 @@ use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; use App\Entity\Parts\PartAssociation; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\PartLot; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; @@ -71,6 +72,7 @@ enum LogTargetType: int case PART_ASSOCIATION = 20; case ASSEMBLY = 21; case ASSEMBLY_BOM_ENTRY = 22; + case PART_CUSTOM_STATE = 23; /** * Returns the class name of the target type or null if the target type is NONE. @@ -102,6 +104,7 @@ public function toClass(): ?string self::PARAMETER => AbstractParameter::class, self::LABEL_PROFILE => LabelProfile::class, self::PART_ASSOCIATION => PartAssociation::class, + self::PART_CUSTOM_STATE => PartCustomState::class }; } diff --git a/src/Entity/Parameters/AbstractParameter.php b/src/Entity/Parameters/AbstractParameter.php index b6ef0412b..8170d3e3c 100644 --- a/src/Entity/Parameters/AbstractParameter.php +++ b/src/Entity/Parameters/AbstractParameter.php @@ -73,7 +73,8 @@ #[ORM\DiscriminatorMap([0 => CategoryParameter::class, 1 => CurrencyParameter::class, 2 => ProjectParameter::class, 3 => FootprintParameter::class, 4 => GroupParameter::class, 5 => ManufacturerParameter::class, 6 => MeasurementUnitParameter::class, 7 => PartParameter::class, 8 => StorageLocationParameter::class, - 9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class, 11 => AssemblyParameter::class])] + 9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class, 11 => AssemblyParameter::class, + 12 => PartCustomStateParameter::class])] #[ORM\Table('parameters')] #[ORM\Index(columns: ['name'], name: 'parameter_name_idx')] #[ORM\Index(columns: ['param_group'], name: 'parameter_group_idx')] @@ -105,7 +106,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu "AttachmentType" => AttachmentTypeParameter::class, "Category" => CategoryParameter::class, "Currency" => CurrencyParameter::class, "Project" => ProjectParameter::class, "Assembly" => AssemblyParameter::class, "Footprint" => FootprintParameter::class, "Group" => GroupParameter::class, "Manufacturer" => ManufacturerParameter::class, "MeasurementUnit" => MeasurementUnitParameter::class, - "StorageLocation" => StorageLocationParameter::class, "Supplier" => SupplierParameter::class]; + "StorageLocation" => StorageLocationParameter::class, "Supplier" => SupplierParameter::class, "PartCustomState" => PartCustomStateParameter::class]; /** * @var string The class of the element that can be passed to this attachment. Must be overridden in subclasses. @@ -460,7 +461,7 @@ protected function formatWithUnit(float $value, string $format = '%g', bool $wit return $str; } - + /** * Returns the class of the element that is allowed to be associated with this attachment. * @return string diff --git a/src/Entity/Parameters/PartCustomStateParameter.php b/src/Entity/Parameters/PartCustomStateParameter.php new file mode 100644 index 000000000..ceedf7b4d --- /dev/null +++ b/src/Entity/Parameters/PartCustomStateParameter.php @@ -0,0 +1,65 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 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 . + */ + +namespace App\Entity\Parameters; + +use App\Entity\Base\AbstractDBElement; +use App\Entity\Parts\PartCustomState; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; + +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] +class PartCustomStateParameter extends AbstractParameter +{ + final public const ALLOWED_ELEMENT_CLASS = PartCustomState::class; + + /** + * @var PartCustomState the element this para is associated with + */ + #[ORM\ManyToOne(targetEntity: PartCustomState::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; +} diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index 3530d15df..3a582edd7 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -95,7 +95,7 @@ denormalizationContext: ['groups' => ['part:write', 'api:basic:write', 'eda_info:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], )] #[ApiFilter(PropertyFilter::class)] -#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])] +#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit", "partCustomState"])] #[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "manufacturer_product_number"])] #[ApiFilter(TagFilter::class, properties: ["tags"])] diff --git a/src/Entity/Parts/PartCustomState.php b/src/Entity/Parts/PartCustomState.php new file mode 100644 index 000000000..dac7fe58a --- /dev/null +++ b/src/Entity/Parts/PartCustomState.php @@ -0,0 +1,121 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Parts; + +use ApiPlatform\Metadata\ApiProperty; +use App\Entity\Attachments\PartCustomStateAttachment; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Base\AbstractPartsContainingDBElement; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Parameters\PartCustomStateParameter; +use App\Repository\Parts\PartCustomStateRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\Criteria; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * This entity represents a custom part state. + * If an organisation uses Part-DB and has its custom part states, this is useful. + * + * @extends AbstractPartsContainingDBElement + */ +#[ORM\Entity(repositoryClass: PartCustomStateRepository::class)] +#[ORM\Table('`part_custom_states`')] +#[ORM\Index(columns: ['name'], name: 'part_custom_state_name')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@part_custom_states.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['part_custom_state:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['part_custom_state:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] +class PartCustomState extends AbstractPartsContainingDBElement +{ + /** + * @var string The comment info for this element as markdown + */ + #[Groups(['part_custom_state:read', 'part_custom_state:write', 'full', 'import'])] + protected string $comment = ''; + + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class, cascade: ['persist'])] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; + + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['part_custom_state:read', 'part_custom_state:write'])] + #[ApiProperty(readableLink: false, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; + + /** + * @var Collection + */ + #[Assert\Valid] + #[ORM\OneToMany(targetEntity: PartCustomStateAttachment::class, mappedBy: 'element', cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + #[Groups(['part_custom_state:read', 'part_custom_state:write'])] + protected Collection $attachments; + + /** @var Collection + */ + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: PartCustomStateAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => 'ASC'])] + #[Groups(['part_custom_state:read', 'part_custom_state:write'])] + protected Collection $parameters; + + #[Groups(['part_custom_state:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['part_custom_state:read'])] + protected ?\DateTimeImmutable $lastModified = null; + + public function __construct() + { + parent::__construct(); + $this->children = new ArrayCollection(); + $this->attachments = new ArrayCollection(); + $this->parameters = new ArrayCollection(); + } +} diff --git a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php index 5605ef59a..b4138f934 100644 --- a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php @@ -23,6 +23,7 @@ namespace App\Entity\Parts\PartTraits; use App\Entity\Parts\InfoProviderReference; +use App\Entity\Parts\PartCustomState; use Doctrine\DBAL\Types\Types; use App\Entity\Parts\Part; use Doctrine\ORM\Mapping as ORM; @@ -75,6 +76,14 @@ trait AdvancedPropertyTrait #[Groups(['full', 'part:read'])] protected InfoProviderReference $providerReference; + /** + * @var ?PartCustomState the custom state for the part + */ + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\ManyToOne(targetEntity: PartCustomState::class)] + #[ORM\JoinColumn(name: 'id_part_custom_state')] + protected ?PartCustomState $partCustomState = null; + /** * Checks if this part is marked, for that it needs further review. */ @@ -182,7 +191,24 @@ public function setProviderReference(InfoProviderReference $providerReference): return $this; } + /** + * Gets the custom part state for the part + * Returns null if no specific part state is set. + */ + public function getPartCustomState(): ?PartCustomState + { + return $this->partCustomState; + } + /** + * Sets the custom part state. + * + * @return $this + */ + public function setPartCustomState(?PartCustomState $partCustomState): self + { + $this->partCustomState = $partCustomState; - + return $this; + } } diff --git a/src/Form/AdminPages/PartCustomStateAdminForm.php b/src/Form/AdminPages/PartCustomStateAdminForm.php new file mode 100644 index 000000000..b8bb2815e --- /dev/null +++ b/src/Form/AdminPages/PartCustomStateAdminForm.php @@ -0,0 +1,27 @@ +. + */ + +declare(strict_types=1); + +namespace App\Form\AdminPages; + +class PartCustomStateAdminForm extends BaseEntityAdminForm +{ +} diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php index dfe449d18..7d0c14aa1 100644 --- a/src/Form/Filters/PartFilterType.php +++ b/src/Form/Filters/PartFilterType.php @@ -29,6 +29,7 @@ use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\ProjectSystem\Project; @@ -130,6 +131,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'entity_class' => MeasurementUnit::class ]); + $builder->add('partCustomState', StructuralEntityConstraintType::class, [ + 'label' => 'part.edit.partCustomState', + 'entity_class' => PartCustomState::class + ]); + $builder->add('lastModified', DateTimeConstraintType::class, [ 'label' => 'lastModified' ]); diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index c493f12bb..1c904eaa1 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -30,6 +30,7 @@ use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; +use App\Entity\Parts\PartCustomState; use App\Entity\PriceInformations\Orderdetail; use App\Form\AttachmentFormType; use App\Form\ParameterType; @@ -175,6 +176,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'disable_not_selectable' => true, 'label' => 'part.edit.partUnit', ]) + ->add('partCustomState', StructuralEntityType::class, [ + 'class' => PartCustomState::class, + 'required' => false, + 'disable_not_selectable' => true, + 'label' => 'part.edit.partCustomState', + ]) ->add('ipn', TextType::class, [ 'required' => false, 'empty_data' => null, diff --git a/src/Repository/Parts/PartCustomStateRepository.php b/src/Repository/Parts/PartCustomStateRepository.php new file mode 100644 index 000000000..d66221a24 --- /dev/null +++ b/src/Repository/Parts/PartCustomStateRepository.php @@ -0,0 +1,48 @@ +. + */ +namespace App\Repository\Parts; + +use App\Entity\Parts\PartCustomState; +use App\Repository\AbstractPartsContainingRepository; +use InvalidArgumentException; + +class PartCustomStateRepository extends AbstractPartsContainingRepository +{ + public function getParts(object $element, string $nameOrderDirection = "ASC"): array + { + if (!$element instanceof PartCustomState) { + throw new InvalidArgumentException('$element must be an PartCustomState!'); + } + + return $this->getPartsByField($element, $nameOrderDirection, 'partUnit'); + } + + public function getPartsCount(object $element): int + { + if (!$element instanceof PartCustomState) { + throw new InvalidArgumentException('$element must be an PartCustomState!'); + } + + return $this->getPartsCountByField($element, 'partUnit'); + } +} diff --git a/src/Security/Voter/AttachmentVoter.php b/src/Security/Voter/AttachmentVoter.php index 766aaeac4..c233a236f 100644 --- a/src/Security/Voter/AttachmentVoter.php +++ b/src/Security/Voter/AttachmentVoter.php @@ -23,6 +23,7 @@ namespace App\Security\Voter; use App\Entity\Attachments\AssemblyAttachment; +use App\Entity\Attachments\PartCustomStateAttachment; use App\Services\UserSystem\VoterHelper; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Attachments\AttachmentContainingDBElement; @@ -102,6 +103,8 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $ $param = 'measurement_units'; } elseif (is_a($subject, PartAttachment::class, true)) { $param = 'parts'; + } elseif (is_a($subject, PartCustomStateAttachment::class, true)) { + $param = 'part_custom_states'; } elseif (is_a($subject, StorageLocationAttachment::class, true)) { $param = 'storelocations'; } elseif (is_a($subject, SupplierAttachment::class, true)) { diff --git a/src/Security/Voter/ParameterVoter.php b/src/Security/Voter/ParameterVoter.php index f59bdeaf5..5dc30ea25 100644 --- a/src/Security/Voter/ParameterVoter.php +++ b/src/Security/Voter/ParameterVoter.php @@ -22,6 +22,7 @@ */ namespace App\Security\Voter; +use App\Entity\Parameters\PartCustomStateParameter; use App\Services\UserSystem\VoterHelper; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractDBElement; @@ -97,6 +98,8 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $ $param = 'measurement_units'; } elseif (is_a($subject, PartParameter::class, true)) { $param = 'parts'; + } elseif (is_a($subject, PartCustomStateParameter::class, true)) { + $param = 'part_custom_states'; } elseif (is_a($subject, StorageLocationParameter::class, true)) { $param = 'storelocations'; } elseif (is_a($subject, SupplierParameter::class, true)) { diff --git a/src/Security/Voter/StructureVoter.php b/src/Security/Voter/StructureVoter.php index 8079757cd..cb05ffdd5 100644 --- a/src/Security/Voter/StructureVoter.php +++ b/src/Security/Voter/StructureVoter.php @@ -24,6 +24,7 @@ use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\AttachmentType; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -55,6 +56,7 @@ final class StructureVoter extends Voter Supplier::class => 'suppliers', Currency::class => 'currencies', MeasurementUnit::class => 'measurement_units', + PartCustomState::class => 'part_custom_states', ]; public function __construct(private readonly VoterHelper $helper) diff --git a/src/Services/Attachments/PartPreviewGenerator.php b/src/Services/Attachments/PartPreviewGenerator.php index ba6e5db0d..ef13265f1 100644 --- a/src/Services/Attachments/PartPreviewGenerator.php +++ b/src/Services/Attachments/PartPreviewGenerator.php @@ -23,6 +23,7 @@ namespace App\Services\Attachments; use App\Entity\Parts\Footprint; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\StorageLocation; @@ -103,6 +104,13 @@ public function getPreviewAttachments(Part $part): array } } + if ($part->getPartCustomState() instanceof PartCustomState) { + $attachment = $part->getPartCustomState()->getMasterPictureAttachment(); + if ($this->isAttachmentValidPicture($attachment)) { + $list[] = $attachment; + } + } + if ($part->getManufacturer() instanceof Manufacturer) { $attachment = $part->getManufacturer()->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { diff --git a/src/Services/EDA/KiCadHelper.php b/src/Services/EDA/KiCadHelper.php index 75c2cc346..4b7c5e5a5 100644 --- a/src/Services/EDA/KiCadHelper.php +++ b/src/Services/EDA/KiCadHelper.php @@ -233,6 +233,10 @@ public function getKiCADPart(Part $part): array } $result["fields"]["Part-DB Unit"] = $this->createField($unit); } + if ($part->getPartCustomState() !== null) { + $customState = $part->getPartCustomState()->getName(); + $result["fields"]["Part-DB Custom state"] = $this->createField($customState); + } if ($part->getMass()) { $result["fields"]["Mass"] = $this->createField($part->getMass() . ' g'); } diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php index db495cc7e..b2d473f25 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -30,6 +30,7 @@ use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\NamedElementInterface; use App\Entity\Parts\PartAssociation; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parameters\AbstractParameter; @@ -83,6 +84,7 @@ public function __construct(protected TranslatorInterface $translator, private r AbstractParameter::class => $this->translator->trans('parameter.label'), LabelProfile::class => $this->translator->trans('label_profile.label'), PartAssociation::class => $this->translator->trans('part_association.label'), + PartCustomState::class => $this->translator->trans('part_custom_state.label'), ]; } diff --git a/src/Services/EntityMergers/Mergers/PartMerger.php b/src/Services/EntityMergers/Mergers/PartMerger.php index 4ce779e88..f1c25a7b2 100644 --- a/src/Services/EntityMergers/Mergers/PartMerger.php +++ b/src/Services/EntityMergers/Mergers/PartMerger.php @@ -65,6 +65,7 @@ public function merge(object $target, object $other, array $context = []): Part $this->useOtherValueIfNotNull($target, $other, 'footprint'); $this->useOtherValueIfNotNull($target, $other, 'category'); $this->useOtherValueIfNotNull($target, $other, 'partUnit'); + $this->useOtherValueIfNotNull($target, $other, 'partCustomState'); //We assume that the higher value is the correct one for minimum instock $this->useLargerValue($target, $other, 'minamount'); diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php index 8e1704b46..bdf26fa94 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -28,6 +28,7 @@ use App\Entity\Attachments\PartAttachment; use App\Entity\Base\AbstractDBElement; use App\Entity\Parameters\PartParameter; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; @@ -109,6 +110,7 @@ public function timeTravelURL(AbstractDBElement $entity, \DateTimeInterface $dat MeasurementUnit::class => 'measurement_unit_edit', Group::class => 'group_edit', LabelProfile::class => 'label_profile_edit', + PartCustomState::class => 'part_custom_state_edit', ]; try { @@ -216,6 +218,7 @@ public function infoURL(AbstractDBElement $entity): string MeasurementUnit::class => 'measurement_unit_edit', Group::class => 'group_edit', LabelProfile::class => 'label_profile_edit', + PartCustomState::class => 'part_custom_state_edit', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); @@ -247,6 +250,7 @@ public function editURL(AbstractDBElement $entity): string MeasurementUnit::class => 'measurement_unit_edit', Group::class => 'group_edit', LabelProfile::class => 'label_profile_edit', + PartCustomState::class => 'part_custom_state_edit', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); @@ -279,6 +283,7 @@ public function createURL(AbstractDBElement|string $entity): string MeasurementUnit::class => 'measurement_unit_new', Group::class => 'group_new', LabelProfile::class => 'label_profile_new', + PartCustomState::class => 'part_custom_state_new', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity)); @@ -311,6 +316,7 @@ public function cloneURL(AbstractDBElement $entity): string MeasurementUnit::class => 'measurement_unit_clone', Group::class => 'group_clone', LabelProfile::class => 'label_profile_clone', + PartCustomState::class => 'part_custom_state_clone', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); @@ -358,6 +364,7 @@ public function deleteURL(AbstractDBElement $entity): string MeasurementUnit::class => 'measurement_unit_delete', Group::class => 'group_delete', LabelProfile::class => 'label_profile_delete', + PartCustomState::class => 'part_custom_state_delete', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php index 1f842c23d..9e674f05c 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php @@ -29,6 +29,7 @@ use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use Doctrine\ORM\EntityManagerInterface; @@ -148,6 +149,26 @@ public function importPartUnits(array $data): int return is_countable($partunit_data) ? count($partunit_data) : 0; } + public function importPartCustomStates(array $data): int + { + if (!isset($data['partcustomstate'])) { + throw new \RuntimeException('$data must contain a "partcustomstate" key!'); + } + + $partCustomStateData = $data['partcustomstate']; + foreach ($partCustomStateData as $partCustomState) { + $customState = new PartCustomState(); + $customState->setName($partCustomState['name']); + + $this->setIDOfEntity($customState, $partCustomState['id']); + $this->em->persist($customState); + } + + $this->em->flush(); + + return is_countable($partCustomStateData) ? count($partCustomStateData) : 0; + } + public function importCategories(array $data): int { if (!isset($data['partcategory'])) { diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php index 80c2dbf77..ab06a1346 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php @@ -91,6 +91,8 @@ public function importParts(array $data): int $this->setAssociationField($entity, 'partUnit', MeasurementUnit::class, $part['partUnit_id']); } + $this->setAssociationField($entity, 'partCustomState', MeasurementUnit::class, $part['partCustomState_id']); + //Create a part lot to store the stock level and location $lot = new PartLot(); $lot->setAmount((float) ($part['stockLevel'] ?? 0)); diff --git a/src/Services/LabelSystem/SandboxedTwigFactory.php b/src/Services/LabelSystem/SandboxedTwigFactory.php index d6ea69685..d5e09fa54 100644 --- a/src/Services/LabelSystem/SandboxedTwigFactory.php +++ b/src/Services/LabelSystem/SandboxedTwigFactory.php @@ -133,7 +133,7 @@ final class SandboxedTwigFactory Supplier::class => ['getShippingCosts', 'getDefaultCurrency'], Part::class => ['isNeedsReview', 'getTags', 'getMass', 'getIpn', 'getProviderReference', 'getDescription', 'getComment', 'isFavorite', 'getCategory', 'getFootprint', - 'getPartLots', 'getPartUnit', 'useFloatAmount', 'getMinAmount', 'getAmountSum', 'isNotEnoughInstock', 'isAmountUnknown', 'getExpiredAmountSum', + 'getPartLots', 'getPartUnit', 'getPartCustomState', 'useFloatAmount', 'getMinAmount', 'getAmountSum', 'isNotEnoughInstock', 'isAmountUnknown', 'getExpiredAmountSum', 'getManufacturerProductUrl', 'getCustomProductURL', 'getManufacturingStatus', 'getManufacturer', 'getManufacturerProductNumber', 'getOrderdetails', 'isObsolete', 'getParameters', 'getGroupedParameters', diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index a2c86b7e8..26c3295af 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -30,6 +30,7 @@ use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; @@ -226,6 +227,12 @@ protected function getEditNodes(): array $this->urlGenerator->generate('label_profile_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-qrcode'); } + if ($this->security->isGranted('read', new PartCustomState())) { + $nodes[] = (new TreeViewNode( + $this->translator->trans('tree.tools.edit.part_custom_state'), + $this->urlGenerator->generate('part_custom_state_new') + ))->setIcon('fa-fw fa-treeview fa-solid fa-tools'); + } if ($this->security->isGranted('create', new Part())) { $nodes[] = (new TreeViewNode( $this->translator->trans('tree.tools.edit.part'), diff --git a/src/Services/UserSystem/PermissionPresetsHelper.php b/src/Services/UserSystem/PermissionPresetsHelper.php index 554da8b32..a63013886 100644 --- a/src/Services/UserSystem/PermissionPresetsHelper.php +++ b/src/Services/UserSystem/PermissionPresetsHelper.php @@ -102,6 +102,7 @@ private function admin(HasPermissionsInterface $perm_holder): void $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, 'part_custom_states', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'suppliers', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'projects', PermissionData::ALLOW); diff --git a/src/Twig/EntityExtension.php b/src/Twig/EntityExtension.php index 086b21c5a..b757d75e8 100644 --- a/src/Twig/EntityExtension.php +++ b/src/Twig/EntityExtension.php @@ -25,6 +25,7 @@ use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Base\AbstractDBElement; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; @@ -117,6 +118,7 @@ public function getEntityType(object $entity): ?string Currency::class => 'currency', MeasurementUnit::class => 'measurement_unit', LabelProfile::class => 'label_profile', + PartCustomState::class => 'part_custom_state', ]; foreach ($map as $class => $type) { diff --git a/templates/admin/part_custom_state_admin.html.twig b/templates/admin/part_custom_state_admin.html.twig new file mode 100644 index 000000000..004ceb657 --- /dev/null +++ b/templates/admin/part_custom_state_admin.html.twig @@ -0,0 +1,14 @@ +{% extends "admin/base_admin.html.twig" %} + +{% block card_title %} + {% trans %}part_custom_state.caption{% endtrans %} +{% endblock %} + +{% block edit_title %} + {% trans %}part_custom_state.edit{% endtrans %}: {{ entity.name }} +{% endblock %} + +{% block new_title %} + {% trans %}part_custom_state.new{% endtrans %} +{% endblock %} + diff --git a/templates/parts/edit/_advanced.html.twig b/templates/parts/edit/_advanced.html.twig index de31786c2..991a36ebb 100644 --- a/templates/parts/edit/_advanced.html.twig +++ b/templates/parts/edit/_advanced.html.twig @@ -12,4 +12,5 @@ }) }}> {{ form_row(form.ipn) }}
      -{{ form_row(form.partUnit) }} \ No newline at end of file +{{ form_row(form.partUnit) }} +{{ form_row(form.partCustomState) }} diff --git a/templates/parts/lists/_filter.html.twig b/templates/parts/lists/_filter.html.twig index c29e8ecdc..72cfc5bed 100644 --- a/templates/parts/lists/_filter.html.twig +++ b/templates/parts/lists/_filter.html.twig @@ -56,6 +56,7 @@ {{ form_row(filterForm.favorite) }} {{ form_row(filterForm.needsReview) }} {{ form_row(filterForm.measurementUnit) }} + {{ form_row(filterForm.partCustomState) }} {{ form_row(filterForm.mass) }} {{ form_row(filterForm.dbId) }} {{ form_row(filterForm.ipn) }} diff --git a/tests/Controller/AdminPages/PartCustomStateControllerTest.php b/tests/Controller/AdminPages/PartCustomStateControllerTest.php new file mode 100644 index 000000000..b92308893 --- /dev/null +++ b/tests/Controller/AdminPages/PartCustomStateControllerTest.php @@ -0,0 +1,35 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Controller\AdminPages; + +use App\Entity\Parts\PartCustomState; + +/** + * @group slow + * @group DB + */ +class PartCustomStateControllerTest extends AbstractAdminControllerTest +{ + protected static string $base_path = '/en/part_custom_state'; + protected static string $entity_class = PartCustomState::class; +} diff --git a/tests/Entity/Attachments/AttachmentTest.php b/tests/Entity/Attachments/AttachmentTest.php index 699648eb7..21a0e89fa 100644 --- a/tests/Entity/Attachments/AttachmentTest.php +++ b/tests/Entity/Attachments/AttachmentTest.php @@ -31,6 +31,7 @@ use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Attachments\CategoryAttachment; use App\Entity\Attachments\CurrencyAttachment; +use App\Entity\Attachments\PartCustomStateAttachment; use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\FootprintAttachment; use App\Entity\Attachments\GroupAttachment; @@ -40,6 +41,7 @@ use App\Entity\Attachments\StorageLocationAttachment; use App\Entity\Attachments\SupplierAttachment; use App\Entity\Attachments\UserAttachment; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -89,6 +91,7 @@ public static function subClassesDataProvider(): \Iterator yield [ManufacturerAttachment::class, Manufacturer::class]; yield [MeasurementUnitAttachment::class, MeasurementUnit::class]; yield [PartAttachment::class, Part::class]; + yield [PartCustomStateAttachment::class, PartCustomState::class]; yield [StorageLocationAttachment::class, StorageLocation::class]; yield [SupplierAttachment::class, Supplier::class]; yield [UserAttachment::class, User::class]; diff --git a/tests/Services/EntityMergers/Mergers/PartMergerTest.php b/tests/Services/EntityMergers/Mergers/PartMergerTest.php index 56c7712e0..7db4ddd66 100644 --- a/tests/Services/EntityMergers/Mergers/PartMergerTest.php +++ b/tests/Services/EntityMergers/Mergers/PartMergerTest.php @@ -29,6 +29,7 @@ use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; use App\Entity\Parts\PartAssociation; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\PartLot; use App\Entity\PriceInformations\Orderdetail; use App\Services\EntityMergers\Mergers\PartMerger; @@ -54,6 +55,7 @@ public function testMergeOfEntityRelations(): void $manufacturer1 = new Manufacturer(); $manufacturer2 = new Manufacturer(); $unit = new MeasurementUnit(); + $customState = new PartCustomState(); $part1 = (new Part()) ->setCategory($category) @@ -62,7 +64,8 @@ public function testMergeOfEntityRelations(): void $part2 = (new Part()) ->setFootprint($footprint) ->setManufacturer($manufacturer2) - ->setPartUnit($unit); + ->setPartUnit($unit) + ->setPartCustomState($customState); $merged = $this->merger->merge($part1, $part2); $this->assertSame($merged, $part1); @@ -70,6 +73,7 @@ public function testMergeOfEntityRelations(): void $this->assertSame($footprint, $merged->getFootprint()); $this->assertSame($manufacturer1, $merged->getManufacturer()); $this->assertSame($unit, $merged->getPartUnit()); + $this->assertSame($customState, $merged->getPartCustomState()); } public function testMergeOfTags(): void diff --git a/tests/assets/partkeepr_import_test.xml b/tests/assets/partkeepr_import_test.xml index 4fa497e2e..28e6f0991 100644 --- a/tests/assets/partkeepr_import_test.xml +++ b/tests/assets/partkeepr_import_test.xml @@ -7437,11 +7437,13 @@ + + diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 7b15f8e0e..89edfb80f 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -548,6 +548,12 @@ Měrné jednotky
      + + + part_custom_state.caption + Vlastní stav komponenty + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -4891,6 +4897,12 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měrné jednotky + + + part.table.partCustomState + Vlastní stav součásti + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5755,6 +5767,12 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měrná jednotka + + + part.edit.partCustomState + Vlastní stav součásti + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -6042,6 +6060,12 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měrná jednotka + + + part_custom_state.label + Vlastní stav součásti + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6285,6 +6309,12 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měrné jednotky + + + tree.tools.edit.part_custom_state + Vlastní stav součásti + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -8561,6 +8591,12 @@ Element 3 Měrná jednotka + + + perm.part_custom_states + Vlastní stav součásti + + obsolete @@ -10926,6 +10962,12 @@ Element 3 Měrná jednotka + + + log.element_edited.changed_fields.partCustomState + Vlastní stav součásti + + log.element_edited.changed_fields.expiration_date @@ -11509,6 +11551,18 @@ Element 3 Upravit měrnou jednotku + + + part_custom_state.new + Nový vlastní stav komponenty + + + + + part_custom_state.edit + Upravit vlastní stav komponenty + + user.aboutMe.label diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index d86eab149..8f28ef088 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -548,6 +548,12 @@ Måleenhed + + + part_custom_state.caption + Brugerdefineret komponentstatus + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -4898,6 +4904,12 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Måleenhed + + + part.table.partCustomState + Brugerdefineret komponentstatus + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5762,6 +5774,12 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Måleenhed + + + part.edit.partCustomState + Brugerdefineret deltilstand + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -6049,6 +6067,12 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Måleenhed + + + part_custom_state.label + Brugerdefineret deltilstand + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6292,6 +6316,12 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Måleenhed + + + tree.tools.edit.part_custom_state + Brugerdefineret komponenttilstand + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -8568,6 +8598,12 @@ Element 3 Måleenhed + + + perm.part_custom_states + Brugerdefineret komponentstatus + + obsolete @@ -10952,6 +10988,12 @@ Element 3 Måleenhed + + + log.element_edited.changed_fields.partCustomState + Brugerdefineret komponentstatus + + log.element_edited.changed_fields.expiration_date @@ -11541,6 +11583,18 @@ Oversættelsen Ret måleenhed + + + part_custom_state.new + Ny brugerdefineret komponentstatus + + + + + part_custom_state.edit + Rediger brugerdefineret komponentstatus + + user.aboutMe.label diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index c24c74fe3..fbe651ff4 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -548,6 +548,12 @@ Maßeinheit + + + part_custom_state.caption + Benutzerdefinierter Bauteilstatus + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -4908,6 +4914,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr Maßeinheit + + + part.table.partCustomState + Benutzerdefinierter Bauteilstatus + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5772,6 +5784,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr Maßeinheit + + + part.edit.partCustomState + Benutzerdefinierter Bauteilstatus + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -6059,6 +6077,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr Maßeinheit + + + part_custom_state.label + Benutzerdefinierter Bauteilstatus + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6302,6 +6326,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr Maßeinheiten + + + tree.tools.edit.part_custom_state + Benutzerdefinierter Bauteilstatus + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -8581,6 +8611,12 @@ Element 1 -> Element 1.2 Maßeinheiten + + + perm.part_custom_states + Benutzerdefinierter Bauteilstatus + + obsolete @@ -10946,6 +10982,12 @@ Element 1 -> Element 1.2 Maßeinheit + + + log.element_edited.changed_fields.partCustomState + Benutzerdefinierter Bauteilstatus + + log.element_edited.changed_fields.expiration_date @@ -11529,6 +11571,18 @@ Element 1 -> Element 1.2 Bearbeite Maßeinheit + + + part_custom_state.new + Neuer benutzerdefinierter Bauteilstatus + + + + + part_custom_state.edit + Bearbeite benutzerdefinierten Bauteilstatus + + user.aboutMe.label diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index f2126c5e3..98630c483 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -318,6 +318,12 @@ Μονάδα μέτρησης + + + part_custom_state.caption + Προσαρμοσμένη κατάσταση εξαρτήματος + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -1535,6 +1541,54 @@ Επεξεργασία + + + perm.part_custom_states + Προσαρμοσμένη κατάσταση εξαρτήματος + + + + + tree.tools.edit.part_custom_state + Προσαρμοσμένη κατάσταση εξαρτήματος + + + + + part_custom_state.new + Νέα προσαρμοσμένη κατάσταση εξαρτήματος + + + + + part_custom_state.edit + Επεξεργασία προσαρμοσμένης κατάστασης εξαρτήματος + + + + + part_custom_state.label + Προσαρμοσμένη κατάσταση μέρους + + + + + log.element_edited.changed_fields.partCustomState + Προσαρμοσμένη κατάσταση εξαρτήματος + + + + + part.edit.partCustomState + Προσαρμοσμένη κατάσταση εξαρτήματος + + + + + part.table.partCustomState + Προσαρμοσμένη κατάσταση μέρους + + part.table.name.value.for_part diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index aa140bb55..9eeb1ef05 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -548,6 +548,12 @@ Measurement Unit + + + part_custom_state.caption + Custom part state + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -4909,6 +4915,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Measurement Unit + + + part.table.partCustomState + Custom part state + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5773,6 +5785,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Measuring unit + + + part.edit.partCustomState + Custom part state + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -6060,6 +6078,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Measurement unit + + + part_custom_state.label + Custom part state + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6303,6 +6327,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Measurement Unit + + + tree.tools.edit.part_custom_state + Custom part state + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -8582,6 +8612,12 @@ Element 1 -> Element 1.2 Measurement unit + + + perm.part_custom_states + Custom part state + + obsolete @@ -10947,6 +10983,12 @@ Element 1 -> Element 1.2 Measuring Unit + + + log.element_edited.changed_fields.partCustomState + Custom part state + + log.element_edited.changed_fields.expiration_date @@ -11530,6 +11572,18 @@ Element 1 -> Element 1.2 Edit Measurement Unit + + + part_custom_state.new + New custom part state + + + + + part_custom_state.edit + Edit custom part state + + user.aboutMe.label diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 441fb6fc0..8b48e4428 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -548,6 +548,12 @@ Unidad de medida + + + part_custom_state.caption + Estado personalizado del componente + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -4908,6 +4914,12 @@ Subelementos serán desplazados hacia arriba. Unidad de Medida + + + part.table.partCustomState + Estado personalizado del componente + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5772,6 +5784,12 @@ Subelementos serán desplazados hacia arriba. Unidad de medida + + + part.edit.partCustomState + Estado personalizado de la pieza + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -6059,6 +6077,12 @@ Subelementos serán desplazados hacia arriba. Unidad de medida + + + part_custom_state.label + Estado personalizado de la pieza + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6302,6 +6326,12 @@ Subelementos serán desplazados hacia arriba. Unidad de medida + + + tree.tools.edit.part_custom_state + Estado personalizado del componente + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -8578,6 +8608,12 @@ Elemento 3 Unidad de medida + + + perm.part_custom_states + Estado personalizado del componente + + obsolete @@ -10962,6 +10998,12 @@ Elemento 3 Unidad de medida + + + log.element_edited.changed_fields.partCustomState + Estado personalizado del componente + + log.element_edited.changed_fields.expiration_date @@ -11545,6 +11587,18 @@ Elemento 3 Editar Unidad de Medida + + + part_custom_state.new + Nuevo estado personalizado del componente + + + + + part_custom_state.edit + Editar estado personalizado del componente + + user.aboutMe.label diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 0ed61ac5a..ee1d6ffe1 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -517,6 +517,12 @@ Unité de mesure + + + part_custom_state.caption + État personnalisé du composant + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -4871,6 +4877,12 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia Unité de mesure + + + part.table.partCustomState + État personnalisé de la pièce + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5735,6 +5747,12 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia Unité de mesure + + + part.edit.partCustomState + État personnalisé de la pièce + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -6012,6 +6030,12 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia Unité de mesure + + + part_custom_state.label + État personnalisé de la pièce + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6244,6 +6268,12 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia Unité de mesure + + + tree.tools.edit.part_custom_state + État personnalisé de la pièce + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -8507,6 +8537,12 @@ exemple de ville Unités de mesure + + + perm.part_custom_states + État personnalisé du composant + + obsolete @@ -10128,5 +10164,23 @@ exemple de ville Un préfixe suggéré lors de la saisie de l'IPN d'une pièce. + + + part_custom_state.new + Nouveau statut personnalisé du composant + + + + + part_custom_state.edit + Modifier le statut personnalisé du composant + + + + + log.element_edited.changed_fields.partCustomState + État personnalisé de la pièce + + diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 4b749523d..ac1344052 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -548,6 +548,12 @@ Unità di misura + + + part_custom_state.caption + Stato personalizzato del componente + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -4910,6 +4916,12 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi Unità di misura + + + part.table.partCustomState + Stato personalizzato del componente + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5774,6 +5786,12 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi Unità di misura + + + part.edit.partCustomState + Stato personalizzato della parte + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -6061,6 +6079,12 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi Unità di misura + + + part_custom_state.label + Stato personalizzato della parte + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6304,6 +6328,12 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi Unità di misura + + + tree.tools.edit.part_custom_state + Stato personalizzato del componente + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -8580,6 +8610,12 @@ Element 3 Unità di misura + + + perm.part_custom_states + Stato personalizzato del componente + + obsolete @@ -10964,6 +11000,12 @@ Element 3 Unità di misura + + + log.element_edited.changed_fields.partCustomState + Stato personalizzato della parte + + log.element_edited.changed_fields.expiration_date @@ -11547,6 +11589,18 @@ Element 3 Modificare l'unità di misura + + + part_custom_state.new + Nuovo stato personalizzato del componente + + + + + part_custom_state.edit + Modifica stato personalizzato del componente + + user.aboutMe.label diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index abc01949e..e892af9c4 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -517,6 +517,12 @@ 単位 + + + part_custom_state.caption + 部品のカスタム状態 + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -4871,6 +4877,12 @@ 単位 + + + part.table.partCustomState + 部品のカスタム状態 + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5735,6 +5747,12 @@ 単位 + + + part.edit.partCustomState + 部品のカスタム状態 + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -6012,6 +6030,12 @@ 単位 + + + part_custom_state.label + 部品のカスタム状態 + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6244,6 +6268,12 @@ 単位 + + + tree.tools.edit.part_custom_state + 部品のカスタム状態 + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -8508,6 +8538,12 @@ Exampletown 単位 + + + perm.part_custom_states + 部品のカスタム状態 + + obsolete @@ -9841,5 +9877,23 @@ Exampletown 部品のIPN入力時に提案される接頭辞。 + + + part_custom_state.new + 部品の新しいカスタム状態 + + + + + part_custom_state.edit + 部品のカスタム状態を編集 + + + + + log.element_edited.changed_fields.partCustomState + 部品のカスタム状態 + + diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 14242c3aa..b24e14b9a 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -548,6 +548,12 @@ Meeteenheden + + + part_custom_state.caption + Aangepaste status van het onderdeel + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -724,6 +730,54 @@ Weet u zeker dat u wilt doorgaan? + + + perm.part_custom_states + Aangepaste status van onderdeel + + + + + tree.tools.edit.part_custom_state + Aangepaste status van het onderdeel + + + + + part_custom_state.new + Nieuwe aangepaste status van het onderdeel + + + + + part_custom_state.edit + Aangepaste status van het onderdeel bewerken + + + + + part_custom_state.label + Aangepaste staat van onderdeel + + + + + log.element_edited.changed_fields.partCustomState + Aangepaste staat van het onderdeel + + + + + part.edit.partCustomState + Aangepaste staat van het onderdeel + + + + + part.table.partCustomState + Aangepaste status van onderdeel + + part.table.name.value.for_part diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index f56858e3a..a09e3ac33 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -548,6 +548,12 @@ Jednostka miary + + + part_custom_state.caption + Stan niestandardowy komponentu + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -4913,6 +4919,12 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo Jednostka pomiarowa + + + part.table.partCustomState + Niestandardowy stan części + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5777,6 +5789,12 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo Jednostka pomiarowa + + + part.edit.partCustomState + Własny stan części + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -6064,6 +6082,12 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo Jednostka pomiarowa + + + part_custom_state.label + Własny stan części + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6307,6 +6331,12 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo Jednostka miary + + + tree.tools.edit.part_custom_state + Niestandardowy stan części + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -8583,6 +8613,12 @@ Element 3 Jednostka miary + + + perm.part_custom_states + Stan niestandardowy komponentu + + obsolete @@ -10967,6 +11003,12 @@ Element 3 Jednostka pomiarowa + + + log.element_edited.changed_fields.partCustomState + Niestandardowy stan części + + log.element_edited.changed_fields.expiration_date @@ -11550,6 +11592,18 @@ Element 3 Edytuj jednostkę miary + + + part_custom_state.new + Nowy niestandardowy stan części + + + + + part_custom_state.edit + Edytuj niestandardowy stan części + + user.aboutMe.label diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 45d743baf..8d6a0ba10 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -548,6 +548,12 @@ Единица измерения + + + part_custom_state.caption + Пользовательское состояние компонента + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -4919,6 +4925,12 @@ Единица измерения + + + part.table.partCustomState + Пользовательское состояние детали + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5783,6 +5795,12 @@ Единица измерения + + + part.edit.partCustomState + Пользовательское состояние части + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -6070,6 +6088,12 @@ Единица измерения + + + part_custom_state.label + Пользовательское состояние детали + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6313,6 +6337,12 @@ Единица измерения + + + tree.tools.edit.part_custom_state + Пользовательское состояние компонента + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -8587,6 +8617,12 @@ Единица измерения + + + perm.part_custom_states + Пользовательское состояние компонента + + obsolete @@ -10971,6 +11007,12 @@ Единица измерения + + + log.element_edited.changed_fields.partCustomState + Пользовательское состояние детали + + log.element_edited.changed_fields.expiration_date @@ -11554,6 +11596,18 @@ Изменить единицу измерения + + + part_custom_state.new + Новое пользовательское состояние компонента + + + + + part_custom_state.edit + Редактировать пользовательское состояние компонента + + user.aboutMe.label diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index edb4ec59e..f257f9a2f 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -548,6 +548,12 @@ 计量单位 + + + part_custom_state.caption + 部件的自定义状态 + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -4917,6 +4923,12 @@ 计量单位 + + + part.table.partCustomState + 部件的自定义状态 + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5781,6 +5793,12 @@ 计量单位 + + + part.edit.partCustomState + 部件的自定义状态 + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -6068,6 +6086,12 @@ 计量单位 + + + part_custom_state.label + 部件自定义状态 + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6311,6 +6335,12 @@ 计量单位 + + + tree.tools.edit.part_custom_state + 部件自定义状态 + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -8586,6 +8616,12 @@ Element 3 计量单位 + + + perm.part_custom_states + 部件的自定义状态 + + obsolete @@ -10970,6 +11006,12 @@ Element 3 计量单位 + + + log.element_edited.changed_fields.partCustomState + 部件的自定义状态 + + log.element_edited.changed_fields.expiration_date @@ -11553,6 +11595,18 @@ Element 3 编辑度量单位 + + + part_custom_state.new + 部件的新自定义状态 + + + + + part_custom_state.edit + 编辑部件的自定义状态 + + user.aboutMe.label From f94fbe8b96d0b7cbe722b7f4eda44e0c6dd01f1d Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 24 Mar 2025 15:19:35 +0100 Subject: [PATCH 62/83] =?UTF-8?q?PartCustomStateController=20hinzuf=C3=BCg?= =?UTF-8?q?en?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminPages/PartCustomStateController.php | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/Controller/AdminPages/PartCustomStateController.php diff --git a/src/Controller/AdminPages/PartCustomStateController.php b/src/Controller/AdminPages/PartCustomStateController.php new file mode 100644 index 000000000..60f63abf3 --- /dev/null +++ b/src/Controller/AdminPages/PartCustomStateController.php @@ -0,0 +1,83 @@ +. + */ + +declare(strict_types=1); + +namespace App\Controller\AdminPages; + +use App\Entity\Attachments\PartCustomStateAttachment; +use App\Entity\Parameters\PartCustomStateParameter; +use App\Entity\Parts\PartCustomState; +use App\Form\AdminPages\PartCustomStateAdminForm; +use App\Services\ImportExportSystem\EntityExporter; +use App\Services\ImportExportSystem\EntityImporter; +use App\Services\Trees\StructuralElementRecursionHelper; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +/** + * @see \App\Tests\Controller\AdminPages\PartCustomStateControllerTest + */ +#[Route(path: '/part_custom_state')] +class PartCustomStateController extends BaseAdminController +{ + protected string $entity_class = PartCustomState::class; + protected string $twig_template = 'admin/part_custom_state_admin.html.twig'; + protected string $form_class = PartCustomStateAdminForm::class; + protected string $route_base = 'part_custom_state'; + protected string $attachment_class = PartCustomStateAttachment::class; + protected ?string $parameter_class = PartCustomStateParameter::class; + + #[Route(path: '/{id}', name: 'part_custom_state_delete', methods: ['DELETE'])] + public function delete(Request $request, PartCustomState $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse + { + return $this->_delete($request, $entity, $recursionHelper); + } + + #[Route(path: '/{id}/edit/{timestamp}', name: 'part_custom_state_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}', requirements: ['id' => '\d+'])] + public function edit(PartCustomState $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response + { + return $this->_edit($entity, $request, $em, $timestamp); + } + + #[Route(path: '/new', name: 'part_custom_state_new')] + #[Route(path: '/{id}/clone', name: 'part_custom_state_clone')] + #[Route(path: '/')] + public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?PartCustomState $entity = null): Response + { + return $this->_new($request, $em, $importer, $entity); + } + + #[Route(path: '/export', name: 'part_custom_state_export_all')] + public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response + { + return $this->_exportAll($em, $exporter, $request); + } + + #[Route(path: '/{id}/export', name: 'part_custom_state_export')] + public function exportEntity(PartCustomState $entity, EntityExporter $exporter, Request $request): Response + { + return $this->_exportEntity($entity, $exporter, $request); + } +} From 76d6aff8dec805fe79f39c5cdf8091a4117095c8 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 23 Apr 2025 10:49:07 +0200 Subject: [PATCH 63/83] =?UTF-8?q?Umstellung=20Migrationen=20bzgl.=20Multi-?= =?UTF-8?q?Plattform-Support.=20Zun=C3=A4chst=20MySQL,=20SQLite=20Statemen?= =?UTF-8?q?ts=20integrieren.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/Version20250321075747.php | 59 +++- migrations/Version20250321141740.php | 458 ++++++++++++++++++++++++++- src/Entity/Parts/PartCustomState.php | 6 + 3 files changed, 514 insertions(+), 9 deletions(-) diff --git a/migrations/Version20250321075747.php b/migrations/Version20250321075747.php index 45b35397e..08466961b 100644 --- a/migrations/Version20250321075747.php +++ b/migrations/Version20250321075747.php @@ -4,20 +4,69 @@ namespace DoctrineMigrations; +use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; -use Doctrine\Migrations\AbstractMigration; -final class Version20250321075747 extends AbstractMigration +final class Version20250321075747 extends AbstractMultiPlatformMigration { - public function up(Schema $schema): void + public function getDescription(): string { - $this->addSql('CREATE TABLE `part_custom_states` (id INT AUTO_INCREMENT NOT NULL, parent_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_F552745D727ACA70 (parent_id), INDEX part_custom_state_name (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); + return 'Create entity table for custom part states'; + } + + public function mySQLUp(Schema $schema): void + { + $this->addSql('CREATE TABLE `part_custom_states` (id INT AUTO_INCREMENT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, name VARCHAR(255) NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_F552745D727ACA70 (parent_id), INDEX IDX_F552745DEA7100A1 (id_preview_attachment), INDEX part_custom_state_name (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); $this->addSql('ALTER TABLE `part_custom_states` ADD CONSTRAINT FK_F552745D727ACA70 FOREIGN KEY (parent_id) REFERENCES `part_custom_states` (id)'); + $this->addSql('ALTER TABLE `part_custom_states` ADD CONSTRAINT FK_F552745DEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES `attachments` (id) ON DELETE SET NULL'); } - public function down(Schema $schema): void + public function mySQLDown(Schema $schema): void { $this->addSql('ALTER TABLE `part_custom_states` DROP FOREIGN KEY FK_F552745D727ACA70'); + $this->addSql('ALTER TABLE `part_custom_states` DROP FOREIGN KEY FK_F552745DEA7100A1'); $this->addSql('DROP TABLE `part_custom_states`'); } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE "part_custom_states" ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + name VARCHAR(255) NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + alternative_names CLOB DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT FK_F552745D727ACA70 FOREIGN KEY (parent_id) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_F5AF83CFEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_F552745D727ACA70 ON "part_custom_states" (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX part_custom_state_name ON "part_custom_states" (name) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP TABLE "part_custom_states" + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + //Not needed + } + + public function postgreSQLDown(Schema $schema): void + { + //Not needed + } } diff --git a/migrations/Version20250321141740.php b/migrations/Version20250321141740.php index cd1aefdc0..d518b1f68 100644 --- a/migrations/Version20250321141740.php +++ b/migrations/Version20250321141740.php @@ -4,22 +4,472 @@ namespace DoctrineMigrations; +use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; -use Doctrine\Migrations\AbstractMigration; -final class Version20250321141740 extends AbstractMigration +final class Version20250321141740 extends AbstractMultiPlatformMigration { - public function up(Schema $schema): void + public function getDescription(): string + { + return 'Add custom state to parts'; + } + + public function mySQLUp(Schema $schema): void { $this->addSql('ALTER TABLE parts ADD id_part_custom_state INT DEFAULT NULL AFTER id_part_unit'); $this->addSql('ALTER TABLE parts ADD CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES `part_custom_states` (id)'); $this->addSql('CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state)'); } - public function down(Schema $schema): void + public function mySQLDown(Schema $schema): void { $this->addSql('ALTER TABLE `parts` DROP FOREIGN KEY FK_6940A7FEA3ED1215'); $this->addSql('DROP INDEX IDX_6940A7FEA3ED1215 ON `parts`'); $this->addSql('ALTER TABLE `parts` DROP id_part_custom_state'); } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__parts AS + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM parts + SQL); + + $this->addSql(<<<'SQL' + DROP TABLE parts + SQL); + + $this->addSql(<<<'SQL' + CREATE TABLE parts ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + id_category INTEGER NOT NULL, + id_footprint INTEGER DEFAULT NULL, + id_part_unit INTEGER DEFAULT NULL, + id_manufacturer INTEGER DEFAULT NULL, + id_part_custom_state INTEGER DEFAULT NULL, + order_orderdetails_id INTEGER DEFAULT NULL, + built_project_id INTEGER DEFAULT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + needs_review BOOLEAN NOT NULL, + tags CLOB NOT NULL, + mass DOUBLE PRECISION DEFAULT NULL, + description CLOB NOT NULL, + comment CLOB NOT NULL, + visible BOOLEAN NOT NULL, + favorite BOOLEAN NOT NULL, + minamount DOUBLE PRECISION NOT NULL, + manufacturer_product_url CLOB NOT NULL, + manufacturer_product_number VARCHAR(255) NOT NULL, + manufacturing_status VARCHAR(255) DEFAULT NULL, + order_quantity INTEGER NOT NULL, + manual_order BOOLEAN NOT NULL, + ipn VARCHAR(100) DEFAULT NULL, + provider_reference_provider_key VARCHAR(255) DEFAULT NULL, + provider_reference_provider_id VARCHAR(255) DEFAULT NULL, + provider_reference_provider_url VARCHAR(255) DEFAULT NULL, + provider_reference_last_updated DATETIME DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_value VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO parts ( + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint) + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM __temp__parts + SQL); + + $this->addSql(<<<'SQL' + DROP TABLE __temp__parts + SQL); + + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_name ON parts (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_ipn ON parts (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_datet_name_last_id_needs ON parts (datetime_added, name, last_modified, id, needs_review) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON parts (order_orderdetails_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEEA7100A1 ON parts (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE7E371A10 ON parts (id_footprint) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE5697F554 ON parts (id_category) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE2626CEF9 ON parts (id_part_unit) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__parts AS + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM "parts" + SQL); + $this->addSql(<<<'SQL' + DROP TABLE "parts" + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE "parts" ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + id_category INTEGER NOT NULL, + id_footprint INTEGER DEFAULT NULL, + id_part_unit INTEGER DEFAULT NULL, + id_manufacturer INTEGER DEFAULT NULL, + order_orderdetails_id INTEGER DEFAULT NULL, + built_project_id INTEGER DEFAULT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + needs_review BOOLEAN NOT NULL, + tags CLOB NOT NULL, + mass DOUBLE PRECISION DEFAULT NULL, + description CLOB NOT NULL, + comment CLOB NOT NULL, + visible BOOLEAN NOT NULL, + favorite BOOLEAN NOT NULL, + minamount DOUBLE PRECISION NOT NULL, + manufacturer_product_url CLOB NOT NULL, + manufacturer_product_number VARCHAR(255) NOT NULL, + manufacturing_status VARCHAR(255) DEFAULT NULL, + order_quantity INTEGER NOT NULL, + manual_order BOOLEAN NOT NULL, + ipn VARCHAR(100) DEFAULT NULL, + provider_reference_provider_key VARCHAR(255) DEFAULT NULL, + provider_reference_provider_id VARCHAR(255) DEFAULT NULL, + provider_reference_provider_url VARCHAR(255) DEFAULT NULL, + provider_reference_last_updated DATETIME DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_value VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + $this->addSql(<<<'SQL' + INSERT INTO "parts" ( + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + ) SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM __temp__parts + SQL); + + $this->addSql(<<<'SQL' + DROP TABLE __temp__parts + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_name ON "parts" (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_ipn ON "parts" (ipn) + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + //Not needed + } + + public function postgreSQLDown(Schema $schema): void + { + //Not needed + } } diff --git a/src/Entity/Parts/PartCustomState.php b/src/Entity/Parts/PartCustomState.php index dac7fe58a..e8429199c 100644 --- a/src/Entity/Parts/PartCustomState.php +++ b/src/Entity/Parts/PartCustomState.php @@ -23,6 +23,7 @@ namespace App\Entity\Parts; use ApiPlatform\Metadata\ApiProperty; +use App\Entity\Attachments\Attachment; use App\Entity\Attachments\PartCustomStateAttachment; use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; use ApiPlatform\Doctrine\Orm\Filter\DateFilter; @@ -98,6 +99,11 @@ class PartCustomState extends AbstractPartsContainingDBElement #[Groups(['part_custom_state:read', 'part_custom_state:write'])] protected Collection $attachments; + #[ORM\ManyToOne(targetEntity: PartCustomStateAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['part_custom_state:read', 'part_custom_state:write'])] + protected ?Attachment $master_picture_attachment = null; + /** @var Collection */ #[Assert\Valid] From ad3be390ae4aa8316b9b7062bc29f51edb6d7dd8 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 17 Jun 2025 13:48:52 +0200 Subject: [PATCH 64/83] Postgre Statements integrieren --- migrations/Version20250321075747.php | 69 ++++++++++++++++++++++++---- migrations/Version20250321141740.php | 44 ++++++++++++++---- 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/migrations/Version20250321075747.php b/migrations/Version20250321075747.php index 08466961b..4ab505b3b 100644 --- a/migrations/Version20250321075747.php +++ b/migrations/Version20250321075747.php @@ -16,16 +16,43 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { - $this->addSql('CREATE TABLE `part_custom_states` (id INT AUTO_INCREMENT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, name VARCHAR(255) NOT NULL, comment LONGTEXT NOT NULL, not_selectable TINYINT(1) NOT NULL, alternative_names LONGTEXT DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_F552745D727ACA70 (parent_id), INDEX IDX_F552745DEA7100A1 (id_preview_attachment), INDEX part_custom_state_name (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`'); - $this->addSql('ALTER TABLE `part_custom_states` ADD CONSTRAINT FK_F552745D727ACA70 FOREIGN KEY (parent_id) REFERENCES `part_custom_states` (id)'); - $this->addSql('ALTER TABLE `part_custom_states` ADD CONSTRAINT FK_F552745DEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES `attachments` (id) ON DELETE SET NULL'); + $this->addSql(<<<'SQL' + CREATE TABLE part_custom_states ( + id INT AUTO_INCREMENT NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, + name VARCHAR(255) NOT NULL, + comment LONGTEXT NOT NULL, + not_selectable TINYINT(1) NOT NULL, + alternative_names LONGTEXT DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + INDEX IDX_F552745D727ACA70 (parent_id), + INDEX IDX_F552745DEA7100A1 (id_preview_attachment), + INDEX part_custom_state_name (name), + PRIMARY KEY(id) + ) + DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE part_custom_states ADD CONSTRAINT FK_F552745D727ACA70 FOREIGN KEY (parent_id) REFERENCES part_custom_states (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE part_custom_states ADD CONSTRAINT FK_F552745DEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON DELETE SET NULL + SQL); } public function mySQLDown(Schema $schema): void { - $this->addSql('ALTER TABLE `part_custom_states` DROP FOREIGN KEY FK_F552745D727ACA70'); - $this->addSql('ALTER TABLE `part_custom_states` DROP FOREIGN KEY FK_F552745DEA7100A1'); - $this->addSql('DROP TABLE `part_custom_states`'); + $this->addSql(<<<'SQL' + ALTER TABLE part_custom_states DROP FOREIGN KEY FK_F552745D727ACA70; + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE part_custom_states DROP FOREIGN KEY FK_F552745DEA7100A1 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE part_custom_states + SQL); } public function sqLiteUp(Schema $schema): void @@ -62,11 +89,37 @@ public function sqLiteDown(Schema $schema): void public function postgreSQLUp(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + CREATE TABLE "part_custom_states" ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id), + name VARCHAR(255) NOT NULL, + comment TEXT NOT NULL, + not_selectable BOOLEAN NOT NULL, + alternative_names TEXT DEFAULT NULL, + last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_F552745D727ACA70 ON "part_custom_states" (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_F552745DEA7100A1 ON "part_custom_states" (id_preview_attachment) + SQL); } public function postgreSQLDown(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + ALTER TABLE "part_custom_states" DROP CONSTRAINT FK_F552745D727ACA70 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE "part_custom_states" DROP CONSTRAINT FK_F552745DEA7100A1 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE "part_custom_states" + SQL); } } diff --git a/migrations/Version20250321141740.php b/migrations/Version20250321141740.php index d518b1f68..5e9f87d08 100644 --- a/migrations/Version20250321141740.php +++ b/migrations/Version20250321141740.php @@ -16,16 +16,28 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { - $this->addSql('ALTER TABLE parts ADD id_part_custom_state INT DEFAULT NULL AFTER id_part_unit'); - $this->addSql('ALTER TABLE parts ADD CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES `part_custom_states` (id)'); - $this->addSql('CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state)'); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD id_part_custom_state INT DEFAULT NULL AFTER id_part_unit + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES part_custom_states (id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state) + SQL); } public function mySQLDown(Schema $schema): void { - $this->addSql('ALTER TABLE `parts` DROP FOREIGN KEY FK_6940A7FEA3ED1215'); - $this->addSql('DROP INDEX IDX_6940A7FEA3ED1215 ON `parts`'); - $this->addSql('ALTER TABLE `parts` DROP id_part_custom_state'); + $this->addSql(<<<'SQL' + ALTER TABLE parts DROP FOREIGN KEY FK_6940A7FEA3ED1215 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_6940A7FEA3ED1215 ON parts + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts DROP id_part_custom_state + SQL); } public function sqLiteUp(Schema $schema): void @@ -465,11 +477,27 @@ public function sqLiteDown(Schema $schema): void public function postgreSQLUp(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD id_part_custom_state INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state) + SQL); } public function postgreSQLDown(Schema $schema): void { - //Not needed + $this->addSql(<<<'SQL' + ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FEA3ED1215 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_6940A7FEA3ED1215 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE "parts" DROP id_part_custom_state + SQL); } } From 6b4d7f18389b9ed6f0d4d40c67fe213a1da3488d Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 17 Jun 2025 13:51:29 +0200 Subject: [PATCH 65/83] Semikolon in Migration entfernen --- migrations/Version20250321075747.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/Version20250321075747.php b/migrations/Version20250321075747.php index 4ab505b3b..b236796dc 100644 --- a/migrations/Version20250321075747.php +++ b/migrations/Version20250321075747.php @@ -45,7 +45,7 @@ public function mySQLUp(Schema $schema): void public function mySQLDown(Schema $schema): void { $this->addSql(<<<'SQL' - ALTER TABLE part_custom_states DROP FOREIGN KEY FK_F552745D727ACA70; + ALTER TABLE part_custom_states DROP FOREIGN KEY FK_F552745D727ACA70 SQL); $this->addSql(<<<'SQL' ALTER TABLE part_custom_states DROP FOREIGN KEY FK_F552745DEA7100A1 From a27f59dc00d96c13315d2ce7a29decc6594cfb0a Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 17 Jul 2025 10:54:44 +0200 Subject: [PATCH 66/83] Anpassungen aus Analyse vornehmen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Name Validierung bei Assembly Angabe in Stücklisten anpassen. permission_layout.html hinsichtlich Synonym-Ausgabe Datenquelle anpassen. Anpassung aus Analyse. --- assets/js/lib/datatables.js | 8 ++++++-- migrations/Version20250304154507.php | 2 +- src/DataTables/AssemblyBomEntriesDataTable.php | 2 ++ src/Entity/AssemblySystem/AssemblyBOMEntry.php | 6 +++--- templates/form/permission_layout.html.twig | 15 ++++++++++----- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/assets/js/lib/datatables.js b/assets/js/lib/datatables.js index 67bab02db..7c82439cc 100644 --- a/assets/js/lib/datatables.js +++ b/assets/js/lib/datatables.js @@ -13,11 +13,16 @@ * Initializes the datatable dynamically. */ $.fn.initDataTables = function(config, options) { - //Update default used url, so it reflects the current location (useful on single side apps) //CHANGED jbtronics: Preserve the get parameters (needed so we can pass additional params to query) $.fn.initDataTables.defaults.url = window.location.origin + window.location.pathname + window.location.search; + $.fn.dataTable.ext.errMode = function(settings, helpPage, message) { + if (message.includes('ColReorder')) { + console.warn('ColReorder does not fit the number of columns', message); + } + }; + var root = this, config = $.extend({}, $.fn.initDataTables.defaults, config), state = '' @@ -105,7 +110,6 @@ } } - root.html(data.template); dt = $('table', root).DataTable(dtOpts); if (config.state !== 'none') { diff --git a/migrations/Version20250304154507.php b/migrations/Version20250304154507.php index 4f7fed2f4..8a406b27d 100644 --- a/migrations/Version20250304154507.php +++ b/migrations/Version20250304154507.php @@ -18,7 +18,7 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { $this->addSql(<<<'SQL' - ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL + ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL AFTER build_project_id SQL); $this->addSql(<<<'SQL' ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php index fed6850f4..b2c3e118d 100644 --- a/src/DataTables/AssemblyBomEntriesDataTable.php +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -127,6 +127,8 @@ public function configure(DataTable $dataTable, array $options): void 'render' => function ($value, AssemblyBOMEntry $context) { if($context->getPart() instanceof Part) { return $context->getPart()->getIpn(); + } elseif($context->getReferencedAssembly() instanceof Assembly) { + return $context->getReferencedAssembly()->getIpn(); } return ''; diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 6a3e82d3c..8a4cdbc24 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -106,7 +106,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt /** * @var string|null An optional name describing this BOM entry (useful for non-part entries) */ - #[Assert\Expression('this.getPart() !== null or this.getName() !== null', message: 'validator.assembly.bom_entry.name_or_part_needed')] + #[Assert\Expression('this.getPart() !== null or this.getReferencedAssembly() !== null or this.getName() !== null', message: 'validator.assembly.bom_entry.name_or_part_needed')] #[ORM\Column(type: Types::STRING, nullable: true)] #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] protected ?string $name = null; @@ -206,7 +206,7 @@ public function setMountnames(string $mountnames): AssemblyBOMEntry */ public function getName(): ?string { - return $this->name; + return trim($this->name ?? '') === '' ? null : $this->name; } /** @@ -214,7 +214,7 @@ public function getName(): ?string */ public function setName(?string $name): AssemblyBOMEntry { - $this->name = $name; + $this->name = trim($name ?? '') === '' ? null : $name; return $this; } diff --git a/templates/form/permission_layout.html.twig b/templates/form/permission_layout.html.twig index 896a2defa..dcceae335 100644 --- a/templates/form/permission_layout.html.twig +++ b/templates/form/permission_layout.html.twig @@ -7,15 +7,15 @@
      From 73f1fa9b0f222a04872a62c13360da90b4b3588b Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 17 Jul 2025 11:02:20 +0200 Subject: [PATCH 67/83] =?UTF-8?q?Migration=20f=C3=BCr=20PartCustomState=20?= =?UTF-8?q?aktualisieren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/Version20250321141740.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/Version20250321141740.php b/migrations/Version20250321141740.php index 5e9f87d08..34eca6526 100644 --- a/migrations/Version20250321141740.php +++ b/migrations/Version20250321141740.php @@ -134,7 +134,7 @@ public function sqLiteUp(Schema $schema): void CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE ) From 04ad4572ca69df5cfc035fda8cca17acf3286662 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 17 Jul 2025 11:07:10 +0200 Subject: [PATCH 68/83] Anpassungen aus Analyse vornehmen --- migrations/Version20250325073036.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/migrations/Version20250325073036.php b/migrations/Version20250325073036.php index a9d3eaaad..0070bcbe3 100644 --- a/migrations/Version20250325073036.php +++ b/migrations/Version20250325073036.php @@ -27,7 +27,7 @@ public function mySQLUp(Schema $schema): void public function mySQLDown(Schema $schema): void { $this->addSql(<<<'SQL' - ALTER TABLE categories DROP part_ipn_prefixSQL + ALTER TABLE categories DROP part_ipn_prefix SQL); $this->addSql(<<<'SQL' CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn) @@ -164,6 +164,10 @@ public function sqLiteUp(Schema $schema): void $this->addSql(<<<'SQL' CREATE INDEX category_idx_parent_name ON categories (parent_id, name) SQL); + + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_6940A7FE3D721C14 + SQL); } public function sqLiteDown(Schema $schema): void @@ -295,6 +299,10 @@ public function sqLiteDown(Schema $schema): void $this->addSql(<<<'SQL' CREATE INDEX category_idx_parent_name ON categories (parent_id, name) SQL); + + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn) + SQL); } public function postgreSQLUp(Schema $schema): void From 0e542cb95591838a4fa07d46e17222b89c86252f Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 17 Jul 2025 11:35:37 +0200 Subject: [PATCH 69/83] =?UTF-8?q?Anpassung=20Migration=20aus=20Zusammenf?= =?UTF-8?q?=C3=BChrung=20Feature=20Branches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/Version20250321141740.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/migrations/Version20250321141740.php b/migrations/Version20250321141740.php index 34eca6526..cdf0e17aa 100644 --- a/migrations/Version20250321141740.php +++ b/migrations/Version20250321141740.php @@ -53,6 +53,7 @@ public function sqLiteUp(Schema $schema): void id_manufacturer, order_orderdetails_id, built_project_id, + built_assembly_id, datetime_added, name, last_modified, @@ -100,6 +101,7 @@ public function sqLiteUp(Schema $schema): void id_part_custom_state INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, + built_assembly_id INTEGER DEFAULT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, @@ -136,7 +138,8 @@ public function sqLiteUp(Schema $schema): void CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE ) SQL); @@ -236,6 +239,9 @@ public function sqLiteUp(Schema $schema): void $this->addSql(<<<'SQL' CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id) SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON "parts" (built_assembly_id) + SQL); $this->addSql(<<<'SQL' CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON parts (order_orderdetails_id) SQL); @@ -275,6 +281,7 @@ public function sqLiteDown(Schema $schema): void id_manufacturer, order_orderdetails_id, built_project_id, + built_assembly_id, datetime_added, name, last_modified, @@ -319,6 +326,7 @@ public function sqLiteDown(Schema $schema): void id_manufacturer INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, + built_assembly_id INTEGER DEFAULT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, @@ -354,7 +362,8 @@ public function sqLiteDown(Schema $schema): void CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, - CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE ) SQL); $this->addSql(<<<'SQL' @@ -367,6 +376,7 @@ public function sqLiteDown(Schema $schema): void id_manufacturer, order_orderdetails_id, built_project_id, + built_assembly_id, datetime_added, name, last_modified, @@ -405,6 +415,7 @@ public function sqLiteDown(Schema $schema): void id_manufacturer, order_orderdetails_id, built_project_id, + built_assembly_id, datetime_added, name, last_modified, @@ -464,6 +475,9 @@ public function sqLiteDown(Schema $schema): void $this->addSql(<<<'SQL' CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id) SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON "parts" (built_assembly_id) + SQL); $this->addSql(<<<'SQL' CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review) SQL); From 4845da7363bc85dc150155e463a287641bb8ceeb Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Fri, 18 Jul 2025 10:36:12 +0200 Subject: [PATCH 70/83] =?UTF-8?q?St=C3=BCcklisten=20beim=20L=C3=B6schen:?= =?UTF-8?q?=20Markieren=20von=20referenzierten=20Baugruppen=20als=20Hinwei?= =?UTF-8?q?s=20vornehmen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminPages/BaseAdminController.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index ddf19088c..a45b7ad39 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -24,6 +24,7 @@ use App\DataTables\LogDataTable; use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentUpload; @@ -455,6 +456,10 @@ protected function _delete(Request $request, AbstractNamedDBElement $entity, Str return $this->redirectToRoute($this->route_base.'_edit', ['id' => $entity->getID()]); } } else { + if ($entity instanceof Assembly) { + $this->markReferencedBomEntry($entity); + } + if ($entity instanceof AbstractStructuralDBElement) { $parent = $entity->getParent(); @@ -502,4 +507,16 @@ protected function _exportEntity(AbstractNamedDBElement $entity, EntityExporter return $exporter->exportEntityFromRequest($entity, $request); } + + private function markReferencedBomEntry(Assembly $referencedAssembly): void + { + $bomEntries = $this->entityManager->getRepository(AssemblyBOMEntry::class)->findBy(['referencedAssembly' => $referencedAssembly]); + + foreach ($bomEntries as $entry) { + $entry->setReferencedAssembly(null); + $entry->setName($referencedAssembly->getName(). ' DELETED'); + + $this->entityManager->persist($entry); + } + } } From bdb8ae08293a80bcd259e3a2dc9a94138b23f9f7 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 21 Jul 2025 10:04:54 +0200 Subject: [PATCH 71/83] Migration: Spaltenname korrigieren --- migrations/Version20250304154507.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/Version20250304154507.php b/migrations/Version20250304154507.php index 8a406b27d..bb25b8022 100644 --- a/migrations/Version20250304154507.php +++ b/migrations/Version20250304154507.php @@ -18,7 +18,7 @@ public function getDescription(): string public function mySQLUp(Schema $schema): void { $this->addSql(<<<'SQL' - ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL AFTER build_project_id + ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL AFTER built_project_id SQL); $this->addSql(<<<'SQL' ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) From f5f343558e8c4fc0108fcfe413bd5241fee381a0 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 22 Jul 2025 10:30:22 +0200 Subject: [PATCH 72/83] BOMImporter: Verbesserung des CSV-Parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Das Parsing wurde angepasst, um sowohl Komma- als auch Semikolon-getrennte CSV-Dateien zu unterstützen. Zudem werden Spaltennamen in Kleinbuchstaben konvertiert und zusätzliche Fallback-Logik für bestimmte Felder hinzugefügt. --- .../ImportExportSystem/BOMImporter.php | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index d1c5f939a..5332d5588 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -446,14 +446,27 @@ private function parseJson(string $data, array $options = [], string $objectType function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): ImporterResult { $result = new ImporterResult(); - $rows = explode("\n", trim($csvData)); - $headers = str_getcsv(array_shift($rows), ';'); + $rows = explode("\r\n", trim($csvData)); + $headers = str_getcsv(array_shift($rows), ','); + + if (count($headers) === 1 && isset($headers[0])) { + //If only one column was recognized, try fallback with semicolon as a separator + $headers = str_getcsv($headers[0], ';'); + } foreach ($rows as $key => $row) { $entry = []; - $values = str_getcsv($row, ';'); + $values = str_getcsv($row, ','); + + if (count($values) === 1 || count($values) !== count($headers)) { + //If only one column was recognized, try fallback with semicolon as a separator + $values = str_getcsv($row, ';'); + } foreach ($headers as $index => $column) { + //Change the column names in small letters + $column = strtolower($column); + //Convert column name into hierarchy $path = explode('_', $column); $temp = &$entry; @@ -506,6 +519,11 @@ function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): )); } + if (isset($entry['id']) && is_numeric($entry['id'])) { + //Use id column as a fallback for the expected part_id column + $entry['part']['id'] = (int) $entry['id']; + } + if (isset($entry['part'])) { $this->processPart($entry, $result, $key, $objectType, self::IMPORT_TYPE_CSV); } else { @@ -788,7 +806,12 @@ private function processPart(array $entry, ImporterResult $result, int $key, str $bomEntry->setQuantity((float) $entry['quantity']); if (isset($entry['name'])) { - $bomEntry->setName(trim($entry['name']) === '' ? null : trim ($entry['name'])); + $givenName = trim($entry['name']) === '' ? null : trim ($entry['name']); + + if ($givenName !== null && $bomEntry->getPart() !== null && $bomEntry->getPart()->getName() !== $givenName) { + //Apply different names for parts list entry + $bomEntry->setName(trim($entry['name']) === '' ? null : trim ($entry['name'])); + } } else { $bomEntry->setName(null); } From 5deaad2591afae36d67ee897b90dbe0c25ef6416 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 22 Jul 2025 12:20:26 +0200 Subject: [PATCH 73/83] Reihenfolge der Tabs in Baugruppenansicht korrigiert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Tabs "Details" und "Stückliste" wurden in der Baugruppenansicht vertauscht, um der Nutzererwartung besser zu entsprechen. Zudem wurde die Methode zur rekursiven Ermittlung referenzierter Baugruppen überarbeitet und in der Datentabelle integriert. --- src/DataTables/AssemblyDataTable.php | 5 ++-- src/Entity/AssemblySystem/Assembly.php | 29 +++++++++++++++++++----- templates/assemblies/info/info.html.twig | 24 ++++++++++---------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/DataTables/AssemblyDataTable.php b/src/DataTables/AssemblyDataTable.php index f3854ebca..745a62b62 100644 --- a/src/DataTables/AssemblyDataTable.php +++ b/src/DataTables/AssemblyDataTable.php @@ -109,14 +109,13 @@ public function configure(DataTable $dataTable, array $options): void $this->csh->add('referencedAssemblies', TextColumn::class, [ 'label' => $this->translator->trans('assembly.referencedAssembly.labelp'), 'render' => function ($value, Assembly $context): string { - $assemblies = $context->getReferencedAssemblies(); + $assemblies = $context->getAllReferencedAssembliesRecursive($context); $max = 5; $tmp = ""; for ($i = 0; $i < min($max, count($assemblies)); $i++) { - $url = $this->urlGenerator->infoURL($assemblies[$i]); - $tmp .= sprintf('%s', $url, htmlspecialchars($assemblies[$i]->getName())); + $tmp .= $this->assemblyDataTableHelper->renderName($assemblies[$i]); if ($i < count($assemblies) - 1) { $tmp .= ", "; } diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 5ce060d8e..cb6e94def 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -393,20 +393,37 @@ public function validate(ExecutionContextInterface $context, $payload): void } /** - * Get all referenced assemblies which uses this assembly. + * Get all assemblies and sub-assemblies recursive that are referenced in the assembly bom entries. * - * @return Assembly[] all referenced assemblies which uses this assembly as a one-dimensional array of assembly objects + * @param Assembly $assembly Assembly, which is to be processed recursively. + * @param array $processedAssemblies (optional) a list of the already edited assemblies to avoid circulatory references. + * @return Assembly[] A flat list of all recursively found assemblies. */ - public function getReferencedAssemblies(): array + public function getAllReferencedAssembliesRecursive(Assembly $assembly, array &$processedAssemblies = []): array { $assemblies = []; - foreach($this->bom_entries as $entry) { - if ($entry->getReferencedAssembly() !== null) { - $assemblies[] = $entry->getReferencedAssembly(); + // Avoid circular references + if (in_array($assembly, $processedAssemblies, true)) { + return $assemblies; + } + + // Add the current assembly to the processed + $processedAssemblies[] = $assembly; + + // Iterate by the bom entries of the current assembly + foreach ($assembly->getBomEntries() as $bomEntry) { + if ($bomEntry->getReferencedAssembly() !== null) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + $assemblies[] = $referencedAssembly; + + // Continue recursively to process sub-assemblies + $assemblies = array_merge($assemblies, $this->getAllReferencedAssembliesRecursive($referencedAssembly, $processedAssemblies)); } } return $assemblies; } + } diff --git a/templates/assemblies/info/info.html.twig b/templates/assemblies/info/info.html.twig index d787ea086..098173861 100644 --- a/templates/assemblies/info/info.html.twig +++ b/templates/assemblies/info/info.html.twig @@ -64,18 +64,18 @@ {% block card_content %}
      -
      - {% include "assemblies/info/_part.html.twig" %} -
      {% include "assemblies/info/_info.html.twig" %}
      +
      + {% include "assemblies/info/_part.html.twig" %} +
      {% include "assemblies/info/_builds.html.twig" %}
      From 6ed084f9131a4fdf793ee3af214312318f1e2cb6 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 22 Jul 2025 15:52:57 +0200 Subject: [PATCH 74/83] BOMImporter und AssemblyCycleValidator: Verbesserte Import-Logik MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Importmethoden wurden vereinheitlicht, um Projekte und Baugruppen gleichermaßen zu unterstützen. Zudem wurde die Validierung optimiert und die CSV-, JSON- und KiCAD-Parsing-Methoden angepasst, um kontextbasierte BOM-Einträge effizienter zu erstellen. --- .../ImportExportSystem/BOMImporter.php | 180 ++++++++++++------ .../AssemblySystem/AssemblyCycleValidator.php | 3 +- 2 files changed, 123 insertions(+), 60 deletions(-) diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 5332d5588..f022fb5e4 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -114,7 +114,7 @@ protected function configureOptions(OptionsResolver $resolver): OptionsResolver */ public function importFileIntoProject(UploadedFile $file, Project $project, array $options): ImporterResult { - $importerResult = $this->fileToImporterResult($file, $options); + $importerResult = $this->fileToImporterResult($project, $file, $options); if ($importerResult->getViolations()->count() === 0) { //Assign the bom_entries to the project @@ -127,12 +127,21 @@ public function importFileIntoProject(UploadedFile $file, Project $project, arra } /** - * Converts the given file into an ImporterResult with an array of BOM entries using the given options and save them into the given assembly. - * The changes are not saved into the database yet. + * Imports a file into an Assembly object and processes its contents. + * + * This method converts the provided file into an ImporterResult object that contains BOM entries and potential + * validation violations. If no violations are found, the BOM entries extracted from the file are added to the + * provided Assembly object. + * + * @param UploadedFile $file The file to be imported and processed. + * @param Assembly $assembly The target Assembly object to which the BOM entries are added. + * @param array $options Options or configurations related to the import process. + * + * @return ImporterResult An object containing the result of the import process, including BOM entries and any violations. */ public function importFileIntoAssembly(UploadedFile $file, Assembly $assembly, array $options): ImporterResult { - $importerResult = $this->fileToImporterResult($file, $options, AssemblyBOMEntry::class); + $importerResult = $this->fileToImporterResult($assembly, $file, $options); if ($importerResult->getViolations()->count() === 0) { //Assign the bom_entries to the assembly @@ -145,12 +154,20 @@ public function importFileIntoAssembly(UploadedFile $file, Assembly $assembly, a } /** - * Converts the given file into an array of BOM entries using the given options. - * @return ProjectBOMEntry[]|AssemblyBOMEntry[] + * Converts the content of a file into an array of BOM (Bill of Materials) entries. + * + * This method processes the content of the provided file and delegates the conversion + * to a helper method that generates BOM entries based on the provided import object and options. + * + * @param Project|Assembly $importObject The object determining the context of the BOM entries (either a Project or Assembly). + * @param File $file The file whose content will be converted into BOM entries. + * @param array $options Additional options or configurations to be applied during the conversion process. + * + * @return array An array of BOM entries created from the file content. */ - public function fileToBOMEntries(File $file, array $options, string $objectType = ProjectBOMEntry::class): array + public function fileToBOMEntries(Project|Assembly $importObject, File $file, array $options): array { - return $this->stringToBOMEntries($file->getContent(), $options, $objectType); + return $this->stringToBOMEntries($importObject, $file->getContent(), $options); } /** @@ -171,9 +188,19 @@ public function validateBOMData(string $data, array $options): array } /** - * Converts the given file into an ImporterResult with an array of BOM entries using the given options. + * Handles the conversion of an uploaded file into an ImporterResult for a given project or assembly. + * + * This method processes the uploaded file by validating its file extension based on the provided import type + * options and then proceeds to convert the file content into an ImporterResult. If the file extension is + * invalid or unsupported, the result will contain a corresponding violation. + * + * @param Project|Assembly $importObject The context of the import operation (either a Project or Assembly). + * @param UploadedFile $file The uploaded file to be processed. + * @param array $options An array of options, expected to include an 'type' key to determine valid file types. + * + * @return ImporterResult An object containing the results of the import process, including any detected violations. */ - public function fileToImporterResult(UploadedFile $file, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult + public function fileToImporterResult(Project|Assembly $importObject, UploadedFile $file, array $options): ImporterResult { $result = new ImporterResult(); @@ -203,7 +230,7 @@ public function fileToImporterResult(UploadedFile $file, array $options, string $fileExtension, [ '%extension%' => $fileExtension, - '%importType%' => $this->translator->trans($objectType === ProjectBOMEntry::class ? 'project.bom_import.type.'.$options['type'] : 'assembly.bom_import.type.'.$options['type']), + '%importType%' => $this->translator->trans($importObject instanceof Project ? 'project.bom_import.type.'.$options['type'] : 'assembly.bom_import.type.'.$options['type']), '%allowedExtensions%' => implode(', ', $validExtensions), ] )); @@ -211,16 +238,19 @@ public function fileToImporterResult(UploadedFile $file, array $options, string return $result; } - return $this->stringToImporterResult($file->getContent(), $options, $objectType); + return $this->stringToImporterResult($importObject, $file->getContent(), $options); } /** * Import string data into an array of BOM entries, which are not yet assigned to a project. - * @param string $data The data to import - * @param array $options An array of options + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $data The data to import + * @param array $options An array of options + * * @return ProjectBOMEntry[]|AssemblyBOMEntry[] An array of imported entries */ - public function stringToBOMEntries(string $data, array $options, string $objectType = ProjectBOMEntry::class): array + public function stringToBOMEntries(Project|Assembly $importObject, string $data, array $options): array { $resolver = new OptionsResolver(); $resolver = $this->configureOptions($resolver); @@ -234,11 +264,14 @@ public function stringToBOMEntries(string $data, array $options, string $objectT /** * Import string data into an array of BOM entries, which are not yet assigned to a project. - * @param string $data The data to import - * @param array $options An array of options + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $data The data to import + * @param array $options An array of options + * * @return ImporterResult An result of imported entries or a violation list */ - public function stringToImporterResult(string $data, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult + public function stringToImporterResult(Project|Assembly $importObject, string $data, array $options): ImporterResult { $resolver = new OptionsResolver(); $resolver = $this->configureOptions($resolver); @@ -251,14 +284,28 @@ public function stringToImporterResult(string $data, array $options, string $obj )); return match ($options['type']) { - self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $objectType), - self::IMPORT_TYPE_JSON => $this->parseJson($data, $objectType), - self::IMPORT_TYPE_CSV => $this->parseCsv($data, $objectType), + self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $importObject), + self::IMPORT_TYPE_JSON => $this->parseJson($data, $importObject), + self::IMPORT_TYPE_CSV => $this->parseCsv($data, $importObject), default => $defaultImporterResult, }; } - private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntry::class): ImporterResult + /** + * Parses a KiCAD PCB file and imports its BOM (Bill of Materials) entries into the given Project or Assembly context. + * + * This method processes a semicolon-delimited CSV data string, normalizes column names, + * validates the required fields, and creates BOM entries for each record in the data. + * The BOM entries are added to the provided Project or Assembly, depending on the context. + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $data The semicolon- or comma-delimited CSV data to be parsed + * + * @return ImporterResult The result of the import process, containing the created BOM entries. + * + * @throws UnexpectedValueException If required fields are missing in the provided data. + */ + private function parseKiCADPCB(string $data, Project|Assembly $importObject): ImporterResult { $result = new ImporterResult(); @@ -284,8 +331,8 @@ private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntr throw new \UnexpectedValueException('Quantity missing at line ' . ($offset + 1) . '!'); } - $bom_entry = $objectType === ProjectBOMEntry::class ? new ProjectBOMEntry() : new AssemblyBOMEntry(); - if ($objectType === ProjectBOMEntry::class) { + $bom_entry = $importObject instanceof Project ? new ProjectBOMEntry() : new AssemblyBOMEntry(); + if ($bom_entry instanceof ProjectBOMEntry) { $bom_entry->setName($entry['Designation'] . ' (' . $entry['Package'] . ')'); } else { $bom_entry->setName($entry['Designation']); @@ -383,15 +430,15 @@ private function validateKiCADSchematicData(string $data, array $options): array * - Checking for empty or invalid descriptions. * - Ensuring manufacturers, if specified, have valid `name` or `id` values. * - * @param string $data JSON encoded string containing BOM entries data. - * @param string $objectType The type of entries expected during import (e.g., `ProjectBOMEntry` or `AssemblyBOMEntry`). + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $data JSON encoded string containing BOM entries data. * * @return ImporterResult The result containing parsed data and any violations encountered during the parsing process. */ - private function parseJson(string $data, array $options = [], string $objectType = ProjectBOMEntry::class): ImporterResult + private function parseJson(Project|Assembly $importObject, string $data): ImporterResult { $result = new ImporterResult(); - $this->jsonRoot = 'JSON Import for '.$objectType === ProjectBOMEntry::class ? 'Project' : 'Assembly'; + $this->jsonRoot = 'JSON Import for '.($importObject instanceof Project ? 'Project' : 'Assembly'); $data = json_decode($data, true); @@ -420,9 +467,9 @@ private function parseJson(string $data, array $options = [], string $objectType } if (isset($entry['part'])) { - $this->processPart($entry, $result, $key, $objectType,self::IMPORT_TYPE_JSON); + $this->processPart($importObject, $entry, $result, $key, self::IMPORT_TYPE_JSON); } else { - $bomEntry = $this->getOrCreateBomEntry($objectType, $entry['name'] ?? null); + $bomEntry = $this->getOrCreateBomEntry($importObject, $entry['name'] ?? null); $bomEntry->setQuantity((float) $entry['quantity']); $result->addBomEntry($bomEntry); @@ -438,16 +485,16 @@ private function parseJson(string $data, array $options = [], string $objectType * performing validations and converting data based on the provided headers. * Handles potential violations and manages the creation of BOM entries based on the given type. * - * @param string $csvData The raw CSV data to parse, with rows separated by newlines. - * @param string $objectType The class type to instantiate for BOM entries, defaults to ProjectBOMEntry. + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $csvData The raw CSV data to parse, with rows separated by newlines. * * @return ImporterResult Returns an ImporterResult instance containing BOM entries and any validation violations encountered. */ - function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): ImporterResult + function parseCsv(Project|Assembly $importObject, string $csvData): ImporterResult { $result = new ImporterResult(); $rows = explode("\r\n", trim($csvData)); - $headers = str_getcsv(array_shift($rows), ','); + $headers = str_getcsv(array_shift($rows)); if (count($headers) === 1 && isset($headers[0])) { //If only one column was recognized, try fallback with semicolon as a separator @@ -456,7 +503,7 @@ function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): foreach ($rows as $key => $row) { $entry = []; - $values = str_getcsv($row, ','); + $values = str_getcsv($row); if (count($values) === 1 || count($values) !== count($headers)) { //If only one column was recognized, try fallback with semicolon as a separator @@ -484,7 +531,7 @@ function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): //Check whether the value is numerical if (is_numeric($values[$index])) { //Convert to integer or float - $temp = (strpos($values[$index], '.') !== false) + $temp = (str_contains($values[$index], '.')) ? floatval($values[$index]) : intval($values[$index]); } else { @@ -525,9 +572,9 @@ function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): } if (isset($entry['part'])) { - $this->processPart($entry, $result, $key, $objectType, self::IMPORT_TYPE_CSV); + $this->processPart($importObject, $entry, $result, $key, self::IMPORT_TYPE_CSV); } else { - $bomEntry = $this->getOrCreateBomEntry($objectType, $entry['name'] ?? null); + $bomEntry = $this->getOrCreateBomEntry($importObject, $entry['name'] ?? null); $bomEntry->setQuantity((float) $entry['quantity'] ?? 0); $result->addBomEntry($bomEntry); @@ -544,15 +591,15 @@ function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): * to identify corresponding objects in the database. The result is recorded, and violations are * logged if issues or discrepancies exist in the validation or database matching process. * - * @param array $entry The array representation of the part entry. - * @param ImporterResult $result The result object used for recording validation violations. - * @param int $key The index of the entry in the data array. - * @param string $objectType The type of object being processed. - * @param string $importType The type of import being performed. + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param array $entry The array representation of the part entry. + * @param ImporterResult $result The result object used for recording validation violations. + * @param int $key The index of the entry in the data array. + * @param string $importType The type of import being performed. * * @return void */ - private function processPart(array $entry, ImporterResult $result, int $key, string $objectType, string $importType): void + private function processPart(Project|Assembly $importObject, array $entry, ImporterResult $result, int $key, string $importType): void { $prefix = $importType === self::IMPORT_TYPE_JSON ? 'entry' : 'row'; @@ -777,12 +824,12 @@ private function processPart(array $entry, ImporterResult $result, int $key, str $part->setCategory($category); } - if ($objectType === AssemblyBOMEntry::class) { - $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['part' => $part]); + if ($importObject instanceof Assembly) { + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['assembly' => $importObject, 'part' => $part]); if ($bomEntry === null) { if (isset($entry['name']) && $entry['name'] !== '') { - $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $entry['name']]); + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['assembly' => $importObject, 'name' => $entry['name']]); } if ($bomEntry === null) { @@ -790,11 +837,11 @@ private function processPart(array $entry, ImporterResult $result, int $key, str } } } else { - $bomEntry = $this->projectBOMEntryRepository->findOneBy(['part' => $part]); + $bomEntry = $this->projectBOMEntryRepository->findOneBy(['project' => $importObject, 'part' => $part]); if ($bomEntry === null) { if (isset($entry['name']) && $entry['name'] !== '') { - $bomEntry = $this->projectBOMEntryRepository->findOneBy(['name' => $entry['name']]); + $bomEntry = $this->projectBOMEntryRepository->findOneBy(['project' => $importObject, 'name' => $entry['name']]); } if ($bomEntry === null) { @@ -841,22 +888,38 @@ private function removeEmptyProperties(array $data): array return $data; } - private function getOrCreateBomEntry(string $objectType, ?string $name) + /** + * Retrieves an existing BOM (Bill of Materials) entry by name or creates a new one if not found. + * + * Depending on whether the provided import object is a Project or Assembly, this method attempts to locate + * a corresponding BOM entry in the appropriate repository. If no entry is located, a new BOM entry object + * is instantiated according to the type of the import object. + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string|null $name The name of the BOM entry to search for or assign to a new entry. + * + * @return ProjectBOMEntry|AssemblyBOMEntry An existing or newly created BOM entry. + */ + private function getOrCreateBomEntry(Project|Assembly $importObject, ?string $name): ProjectBOMEntry|AssemblyBOMEntry { $bomEntry = null; //Check whether there is a name if (!empty($name)) { - if ($objectType === ProjectBOMEntry::class) { + if ($importObject instanceof Project) { $bomEntry = $this->projectBOMEntryRepository->findOneBy(['name' => $name]); - } elseif ($objectType === AssemblyBOMEntry::class) { + } else { $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $name]); } } - //If no bom enttry was found, a new object create + //If no bom entry was found, a new object create if ($bomEntry === null) { - $bomEntry = new $objectType(); + if ($importObject instanceof Project) { + $bomEntry = new ProjectBOMEntry(); + } else { + $bomEntry = new AssemblyBOMEntry(); + } } $bomEntry->setName($name); @@ -887,7 +950,6 @@ private function normalizeColumnNames(array $entry): array return $out; } - /** * Builds a JSON-based constraint violation. * @@ -895,10 +957,10 @@ private function normalizeColumnNames(array $entry): array * The violation includes a message, property path, invalid value, and other contextual information. * Translations for the violation message can be applied through the translator service. * - * @param string $message The translation key for the validation message. - * @param string $propertyPath The property path where the violation occurred. - * @param mixed|null $invalidValue The value that caused the violation (optional). - * @param array $parameters Additional parameters for message placeholders (default is an empty array). + * @param string $message The translation key for the validation message. + * @param string $propertyPath The property path where the violation occurred. + * @param mixed|null $invalidValue The value that caused the violation (optional). + * @param array $parameters Additional parameters for message placeholders (default is an empty array). * * @return ConstraintViolation The created constraint violation object. */ diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php index 3483f94a1..73df284dd 100644 --- a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php +++ b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php @@ -23,6 +23,7 @@ namespace App\Validator\Constraints\AssemblySystem; use App\Entity\AssemblySystem\Assembly; +use Symfony\Component\Form\Form; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; @@ -51,7 +52,7 @@ public function validate($value, Constraint $constraint): void $bomEntries = $value->getBomEntries()->toArray(); // Consider additional entries from the form - if ($this->context->getRoot()->has('bom_entries')) { + if ($this->context->getRoot() instanceof Form && $this->context->getRoot()->has('bom_entries')) { $formBomEntries = $this->context->getRoot()->get('bom_entries')->getData(); if ($formBomEntries) { $given = is_array($formBomEntries) ? $formBomEntries : iterator_to_array($formBomEntries); From 31b1829761268388756ee43dc762613a98e21d59 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 24 Jul 2025 09:11:28 +0200 Subject: [PATCH 75/83] =?UTF-8?q?BOMValidator:=20Validierung=20f=C3=BCr=20?= =?UTF-8?q?rekursive=20Baugruppen-Eintragspr=C3=BCfung=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Es wurde eine neue Validierung hinzugefügt, um sicherzustellen, dass keine Baugruppe in ihrer eigenen Hierarchie als Unterbaugruppe referenziert wird. Diese Logik wurde in die entsprechenden Dateien integriert und unterstützt Mehrsprachigkeit durch neue Übersetzungen. --- src/Entity/AssemblySystem/Assembly.php | 6 +- .../AssemblySystem/AssemblyBOMEntry.php | 2 + .../AssemblySystem/AssemblyCycleValidator.php | 67 +++++--- .../AssemblyInvalidBomEntry.php | 21 +++ .../AssemblyInvalidBomEntryValidator.php | 155 ++++++++++++++++++ translations/validators.cs.xlf | 6 + translations/validators.da.xlf | 6 + translations/validators.de.xlf | 6 + translations/validators.el.xlf | 6 + translations/validators.en.xlf | 6 + translations/validators.fr.xlf | 6 + translations/validators.hr.xlf | 6 + translations/validators.it.xlf | 6 + translations/validators.ja.xlf | 6 + translations/validators.pl.xlf | 6 + translations/validators.ru.xlf | 6 + translations/validators.zh.xlf | 6 + 17 files changed, 302 insertions(+), 21 deletions(-) create mode 100644 src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php create mode 100644 src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntryValidator.php diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index cb6e94def..c65ca71fe 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -24,6 +24,7 @@ use App\Repository\AssemblyRepository; use App\Validator\Constraints\AssemblySystem\AssemblyCycle; +use App\Validator\Constraints\AssemblySystem\AssemblyInvalidBomEntry; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; @@ -111,11 +112,12 @@ class Assembly extends AbstractStructuralDBElement * @var Collection */ #[Assert\Valid] + #[AssemblyCycle] + #[AssemblyInvalidBomEntry] + #[UniqueReferencedAssembly] #[Groups(['extended', 'full', 'import'])] #[ORM\OneToMany(targetEntity: AssemblyBOMEntry::class, mappedBy: 'assembly', cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])] - #[AssemblyCycle] - #[UniqueReferencedAssembly] #[UniqueObjectCollection(message: 'assembly.bom_entry.project_already_in_bom', fields: ['project'])] #[UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name'])] protected Collection $bom_entries; diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 8a4cdbc24..7d54fe684 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -39,6 +39,7 @@ use App\Entity\ProjectSystem\Project; use App\Repository\DBElementRepository; use App\Validator\Constraints\AssemblySystem\AssemblyCycle; +use App\Validator\Constraints\AssemblySystem\AssemblyInvalidBomEntry; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; @@ -142,6 +143,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt message: 'validator.assembly.bom_entry.only_part_or_assembly_allowed' )] #[AssemblyCycle] + #[AssemblyInvalidBomEntry] #[ORM\ManyToOne(targetEntity: Assembly::class)] #[ORM\JoinColumn(name: 'id_referenced_assembly', nullable: true, onDelete: 'SET NULL')] #[Groups(['bom_entry:read', 'bom_entry:write', ])] diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php index 73df284dd..f12f19a73 100644 --- a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php +++ b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php @@ -49,49 +49,78 @@ public function validate($value, Constraint $constraint): void return; } - $bomEntries = $value->getBomEntries()->toArray(); + $availableViolations = $this->context->getViolations(); + if (count($availableViolations) > 0) { + //already violations given, currently no more needed to check + + return; + } + + $bomEntries = []; - // Consider additional entries from the form if ($this->context->getRoot() instanceof Form && $this->context->getRoot()->has('bom_entries')) { - $formBomEntries = $this->context->getRoot()->get('bom_entries')->getData(); - if ($formBomEntries) { - $given = is_array($formBomEntries) ? $formBomEntries : iterator_to_array($formBomEntries); - foreach ($given as $givenIdx => $entry) { - if (in_array($entry, $bomEntries, true)) { - continue; - } else { - $bomEntries[$givenIdx] = $entry; - } - } + $bomEntries = $this->context->getRoot()->get('bom_entries')->getData(); + $bomEntries = is_array($bomEntries) ? $bomEntries : iterator_to_array($bomEntries); + } elseif ($this->context->getRoot() instanceof Assembly) { + $bomEntries = $value->getBomEntries()->toArray(); + } + + $relevantEntries = []; + + foreach ($bomEntries as $bomEntry) { + if ($bomEntry->getReferencedAssembly() !== null) { + $relevantEntries[$bomEntry->getId()] = $bomEntry; } } $visitedAssemblies = []; - foreach ($bomEntries as $bomEntry) { + foreach ($relevantEntries as $bomEntry) { if ($this->hasCycle($bomEntry->getReferencedAssembly(), $value, $visitedAssemblies)) { $this->addViolation($value, $constraint); } } } - private function hasCycle(?Assembly $currentAssembly, Assembly $originalAssembly, array &$visitedAssemblies): bool + /** + * Determines if there is a cyclic dependency in the assembly hierarchy. + * + * This method checks if a cycle exists in the hierarchy of referenced assemblies starting + * from a given assembly. It traverses through the Bill of Materials (BOM) entries of each + * assembly recursively and keeps track of visited assemblies to detect cycles. + * + * @param Assembly|null $currentAssembly The current assembly being checked for cycles. + * @param Assembly $originalAssembly The original assembly from where the cycle detection started. + * @param Assembly[] $visitedAssemblies A list of assemblies that have been visited during the current traversal. + * + * @return bool True if a cycle is detected, false otherwise. + */ + private function hasCycle(?Assembly $currentAssembly, Assembly $originalAssembly, array $visitedAssemblies = []): bool { + //No referenced assembly → no cycle if ($currentAssembly === null) { return false; } - if (in_array($currentAssembly, $visitedAssemblies, true)) { + //If the assembly has already been visited, there is a cycle + if (in_array($currentAssembly->getId(), array_map(fn($a) => $a->getId(), $visitedAssemblies), true)) { return true; } + //Add the current assembly to the visited $visitedAssemblies[] = $currentAssembly; + //Go through the bom entries of the current assembly foreach ($currentAssembly->getBomEntries() as $bomEntry) { - if ($this->hasCycle($bomEntry->getReferencedAssembly(), $originalAssembly, $visitedAssemblies)) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + if ($referencedAssembly !== null && $this->hasCycle($referencedAssembly, $originalAssembly, $visitedAssemblies)) { return true; } } + //Remove the current assembly from the list of visit (recursion completed) + array_pop($visitedAssemblies); + return false; } @@ -102,11 +131,11 @@ private function hasCycle(?Assembly $currentAssembly, Assembly $originalAssembly * already exists in the context. If such a violation is found, the current violation is not added again. * The process involves reflection to access private or protected properties of violation objects. * - * @param mixed $value The value that triggered the violation. - * @param Constraint $constraint The constraint containing the validation details. + * @param mixed $value The value that triggered the violation. + * @param Constraint $constraint The constraint containing the validation details. * */ - private function addViolation($value, Constraint $constraint): void + private function addViolation(mixed $value, Constraint $constraint): void { /** @var ConstraintViolationBuilder $buildViolation */ $buildViolation = $this->context->buildViolation($constraint->message) diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php b/src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php new file mode 100644 index 000000000..73234c86e --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php @@ -0,0 +1,21 @@ +context->getViolations(); + if (count($availableViolations) > 0) { + //already violations given, currently no more needed to check + + return; + } + + $bomEntries = []; + + if ($this->context->getRoot() instanceof Form && $this->context->getRoot()->has('bom_entries')) { + $bomEntries = $this->context->getRoot()->get('bom_entries')->getData(); + $bomEntries = is_array($bomEntries) ? $bomEntries : iterator_to_array($bomEntries); + } elseif ($this->context->getRoot() instanceof Assembly) { + $bomEntries = $value->getBomEntries()->toArray(); + } + + $relevantEntries = []; + + foreach ($bomEntries as $bomEntry) { + if ($bomEntry->getReferencedAssembly() !== null) { + $relevantEntries[$bomEntry->getId()] = $bomEntry; + } + } + + foreach ($relevantEntries as $bomEntry) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + if ($bomEntry->getAssembly()->getParent()?->getId() === $referencedAssembly->getParent()?->getId()) { + //Save on the same assembly level + continue; + } elseif ($this->isInvalidBomEntry($referencedAssembly, $bomEntry->getAssembly())) { + $this->addViolation($value, $constraint); + } + } + } + + /** + * Determines whether a Bill of Materials (BOM) entry is invalid based on the relationship + * between the current assembly and the parent assembly. + * + * @param Assembly|null $currentAssembly The current assembly being analyzed. Null indicates no assembly is referenced. + * @param Assembly $parentAssembly The parent assembly to check against the current assembly. + * + * @return bool Returns + */ + private function isInvalidBomEntry(?Assembly $currentAssembly, Assembly $parentAssembly): bool + { + //No assembly referenced -> no problems + if ($currentAssembly === null) { + return false; + } + + //Check: is the current assembly a descendant of the parent assembly? + if ($currentAssembly->isChildOf($parentAssembly)) { + return true; + } + + //Recursive check: Analyze the current assembly list + foreach ($currentAssembly->getBomEntries() as $bomEntry) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + if ($this->isInvalidBomEntry($referencedAssembly, $parentAssembly)) { + return true; + } + } + + return false; + + } + + private function isOnSameLevel(Assembly $assembly1, Assembly $assembly2): bool + { + $parent1 = $assembly1->getParent(); + $parent2 = $assembly2->getParent(); + + if ($parent1 === null || $parent2 === null) { + return false; + } + + // Beide Assemblies teilen denselben Parent + return $parent1 !== null && $parent2 !== null && $parent1->getId() === $parent2->getId(); + } + + /** + * Adds a violation to the current context if it hasn’t already been added. + * + * This method checks whether a violation with the same property path as the current violation + * already exists in the context. If such a violation is found, the current violation is not added again. + * The process involves reflection to access private or protected properties of violation objects. + * + * @param mixed $value The value that triggered the violation. + * @param Constraint $constraint The constraint containing the validation details. + * + */ + private function addViolation($value, Constraint $constraint): void + { + /** @var ConstraintViolationBuilder $buildViolation */ + $buildViolation = $this->context->buildViolation($constraint->message) + ->setParameter('%name%', $value->getName()); + + $alreadyAdded = false; + + try { + $reflectionClass = new ReflectionClass($buildViolation); + $property = $reflectionClass->getProperty('propertyPath'); + $propertyPath = $property->getValue($buildViolation); + + $availableViolations = $this->context->getViolations(); + + foreach ($availableViolations as $tmpViolation) { + $tmpReflectionClass = new ReflectionClass($tmpViolation); + $tmpProperty = $tmpReflectionClass->getProperty('propertyPath'); + $tmpPropertyPath = $tmpProperty->getValue($tmpViolation); + + if ($tmpPropertyPath === $propertyPath) { + $alreadyAdded = true; + } + } + } catch (\ReflectionException) { + } + + if (!$alreadyAdded) { + $buildViolation->addViolation(); + } + } +} \ No newline at end of file diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 7ee171b26..245e61e3b 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -401,6 +401,12 @@ Byl zjištěn cyklus: Sestava "%name%" nepřímo odkazuje sama na sebe. + + + assembly.bom_entry.invalid_child_entry + Sestava nesmí ve svém seznamu materiálů (BOM) odkazovat na podskupinu, která je součástí její vlastní hierarchie. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 24fa330af..f30dd2116 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -377,6 +377,12 @@ En cyklus blev opdaget: Samlingen "%name%" refererer indirekte til sig selv. + + + assembly.bom_entry.invalid_child_entry + En samling må ikke referere til en undergruppe fra sin egen hierarki i BOM-listerne. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index fa84354b9..645d72ed1 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -401,6 +401,12 @@ Ein Zyklus wurde entdeckt: Die Baugruppe "%name%" referenziert sich indirekt selbst. + + + assembly.bom_entry.invalid_child_entry + Eine Baugruppe darf keine Unterbaugruppe aus seiner eigenen Hierarchie in den BOM-Einträgen referenzieren. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index bc9b0947e..9464f2886 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -43,6 +43,12 @@ Εντοπίστηκε κύκλος: Η συναρμολόγηση "%name%" αναφέρεται έμμεσα στον εαυτό της. + + + assembly.bom_entry.invalid_child_entry + Μία συναρμολόγηση δεν πρέπει να αναφέρεται σε μία υποσυναρμολόγηση από την ίδια την ιεραρχία της στη λίστα BOM. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 7d9beb4ec..53ad4cde6 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -401,6 +401,12 @@ A cycle was detected: the assembly "%name%" indirectly references itself. + + + assembly.bom_entry.invalid_child_entry + An assembly must not reference a subassembly from its own hierarchy in the BOM entries. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index 1c9c53029..440468295 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -233,6 +233,12 @@ Un cycle a été détecté : L'assemblage "%name%" se réfère indirectement à lui-même. + + + assembly.bom_entry.invalid_child_entry + Un assemblage ne doit pas référencer un sous-assemblage de sa propre hiérarchie dans les entrées de la nomenclature (BOM). + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 89d470e7e..485cb0e2f 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -395,6 +395,12 @@ Otkriven je ciklus: Sklop "%name%" neizravno referencira samog sebe. + + + assembly.bom_entry.invalid_child_entry + Sklop ne smije referencirati podsklop iz vlastite hijerarhije u unosima BOM-a. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index e9b528bb7..74d3969f7 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -395,6 +395,12 @@ È stato rilevato un ciclo: L'assemblaggio "%name%" fa riferimento indirettamente a sé stesso. + + + assembly.bom_entry.invalid_child_entry + Un assemblaggio non deve fare riferimento a un sottoassemblaggio nella propria gerarchia nelle voci della distinta base (BOM). + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index 80ec65ff2..f9f8a54f8 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -233,6 +233,12 @@ 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + + assembly.bom_entry.invalid_child_entry + アセンブリは、BOMエントリで自身の階層内のサブアセンブリを参照してはいけません。 + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 5df01cd6a..9916178ca 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -395,6 +395,12 @@ 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + + assembly.bom_entry.invalid_child_entry + Zespół nie może odwoływać się do podzespołu w swojej własnej hierarchii w wpisach BOM. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 8bf08ab3c..b8029e47f 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -395,6 +395,12 @@ Обнаружен цикл: Сборка «%name%» косвенно ссылается на саму себя. + + + assembly.bom_entry.invalid_child_entry + Сборка не должна ссылаться на подсборку внутри своей собственной иерархии в записях спецификации (BOM). + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 87c507c19..6e4fc0568 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -383,6 +383,12 @@ 检测到循环:装配体“%name%”间接引用了其自身。 + + + assembly.bom_entry.invalid_child_entry + Сборка не должна ссылаться на подсборку внутри своей собственной иерархии в записях спецификации (BOM). + + assembly.bom_entry.project_already_in_bom From 412818a7ccdd797fd4949b0d7834adf4aa3f3fce Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 24 Jul 2025 10:31:19 +0200 Subject: [PATCH 76/83] Tabs und BOMImporter: Verbesserte Anzeige und Validierung Die Standardanzeige des Tabs "Details" wurde korrigiert. Im BOMImporter wurden nichtnumerische Spalten kategorisch ausgeschlossen und eine Validation-message angepasst. --- src/Services/ImportExportSystem/BOMImporter.php | 4 ++-- templates/assemblies/info/info.html.twig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index f022fb5e4..ddad05585 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -529,7 +529,7 @@ function parseCsv(Project|Assembly $importObject, string $csvData): ImporterResu //If there is no value, skip if (isset($values[$index]) && $values[$index] !== '') { //Check whether the value is numerical - if (is_numeric($values[$index])) { + if (is_numeric($values[$index]) && !in_array($column, ['name','description','manufacturer','designator'])) { //Convert to integer or float $temp = (str_contains($values[$index], '.')) ? floatval($values[$index]) @@ -560,7 +560,7 @@ function parseCsv(Project|Assembly $importObject, string $csvData): ImporterResu if (isset($entry['name']) && !is_string($entry['name'])) { $result->addViolation($this->buildJsonViolation( - 'validator.bom_importer.csv.parameter.string.notEmpty', + 'validator.bom_importer.json_csv.parameter.string.notEmpty', "row[$key].name", $entry['name'] )); diff --git a/templates/assemblies/info/info.html.twig b/templates/assemblies/info/info.html.twig index 098173861..5a419e364 100644 --- a/templates/assemblies/info/info.html.twig +++ b/templates/assemblies/info/info.html.twig @@ -98,10 +98,10 @@
      -
      +
      {% include "assemblies/info/_info.html.twig" %}
      -
      +
      {% include "assemblies/info/_part.html.twig" %}
      From e21ed95bfd3c7598cf39ab98443638a2589c7ba6 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 28 Jul 2025 10:33:30 +0200 Subject: [PATCH 77/83] =?UTF-8?q?BOMImporter=20und=20AssemblyBomEntriesDat?= =?UTF-8?q?aTable:=20Mountnames=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Verarbeitung der Designators im BOMImporter wurde ergänzt, um Mountnames zu setzen. Zudem wurden neue Spalten wie Kategorie, Footprint, Hersteller und Mountnames in der AssemblyBomEntriesDataTable hinzugefügt. Dies verbessert die Darstellung und Handhabung von Bauteildaten in der Stücklisten-Ansicht. --- .../AssemblyBomEntriesDataTable.php | 49 +++++++++++++++++++ .../ImportExportSystem/BOMImporter.php | 9 ++++ 2 files changed, 58 insertions(+) diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php index b2c3e118d..e506dccd2 100644 --- a/src/DataTables/AssemblyBomEntriesDataTable.php +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -22,6 +22,7 @@ */ namespace App\DataTables; +use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; use App\DataTables\Helpers\AssemblyDataTableHelper; @@ -146,6 +147,54 @@ public function configure(DataTable $dataTable, array $options): void return $context->getComment(); }, ]) + ->add('category', EntityColumn::class, [ + 'label' => $this->translator->trans('part.table.category'), + 'property' => 'part.category', + 'orderField' => 'NATSORT(category.name)', + ]) + ->add('footprint', EntityColumn::class, [ + 'property' => 'part.footprint', + 'label' => $this->translator->trans('part.table.footprint'), + 'orderField' => 'NATSORT(footprint.name)', + ]) + ->add('manufacturer', EntityColumn::class, [ + 'property' => 'part.manufacturer', + 'label' => $this->translator->trans('part.table.manufacturer'), + 'orderField' => 'NATSORT(manufacturer.name)', + ]) + ->add('mountnames', TextColumn::class, [ + 'label' => 'assembly.bom.mountnames', + 'render' => function ($value, AssemblyBOMEntry $context) { + $html = ''; + + foreach (explode(',', $context->getMountnames()) as $mountname) { + $html .= sprintf('%s ', htmlspecialchars($mountname)); + } + return $html; + }, + ]) + ->add('instockAmount', TextColumn::class, [ + 'label' => 'assembly.bom.instockAmount', + 'visible' => false, + 'render' => function ($value, AssemblyBOMEntry $context) { + if ($context->getPart() !== null) { + return $this->partDataTableHelper->renderAmount($context->getPart()); + } + + return ''; + } + ]) + ->add('storageLocations', TextColumn::class, [ + 'label' => 'part.table.storeLocations', + 'visible' => false, + 'render' => function ($value, AssemblyBOMEntry $context) { + if ($context->getPart() !== null) { + return $this->partDataTableHelper->renderStorageLocations($context->getPart()); + } + + return ''; + } + ]) ->add('addedDate', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.addedDate'), ]) diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index ddad05585..ecda3c5ea 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -575,6 +575,11 @@ function parseCsv(Project|Assembly $importObject, string $csvData): ImporterResu $this->processPart($importObject, $entry, $result, $key, self::IMPORT_TYPE_CSV); } else { $bomEntry = $this->getOrCreateBomEntry($importObject, $entry['name'] ?? null); + + if (isset($entry['designator'])) { + $bomEntry->setMountnames(trim($entry['designator']) === '' ? '' : trim($entry['designator'])); + } + $bomEntry->setQuantity((float) $entry['quantity'] ?? 0); $result->addBomEntry($bomEntry); @@ -863,6 +868,10 @@ private function processPart(Project|Assembly $importObject, array $entry, Impor $bomEntry->setName(null); } + if (isset($entry['designator'])) { + $bomEntry->setMountnames(trim($entry['designator']) === '' ? '' : trim($entry['designator'])); + } + $bomEntry->setPart($part); $result->addBomEntry($bomEntry); From 9e8eb93eb83ff61f87d50fea0f16365b578c859e Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 25 Aug 2025 10:43:58 +0200 Subject: [PATCH 78/83] =?UTF-8?q?IPN-Validierung=20f=C3=BCr=20Parts=20?= =?UTF-8?q?=C3=BCberarbeiten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Constraints/UniquePartIpnValidator.php | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Validator/Constraints/UniquePartIpnValidator.php b/src/Validator/Constraints/UniquePartIpnValidator.php index 51b5402b8..019202f8e 100644 --- a/src/Validator/Constraints/UniquePartIpnValidator.php +++ b/src/Validator/Constraints/UniquePartIpnValidator.php @@ -24,14 +24,27 @@ public function validate($value, Constraint $constraint) return; } + if (!$this->enforceUniqueIpn) { + return; + } + + /** @var Part $currentPart */ + $currentPart = $this->context->getObject(); + + if (!$currentPart instanceof Part) { + return; + } + $repository = $this->entityManager->getRepository(Part::class); - $existingPart = $repository->findOneBy(['ipn' => $value]); + $existingParts = $repository->findBy(['ipn' => $value]); - if ($existingPart) { - if ($this->enforceUniqueIpn) { - $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $value) - ->addViolation(); + foreach ($existingParts as $existingPart) { + if ($currentPart->getId() !== $existingPart->getId()) { + if ($this->enforceUniqueIpn) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $value) + ->addViolation(); + } } } } From b2f8e21d4e2b601ea095c9e9fe5afaaaf383a00d Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 8 Sep 2025 13:32:34 +0200 Subject: [PATCH 79/83] =?UTF-8?q?F=C3=BCge=20Option=20f=C3=BCr=20lesbares?= =?UTF-8?q?=20CSV=20beim=20Export=20hinzu=20(APS-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elements/toggle_visibility_controller.js | 52 ++++ .../AssemblySystem/AssemblyBOMEntry.php | 6 +- .../ImportExportSystem/EntityExporter.php | 291 +++++++++++++++++- templates/admin/_export_form.html.twig | 19 +- translations/messages.cs.xlf | 6 + translations/messages.da.xlf | 6 + translations/messages.de.xlf | 6 + translations/messages.el.xlf | 6 + translations/messages.en.xlf | 6 + translations/messages.es.xlf | 6 + translations/messages.fr.xlf | 10 +- translations/messages.it.xlf | 6 + translations/messages.ja.xlf | 6 + translations/messages.nl.xlf | 6 + translations/messages.pl.xlf | 6 + translations/messages.ru.xlf | 6 + translations/messages.zh.xlf | 6 + 17 files changed, 434 insertions(+), 16 deletions(-) create mode 100644 assets/controllers/elements/toggle_visibility_controller.js diff --git a/assets/controllers/elements/toggle_visibility_controller.js b/assets/controllers/elements/toggle_visibility_controller.js new file mode 100644 index 000000000..4600dfb29 --- /dev/null +++ b/assets/controllers/elements/toggle_visibility_controller.js @@ -0,0 +1,52 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + + static values = { + classes: Array + }; + + connect() { + this.readableCheckbox = this.element.querySelector("#readable"); + + if (!this.readableCheckbox) { + return; + } + + // Apply the initial visibility state based on the checkbox being checked or not + this.toggleContainers(this.readableCheckbox.checked); + + // Add a change event listener to the 'readable' checkbox + this.readableCheckbox.addEventListener("change", (event) => { + // Toggle container visibility when the checkbox value changes + this.toggleContainers(event.target.checked); + }); + } + + /** + * Toggles the visibility of containers based on the checkbox state. + * Hides specified containers if the checkbox is checked and shows them otherwise. + * + * @param {boolean} isChecked - The current state of the checkbox: + * true if checked (hide elements), false if unchecked (show them). + */ + toggleContainers(isChecked) { + if (!Array.isArray(this.classesValue) || this.classesValue.length === 0) { + return; + } + + this.classesValue.forEach((cssClass) => { + const elements = document.querySelectorAll(`.${cssClass}`); + + if (!elements.length) { + return; + } + + // Update the visibility for each selected element + elements.forEach((element) => { + // If the checkbox is checked, hide the container; otherwise, show it + element.style.display = isChecked ? "none" : ""; + }); + }); + } +} \ No newline at end of file diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 7d54fe684..9bca209d0 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -124,7 +124,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt */ #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'bom_entries')] #[ORM\JoinColumn(name: 'id_assembly', nullable: true)] - #[Groups(['bom_entry:read', 'bom_entry:write', ])] + #[Groups(['bom_entry:read', 'bom_entry:write'])] protected ?Assembly $assembly = null; /** @@ -146,7 +146,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt #[AssemblyInvalidBomEntry] #[ORM\ManyToOne(targetEntity: Assembly::class)] #[ORM\JoinColumn(name: 'id_referenced_assembly', nullable: true, onDelete: 'SET NULL')] - #[Groups(['bom_entry:read', 'bom_entry:write', ])] + #[Groups(['bom_entry:read', 'bom_entry:write'])] protected ?Assembly $referencedAssembly = null; /** @@ -158,7 +158,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt )] #[ORM\ManyToOne(targetEntity: Project::class)] #[ORM\JoinColumn(name: 'id_project', nullable: true)] - #[Groups(['bom_entry:read', 'bom_entry:write', ])] + #[Groups(['bom_entry:read', 'bom_entry:write'])] protected ?Project $project = null; /** diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 271642dad..1adbcdcbb 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -22,8 +22,19 @@ namespace App\Services\ImportExportSystem; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\LabelSystem\LabelProfile; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\StorageLocation; +use App\Entity\Parts\Supplier; +use App\Entity\PriceInformations\Currency; +use App\Entity\ProjectSystem\Project; use App\Helpers\FilenameSanatizer; use App\Serializer\APIPlatform\SkippableItemNormalizer; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -62,6 +73,9 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setDefault('include_children', false); $resolver->setAllowedTypes('include_children', 'bool'); + + $resolver->setDefault('readable', false); + $resolver->setAllowedTypes('readable', 'bool'); } /** @@ -144,15 +158,50 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities, $entities = [$entities]; } - //Do the serialization with the given options - $serialized_data = $this->exportEntities($entities, $options); + if ($request->get('readable', false)) { + // Map entity classes to export functions + $entityExportMap = [ + AttachmentType::class => fn($entities) => $this->exportReadable($entities, AttachmentType::class), + Category::class => fn($entities) => $this->exportReadable($entities, Category::class), + Project::class => fn($entities) => $this->exportReadable($entities, Project::class), + Assembly::class => fn($entities) => $this->exportReadable($entities, Assembly::class), + Supplier::class => fn($entities) => $this->exportReadable($entities, Supplier::class), + Manufacturer::class => fn($entities) => $this->exportReadable($entities, Manufacturer::class), + StorageLocation::class => fn($entities) => $this->exportReadable($entities, StorageLocation::class), + Footprint::class => fn($entities) => $this->exportReadable($entities, Footprint::class), + Currency::class => fn($entities) => $this->exportReadable($entities, Currency::class), + MeasurementUnit::class => fn($entities) => $this->exportReadable($entities, MeasurementUnit::class), + LabelProfile::class => fn($entities) => $this->exportReadable($entities, LabelProfile::class, false), + ]; + + // Determine the type of the entity + $type = null; + foreach ($entities as $entity) { + $entityClass = get_class($entity); + if (isset($entityExportMap[$entityClass])) { + $type = $entityClass; + break; + } + } + + // Generate the response + $response = isset($entityExportMap[$type]) + ? new Response($entityExportMap[$type]($entities)) + : new Response(''); + + $options['format'] = 'csv'; + $options['level'] = 'readable'; + } else { + //Do the serialization with the given options + $serialized_data = $this->exportEntities($entities, $options); - $response = new Response($serialized_data); + $response = new Response($serialized_data); - //Resolve the format - $optionsResolver = new OptionsResolver(); - $this->configureOptions($optionsResolver); - $options = $optionsResolver->resolve($options); + //Resolve the format + $optionsResolver = new OptionsResolver(); + $this->configureOptions($optionsResolver); + $options = $optionsResolver->resolve($options); + } //Determine the content type for the response @@ -203,4 +252,232 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities, return $response; } + + /** + * Exports data for multiple entity types in a readable CSV format. + * + * @param array $entities The entities to export. + * @param string $type The type of entities ('category', 'project', 'assembly', 'attachmentType', 'supplier'). + * @return string The generated CSV content as a string. + */ + public function exportReadable(array $entities, string $type, bool $isHierarchical = true): string + { + //Define headers and entity-specific processing logic + $defaultProcessEntity = fn($entity, $depth) => [ + 'Id' => $entity->getId(), + 'ParentId' => $entity->getParent()?->getId() ?? '', + 'NameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(), + 'Name' => $entity->getName(), + 'FullName' => $this->getFullName($entity), + ]; + + $config = [ + AttachmentType::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Category::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Project::class => [ + 'header' => [ + 'Id', 'ParentId', 'Type', 'ProjectNameHierarchical', 'ProjectName', 'ProjectFullName', + 'BomQuantity', 'BomPartId', 'BomPartIpn', 'BomPartName', 'BomName', 'BomPartDescription', 'BomMountNames' + ], + 'processEntity' => fn($entity, $depth) => [ + 'ProjectId' => $entity->getId(), + 'ParentProjectId' => $entity->getParent()?->getId() ?? '', + 'Type' => 'project', + 'ProjectNameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(), + 'ProjectName' => $entity->getName(), + 'ProjectFullName' => $this->getFullName($entity), + 'BomQuantity' => '-', + 'BomPartId' => '-', + 'BomPartIpn' => '-', + 'BomPartName' => '-', + 'BomName' => '-', + 'BomPartDescription' => '-', + 'BomMountNames' => '-', + ], + 'processBomEntries' => fn($entity, $depth) => array_map(fn($bomEntry) => [ + 'Id' => $entity->getId(), + 'ParentId' => '', + 'Type' => 'project_bom_entry', + 'ProjectNameHierarchical' => str_repeat('--', $depth) . '> ' . $entity->getName(), + 'ProjectName' => $entity->getName(), + 'ProjectFullName' => $this->getFullName($entity), + 'BomQuantity' => $bomEntry->getQuantity() ?? '', + 'BomPartId' => $bomEntry->getPart()?->getId() ?? '', + 'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '', + 'BomPartName' => $bomEntry->getPart()?->getName() ?? '', + 'BomName' => $bomEntry->getName() ?? '', + 'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '', + 'BomMountNames' => $bomEntry->getMountNames(), + ], $entity->getBomEntries()->toArray()), + ], + Assembly::class => [ + 'header' => [ + 'Id', 'ParentId', 'Type', 'AssemblyIpn', 'AssemblyNameHierarchical', 'AssemblyName', + 'AssemblyFullName', 'BomQuantity', 'BomPartId', 'BomPartIpn', 'BomPartName', 'BomName', 'BomPartDescription', + 'BomMountNames', 'BomReferencedAssemblyId', 'BomReferencedAssemblyIpn', 'BomReferencedAssemblyFullName' + ], + 'processEntity' => fn($entity, $depth) => [ + 'Id' => $entity->getId(), + 'ParentId' => $entity->getParent()?->getId() ?? '', + 'Type' => 'assembly', + 'AssemblyIpn' => $entity->getIpn(), + 'AssemblyNameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(), + 'AssemblyName' => $entity->getName(), + 'AssemblyFullName' => $this->getFullName($entity), + 'BomQuantity' => '-', + 'BomPartId' => '-', + 'BomPartIpn' => '-', + 'BomPartName' => '-', + 'BomName' => '-', + 'BomPartDescription' => '-', + 'BomMountNames' => '-', + 'BomReferencedAssemblyId' => '-', + 'BomReferencedAssemblyIpn' => '-', + 'BomReferencedAssemblyFullName' => '-', + ], + 'processBomEntries' => fn($entity, $depth) => array_map(fn($bomEntry) => [ + 'Id' => $entity->getId(), + 'ParentId' => '', + 'Type' => 'assembly_bom_entry', + 'AssemblyIpn' => $entity->getIpn(), + 'AssemblyNameHierarchical' => str_repeat('--', $depth) . '> ' . $entity->getName(), + 'AssemblyName' => $entity->getName(), + 'AssemblyFullName' => $this->getFullName($entity), + 'BomQuantity' => $bomEntry->getQuantity() ?? '', + 'BomPartId' => $bomEntry->getPart()?->getId() ?? '', + 'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '', + 'BomPartName' => $bomEntry->getPart()?->getName() ?? '', + 'BomName' => $bomEntry->getName() ?? '', + 'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '', + 'BomMountNames' => $bomEntry->getMountNames(), + 'BomReferencedAssemblyId' => $bomEntry->getReferencedAssembly()?->getId() ?? '', + 'BomReferencedAssemblyIpn' => $bomEntry->getReferencedAssembly()?->getIpn() ?? '', + 'BomReferencedAssemblyFullName' => $this->getFullName($bomEntry->getReferencedAssembly() ?? null), + ], $entity->getBomEntries()->toArray()), + ], + Supplier::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Manufacturer::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + StorageLocation::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Footprint::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Currency::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + MeasurementUnit::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + LabelProfile::class => [ + 'header' => ['Id', 'SupportedElement', 'Name'], + 'processEntity' => fn(LabelProfile $entity, $depth) => [ + 'Id' => $entity->getId(), + 'SupportedElement' => $entity->getOptions()->getSupportedElement()->name, + 'Name' => $entity->getName(), + ], + ], + ]; + + //Get configuration for the entity type + $entityConfig = $config[$type] ?? null; + + if (!$entityConfig) { + return ''; + } + + //Initialize CSV data with the header + $csvData = []; + $csvData[] = $entityConfig['header']; + + $relevantEntities = $entities; + + if ($isHierarchical) { + //Filter root entities (those without parents) + $relevantEntities = array_filter($entities, fn($entity) => $entity->getParent() === null); + + if (count($relevantEntities) === 0 && count($entities) > 0) { + //If no root entities are found, then we need to add all entities + + $relevantEntities = $entities; + } + } + + //Sort root entities alphabetically by `name` + usort($relevantEntities, fn($a, $b) => strnatcasecmp($a->getName(), $b->getName())); + + //Recursive function to process an entity and its children + $processEntity = function ($entity, &$csvData, $depth = 0) use (&$processEntity, $entityConfig, $isHierarchical) { + //Add main entity data to CSV + $csvData[] = $entityConfig['processEntity']($entity, $depth); + + //Process BOM entries if applicable + if (isset($entityConfig['processBomEntries'])) { + $bomRows = $entityConfig['processBomEntries']($entity, $depth); + foreach ($bomRows as $bomRow) { + $csvData[] = $bomRow; + } + } + + if ($isHierarchical) { + //Retrieve children, sort alphabetically, then process them + $children = $entity->getChildren()->toArray(); + usort($children, fn($a, $b) => strnatcasecmp($a->getName(), $b->getName())); + foreach ($children as $childEntity) { + $processEntity($childEntity, $csvData, $depth + 1); + } + } + }; + + //Start processing with root entities + foreach ($relevantEntities as $rootEntity) { + $processEntity($rootEntity, $csvData); + } + + //Generate CSV string + $output = ''; + foreach ($csvData as $line) { + $output .= implode(';', $line) . "\n"; // Use a semicolon as the delimiter + } + + return $output; + } + + /** + * Constructs the full hierarchical name of an object by traversing + * through its parent objects and concatenating their names using + * a specified separator. + * + * @param AttachmentType|Category|Project|Assembly|Supplier|Manufacturer|StorageLocation|Footprint|Currency|MeasurementUnit|LabelProfile|null $object The object whose full name is to be constructed. If null, the result will be an empty string. + * @param string $separator The string used to separate the names of the objects in the full hierarchy. + * + * @return string The full hierarchical name constructed by concatenating the names of the object and its parents. + */ + private function getFullName(AttachmentType|Category|Project|Assembly|Supplier|Manufacturer|StorageLocation|Footprint|Currency|MeasurementUnit|LabelProfile|null $object, string $separator = '->'): string + { + $fullNameParts = []; + + while ($object !== null) { + array_unshift($fullNameParts, $object->getName()); + $object = $object->getParent(); + } + + return implode($separator, $fullNameParts); + } } diff --git a/templates/admin/_export_form.html.twig b/templates/admin/_export_form.html.twig index 07b00d43c..527518640 100644 --- a/templates/admin/_export_form.html.twig +++ b/templates/admin/_export_form.html.twig @@ -1,6 +1,6 @@ -
      + -
      +
      @@ -23,7 +23,7 @@
      -
      +
      @@ -34,6 +34,17 @@
      +
      +
      +
      + + +
      +
      +
      +
      diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 89edfb80f..21b1416e6 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -351,6 +351,12 @@ Exportovat všechny prvky + + + export.readable + Čitelné CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 8f28ef088..f4a3881d5 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -351,6 +351,12 @@ Eksportér alle elementer + + + export.readable + Læsbar CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index fbe651ff4..22a36d1d5 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1010,6 +1010,12 @@ Subelemente werden beim Löschen nach oben verschoben. Unterelemente auch exportieren + + + export.readable + Lesbares CSV + + Part-DB1\templates\AdminPages\_export_form.html.twig:39 diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 98630c483..ccbed9fb7 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -228,6 +228,12 @@ Εξαγωγή όλων των στοιχείων + + + export.readable + Αναγνώσιμο CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 9eeb1ef05..d58a1b4fb 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -351,6 +351,12 @@ Export all elements + + + export.readable + Readable CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 8b48e4428..999fd71a0 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -351,6 +351,12 @@ Exportar todos los elementos + + + export.readable + CSV legible + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index ee1d6ffe1..b5ed6a27b 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -320,6 +320,12 @@ Exporter tous les éléments + + + export.readable + CSV lisible + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -9822,13 +9828,13 @@ exemple de ville - + assembly.bom_import.template.csv.exptected_columns Colonnes possibles : - + assembly.bom_import.template.csv.table diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index ac1344052..951a1bd3d 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -351,6 +351,12 @@ Esportare tutti gli elementi + + + export.readable + CSV leggibile + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index e892af9c4..101afd625 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -320,6 +320,12 @@ すべてエクスポートする + + + export.readable + 読みやすいCSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index b24e14b9a..ebf67d393 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -351,6 +351,12 @@ Exporteer alle elementen + + + export.readable + Leesbare CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index a09e3ac33..d9d4eac22 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -351,6 +351,12 @@ Eksportuj wszystkie elementy + + + export.readable + Czytelny CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 8d6a0ba10..3589e4e16 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -351,6 +351,12 @@ Экспортировать всё + + + export.readable + Читаемый CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index f257f9a2f..a3e44de66 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -351,6 +351,12 @@ 导出所有元素 + + + export.readable + 可读的 CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 From 6bb20fb0cbb5937539cb107d9ec9e9d9567d8994 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Fri, 12 Sep 2025 16:05:42 +0200 Subject: [PATCH 80/83] =?UTF-8?q?Entferne=20Projektbezogene=20Logik=20bzw.?= =?UTF-8?q?=20Verweise=20auf=20Baugruppen,=20da=20nicht=20ben=C3=B6tigt.?= =?UTF-8?q?=20=C3=9Cberarbeitung=20Exporter:=20Aufnahme=20von=20Parts=20au?= =?UTF-8?q?s=20Subassemblies.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Controller/AssemblyController.php | 1 - .../AssemblyBomEntriesDataTable.php | 12 +- src/Entity/AssemblySystem/Assembly.php | 3 +- .../AssemblySystem/AssemblyBOMEntry.php | 23 ++-- .../UserSystem/PartUniqueIpnSubscriber.php | 2 +- .../AssemblySystem/AssemblyBOMEntryType.php | 4 - .../Assemblies/AssemblyPartAggregator.php | 83 ++++++++++++ .../AssemblySystem/AssemblyBuildHelper.php | 12 +- .../ImportExportSystem/EntityExporter.php | 123 +++++++++++++----- src/Twig/AssemblyTwigExtension.php | 30 ----- translations/validators.cs.xlf | 6 - translations/validators.da.xlf | 6 - translations/validators.de.xlf | 6 - translations/validators.el.xlf | 6 - translations/validators.en.xlf | 6 - translations/validators.fr.xlf | 6 - translations/validators.hr.xlf | 6 - translations/validators.it.xlf | 6 - translations/validators.ja.xlf | 6 - translations/validators.pl.xlf | 6 - translations/validators.ru.xlf | 6 - translations/validators.zh.xlf | 6 - 22 files changed, 195 insertions(+), 170 deletions(-) create mode 100644 src/Helpers/Assemblies/AssemblyPartAggregator.php delete mode 100644 src/Twig/AssemblyTwigExtension.php diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php index 520bc52de..5d8211216 100644 --- a/src/Controller/AssemblyController.php +++ b/src/Controller/AssemblyController.php @@ -29,7 +29,6 @@ use App\Entity\AssemblySystem\Assembly; use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Parts\Part; -use App\Entity\UserSystem\User; use App\Exceptions\InvalidRegexException; use App\Form\AssemblySystem\AssemblyAddPartsType; use App\Form\AssemblySystem\AssemblyBuildType; diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php index f9baeb869..ddeedba22 100644 --- a/src/DataTables/AssemblyBomEntriesDataTable.php +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -26,14 +26,12 @@ use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; use App\DataTables\Helpers\AssemblyDataTableHelper; -use App\DataTables\Helpers\ProjectDataTableHelper; use App\DataTables\Helpers\ColumnSortHelper; use App\DataTables\Helpers\PartDataTableHelper; use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; use App\Entity\AssemblySystem\AssemblyBOMEntry; -use App\Entity\ProjectSystem\Project; use App\Services\Formatters\AmountFormatter; use App\Settings\BehaviorSettings\TableSettings; use Doctrine\ORM\QueryBuilder; @@ -49,7 +47,6 @@ class AssemblyBomEntriesDataTable implements DataTableTypeInterface public function __construct( private readonly TranslatorInterface $translator, private readonly PartDataTableHelper $partDataTableHelper, - private readonly ProjectDataTableHelper $projectDataTableHelper, private readonly AssemblyDataTableHelper $assemblyDataTableHelper, private readonly AmountFormatter $amountFormatter, private readonly ColumnSortHelper $csh, @@ -90,7 +87,7 @@ public function configure(DataTable $dataTable, array $options): void 'label' => $this->translator->trans('part.table.name'), 'orderField' => 'NATSORT(part.name)', 'render' => function ($value, AssemblyBOMEntry $context) { - if(!$context->getPart() instanceof Part && !$context->getReferencedAssembly() instanceof Assembly && !$context->getProject() instanceof Project) { + if(!$context->getPart() instanceof Part && !$context->getReferencedAssembly() instanceof Assembly) { return htmlspecialchars((string) $context->getName()); } @@ -105,13 +102,6 @@ public function configure(DataTable $dataTable, array $options): void $tmp = $this->assemblyDataTableHelper->renderName($context->getReferencedAssembly()); $tmp = $this->translator->trans('part.table.name.value.for_assembly', ['%value%' => $tmp]); - if($context->getName() !== null && $context->getName() !== '') { - $tmp .= '
      '.htmlspecialchars($context->getName()).''; - } - } elseif ($context->getProject() !== null) { - $tmp = $this->projectDataTableHelper->renderName($context->getProject()); - $tmp = $this->translator->trans('part.table.name.value.for_project', ['%value%' => $tmp]); - if($context->getName() !== null && $context->getName() !== '') { $tmp .= '
      '.htmlspecialchars($context->getName()).''; } diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index c65ca71fe..fde61cd90 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -118,7 +118,6 @@ class Assembly extends AbstractStructuralDBElement #[Groups(['extended', 'full', 'import'])] #[ORM\OneToMany(targetEntity: AssemblyBOMEntry::class, mappedBy: 'assembly', cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])] - #[UniqueObjectCollection(message: 'assembly.bom_entry.project_already_in_bom', fields: ['project'])] #[UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name'])] protected Collection $bom_entries; @@ -137,7 +136,7 @@ class Assembly extends AbstractStructuralDBElement * @var string|null The internal ipn number of the assembly */ #[Assert\Length(max: 100)] - #[Groups(['extended', 'full', 'project:read', 'project:write', 'import'])] + #[Groups(['extended', 'full', 'assembly:read', 'assembly:write', 'import'])] #[ORM\Column(type: Types::STRING, length: 100, unique: true, nullable: true)] #[Length(max: 100)] protected ?string $ipn = null; diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 6de6cc3a6..9620e4891 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -36,7 +36,6 @@ use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Contracts\TimeStampableInterface; -use App\Entity\ProjectSystem\Project; use App\Repository\DBElementRepository; use App\Validator\Constraints\AssemblySystem\AssemblyCycle; use App\Validator\Constraints\AssemblySystem\AssemblyInvalidBomEntry; @@ -252,17 +251,6 @@ public function setReferencedAssembly(?Assembly $referencedAssembly): AssemblyBO return $this; } - public function getProject(): ?Project - { - return $this->project; - } - - public function setProject(?Project $project): AssemblyBOMEntry - { - $this->project = $project; - return $this; - } - /** * Returns the price of this BOM entry, if existing. * Prices are only valid on non-Part BOM entries. @@ -300,6 +288,15 @@ public function isPartBomEntry(): bool return $this->part instanceof Part; } + /** + * Checks whether this BOM entry is a assembly associated BOM entry or not. + * @return bool True if this BOM entry is a assembly associated BOM entry, false otherwise. + */ + public function isAssemblyBomEntry(): bool + { + return $this->referencedAssembly !== null; + } + #[Assert\Callback] public function validate(ExecutionContextInterface $context, $payload): void { @@ -335,7 +332,7 @@ public function getComparableFields(): array return [ 'name' => $this->getName(), 'part' => $this->getPart()?->getID(), - 'project' => $this->getProject()?->getID(), + 'referencedAssembly' => $this->getReferencedAssembly()?->getID(), ]; } } diff --git a/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php index 9cff3166d..c5d22dbef 100644 --- a/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php +++ b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php @@ -70,4 +70,4 @@ private function ensureUniqueIpn(Part $part): void $part->setIpn($originalIpn . "_$increment"); } } -} \ No newline at end of file +} diff --git a/src/Form/AssemblySystem/AssemblyBOMEntryType.php b/src/Form/AssemblySystem/AssemblyBOMEntryType.php index b86b2fd1b..851ab8157 100644 --- a/src/Form/AssemblySystem/AssemblyBOMEntryType.php +++ b/src/Form/AssemblySystem/AssemblyBOMEntryType.php @@ -39,10 +39,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('part', PartSelectType::class, [ 'required' => false, ]) - ->add('project', ProjectSelectType::class, [ - 'label' => 'assembly.bom.project', - 'required' => false, - ]) ->add('referencedAssembly', AssemblySelectType::class, [ 'label' => 'assembly.bom.referencedAssembly', 'required' => false, diff --git a/src/Helpers/Assemblies/AssemblyPartAggregator.php b/src/Helpers/Assemblies/AssemblyPartAggregator.php new file mode 100644 index 000000000..94c102573 --- /dev/null +++ b/src/Helpers/Assemblies/AssemblyPartAggregator.php @@ -0,0 +1,83 @@ +. + */ +namespace App\Helpers\Assemblies; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Parts\Part; + +class AssemblyPartAggregator +{ + /** + * Aggregate the required parts and their total quantities for an assembly. + * + * @param Assembly $assembly The assembly to process. + * @param float $multiplier The quantity multiplier from the parent assembly. + * @return array Array of parts with their aggregated quantities, keyed by Part ID. + */ + public function getAggregatedParts(Assembly $assembly, float $multiplier): array + { + $aggregatedParts = []; + + // Start processing the assembly recursively + $this->processAssembly($assembly, $multiplier, $aggregatedParts); + + // Return the final aggregated list of parts + return $aggregatedParts; + } + + /** + * Recursive helper to process an assembly and all its BOM entries. + * + * @param Assembly $assembly The current assembly to process. + * @param float $multiplier The quantity multiplier from the parent assembly. + * @param array &$aggregatedParts The array to accumulate parts and their quantities. + */ + private function processAssembly(Assembly $assembly, float $multiplier, array &$aggregatedParts): void + { + foreach ($assembly->getBomEntries() as $bomEntry) { + // If the BOM entry refers to a part, add its quantity + if ($bomEntry->getPart() instanceof Part) { + $part = $bomEntry->getPart(); + + if (!isset($aggregatedParts[$part->getId()])) { + $aggregatedParts[$part->getId()] = [ + 'part' => $part, + 'assembly' => $assembly, + 'quantity' => $bomEntry->getQuantity(), + 'multiplier' => $multiplier, + ]; + } + } elseif ($bomEntry->getReferencedAssembly() instanceof Assembly) { + // If the BOM entry refers to another assembly, process it recursively + $this->processAssembly($bomEntry->getReferencedAssembly(), $bomEntry->getQuantity(), $aggregatedParts); + } else { + $aggregatedParts[] = [ + 'part' => null, + 'assembly' => $assembly, + 'quantity' => $bomEntry->getQuantity(), + 'multiplier' => $multiplier, + ]; + } + } + } +} diff --git a/src/Services/AssemblySystem/AssemblyBuildHelper.php b/src/Services/AssemblySystem/AssemblyBuildHelper.php index b7f2df3ce..9180f3e80 100644 --- a/src/Services/AssemblySystem/AssemblyBuildHelper.php +++ b/src/Services/AssemblySystem/AssemblyBuildHelper.php @@ -27,7 +27,6 @@ use App\Entity\Parts\Part; use App\Helpers\Assemblies\AssemblyBuildRequest; use App\Services\Parts\PartLotWithdrawAddHelper; -use App\Services\ProjectSystem\ProjectBuildHelper; /** * @see \App\Tests\Services\AssemblySystem\AssemblyBuildHelperTest @@ -35,8 +34,7 @@ class AssemblyBuildHelper { public function __construct( - private readonly PartLotWithdrawAddHelper $withdraw_add_helper, - private readonly ProjectBuildHelper $projectBuildHelper + private readonly PartLotWithdrawAddHelper $withdraw_add_helper ) { } @@ -70,15 +68,15 @@ public function getMaximumBuildableCount(Assembly $assembly): int /** @var AssemblyBOMEntry $bom_entry */ foreach ($assembly->getBomEntries() as $bom_entry) { //Skip BOM entries without a part (as we can not determine that) - if (!$bom_entry->isPartBomEntry() && $bom_entry->getProject() === null) { + if (!$bom_entry->isPartBomEntry() && !$bom_entry->isAssemblyBomEntry()) { continue; } - //The maximum buildable count for the whole project is the minimum of all BOM entries + //The maximum buildable count for the whole assembly is the minimum of all BOM entries if ($bom_entry->getPart() !== null) { $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); } elseif ($bom_entry->getReferencedAssembly() !== null) { - $maximum_buildable_count = min($maximum_buildable_count, $this->projectBuildHelper->getMaximumBuildableCount($bom_entry->getReferencedAssembly())); + $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCount($bom_entry->getReferencedAssembly())); } } @@ -105,7 +103,7 @@ public function isBOMEntryBuildable(AssemblyBOMEntry $bom_entry, int $number_of_ } /** - * Returns the project BOM entries for which parts are missing in the stock for the given number of builds + * Returns the referenced assembly BOM entries for which parts are missing in the stock for the given number of builds * @param Assembly $assembly The assembly for which the BOM entries should be checked * @param int $number_of_builds How often should the assembly be build? * @return AssemblyBOMEntry[] diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 1adbcdcbb..3e0aa27cf 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -23,6 +23,7 @@ namespace App\Services\ImportExportSystem; use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; @@ -35,6 +36,7 @@ use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; use App\Entity\ProjectSystem\Project; +use App\Helpers\Assemblies\AssemblyPartAggregator; use App\Helpers\FilenameSanatizer; use App\Serializer\APIPlatform\SkippableItemNormalizer; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -56,8 +58,10 @@ */ class EntityExporter { - public function __construct(protected SerializerInterface $serializer) - { + public function __construct( + protected SerializerInterface $serializer, + protected AssemblyPartAggregator $partAggregator, private readonly AssemblyPartAggregator $assemblyPartAggregator, + ) { } protected function configureOptions(OptionsResolver $resolver): void @@ -282,8 +286,9 @@ public function exportReadable(array $entities, string $type, bool $isHierarchic ], Project::class => [ 'header' => [ - 'Id', 'ParentId', 'Type', 'ProjectNameHierarchical', 'ProjectName', 'ProjectFullName', - 'BomQuantity', 'BomPartId', 'BomPartIpn', 'BomPartName', 'BomName', 'BomPartDescription', 'BomMountNames' + 'Id', 'ParentId', 'Type', 'ProjectNameHierarchical', 'ProjectName', 'ProjectFullName', 'BomQuantity', + 'BomPartId', 'BomPartIpn', 'BomPartMpnr', 'BomPartName', 'BomDesignator', 'BomPartDescription', + 'BomMountNames' ], 'processEntity' => fn($entity, $depth) => [ 'ProjectId' => $entity->getId(), @@ -295,12 +300,13 @@ public function exportReadable(array $entities, string $type, bool $isHierarchic 'BomQuantity' => '-', 'BomPartId' => '-', 'BomPartIpn' => '-', + 'BomPartMpnr' => '-', 'BomPartName' => '-', - 'BomName' => '-', + 'BomDesignator' => '-', 'BomPartDescription' => '-', 'BomMountNames' => '-', ], - 'processBomEntries' => fn($entity, $depth) => array_map(fn($bomEntry) => [ + 'processBomEntries' => fn($entity, $depth) => array_map(fn(AssemblyBOMEntry $bomEntry) => [ 'Id' => $entity->getId(), 'ParentId' => '', 'Type' => 'project_bom_entry', @@ -310,8 +316,9 @@ public function exportReadable(array $entities, string $type, bool $isHierarchic 'BomQuantity' => $bomEntry->getQuantity() ?? '', 'BomPartId' => $bomEntry->getPart()?->getId() ?? '', 'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '', + 'BomPartMpnr' => $bomEntry->getPart()?->getManufacturerProductNumber() ?? '', 'BomPartName' => $bomEntry->getPart()?->getName() ?? '', - 'BomName' => $bomEntry->getName() ?? '', + 'BomDesignator' => $bomEntry->getName() ?? '', 'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '', 'BomMountNames' => $bomEntry->getMountNames(), ], $entity->getBomEntries()->toArray()), @@ -319,8 +326,9 @@ public function exportReadable(array $entities, string $type, bool $isHierarchic Assembly::class => [ 'header' => [ 'Id', 'ParentId', 'Type', 'AssemblyIpn', 'AssemblyNameHierarchical', 'AssemblyName', - 'AssemblyFullName', 'BomQuantity', 'BomPartId', 'BomPartIpn', 'BomPartName', 'BomName', 'BomPartDescription', - 'BomMountNames', 'BomReferencedAssemblyId', 'BomReferencedAssemblyIpn', 'BomReferencedAssemblyFullName' + 'AssemblyFullName', 'BomQuantity', 'BomMultiplier', 'BomPartId', 'BomPartIpn', 'BomPartMpnr', + 'BomPartName', 'BomDesignator', 'BomPartDescription', 'BomMountNames', 'BomReferencedAssemblyId', + 'BomReferencedAssemblyIpn', 'BomReferencedAssemblyFullName' ], 'processEntity' => fn($entity, $depth) => [ 'Id' => $entity->getId(), @@ -331,35 +339,19 @@ public function exportReadable(array $entities, string $type, bool $isHierarchic 'AssemblyName' => $entity->getName(), 'AssemblyFullName' => $this->getFullName($entity), 'BomQuantity' => '-', + 'BomMultiplier' => '-', 'BomPartId' => '-', 'BomPartIpn' => '-', + 'BomPartMpnr' => '-', 'BomPartName' => '-', - 'BomName' => '-', + 'BomDesignator' => '-', 'BomPartDescription' => '-', 'BomMountNames' => '-', 'BomReferencedAssemblyId' => '-', 'BomReferencedAssemblyIpn' => '-', 'BomReferencedAssemblyFullName' => '-', ], - 'processBomEntries' => fn($entity, $depth) => array_map(fn($bomEntry) => [ - 'Id' => $entity->getId(), - 'ParentId' => '', - 'Type' => 'assembly_bom_entry', - 'AssemblyIpn' => $entity->getIpn(), - 'AssemblyNameHierarchical' => str_repeat('--', $depth) . '> ' . $entity->getName(), - 'AssemblyName' => $entity->getName(), - 'AssemblyFullName' => $this->getFullName($entity), - 'BomQuantity' => $bomEntry->getQuantity() ?? '', - 'BomPartId' => $bomEntry->getPart()?->getId() ?? '', - 'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '', - 'BomPartName' => $bomEntry->getPart()?->getName() ?? '', - 'BomName' => $bomEntry->getName() ?? '', - 'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '', - 'BomMountNames' => $bomEntry->getMountNames(), - 'BomReferencedAssemblyId' => $bomEntry->getReferencedAssembly()?->getId() ?? '', - 'BomReferencedAssemblyIpn' => $bomEntry->getReferencedAssembly()?->getIpn() ?? '', - 'BomReferencedAssemblyFullName' => $this->getFullName($bomEntry->getReferencedAssembly() ?? null), - ], $entity->getBomEntries()->toArray()), + 'processBomEntries' => fn($entity, $depth) => $this->processBomEntriesWithAggregatedParts($entity, $depth), ], Supplier::class => [ 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], @@ -459,6 +451,79 @@ public function exportReadable(array $entities, string $type, bool $isHierarchic return $output; } + /** + * Process BOM entries and include aggregated parts as "complete_part_list". + * + * @param Assembly $assembly The assembly being processed. + * @param int $depth The current depth in the hierarchy. + * @return array Processed BOM entries and aggregated parts rows. + */ + private function processBomEntriesWithAggregatedParts(Assembly $assembly, int $depth): array + { + $rows = []; + + foreach ($assembly->getBomEntries() as $bomEntry) { + // Add the BOM entry itself + $rows[] = [ + 'Id' => $assembly->getId(), + 'ParentId' => '', + 'Type' => 'assembly_bom_entry', + 'AssemblyIpn' => $assembly->getIpn(), + 'AssemblyNameHierarchical' => str_repeat('--', $depth) . '> ' . $assembly->getName(), + 'AssemblyName' => $assembly->getName(), + 'AssemblyFullName' => $this->getFullName($assembly), + 'BomQuantity' => $bomEntry->getQuantity() ?? '', + 'BomMultiplier' => '', + 'BomPartId' => $bomEntry->getPart()?->getId() ?? '-', + 'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '-', + 'BomPartMpnr' => $bomEntry->getPart()?->getManufacturerProductNumber() ?? '-', + 'BomPartName' => $bomEntry->getPart()?->getName() ?? '-', + 'BomDesignator' => $bomEntry->getName() ?? '-', + 'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '-', + 'BomMountNames' => $bomEntry->getMountNames(), + 'BomReferencedAssemblyId' => $bomEntry->getReferencedAssembly()?->getId() ?? '-', + 'BomReferencedAssemblyIpn' => $bomEntry->getReferencedAssembly()?->getIpn() ?? '-', + 'BomReferencedAssemblyFullName' => $this->getFullName($bomEntry->getReferencedAssembly() ?? null), + ]; + + // If a referenced assembly exists, add aggregated parts + if ($bomEntry->getReferencedAssembly() instanceof Assembly) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + // Get aggregated parts for the referenced assembly + $aggregatedParts = $this->assemblyPartAggregator->getAggregatedParts($referencedAssembly, $bomEntry->getQuantity());; + + foreach ($aggregatedParts as $partData) { + $partAssembly = $partData['assembly'] ?? null; + + $rows[] = [ + 'Id' => $assembly->getId(), + 'ParentId' => '', + 'Type' => 'subassembly_part_list', + 'AssemblyIpn' => $partAssembly ? $partAssembly->getIpn() : '', + 'AssemblyNameHierarchical' => '', + 'AssemblyName' => $partAssembly ? $partAssembly->getName() : '', + 'AssemblyFullName' => $this->getFullName($partAssembly), + 'BomQuantity' => $partData['quantity'], + 'BomMultiplier' => $partData['multiplier'], + 'BomPartId' => $partData['part']?->getId(), + 'BomPartIpn' => $partData['part']?->getIpn(), + 'BomPartMpnr' => $partData['part']?->getManufacturerProductNumber(), + 'BomPartName' => $partData['part']?->getName(), + 'BomDesignator' => $partData['part']?->getName(), + 'BomPartDescription' => $partData['part']?->getDescription(), + 'BomMountNames' => '-', + 'BomReferencedAssemblyId' => '-', + 'BomReferencedAssemblyIpn' => '-', + 'BomReferencedAssemblyFullName' => '-', + ]; + } + } + } + + return $rows; + } + /** * Constructs the full hierarchical name of an object by traversing * through its parent objects and concatenating their names using diff --git a/src/Twig/AssemblyTwigExtension.php b/src/Twig/AssemblyTwigExtension.php deleted file mode 100644 index a8ca7719a..000000000 --- a/src/Twig/AssemblyTwigExtension.php +++ /dev/null @@ -1,30 +0,0 @@ -getProject() !== null) { - return true; - } - } - return false; - } -} \ No newline at end of file diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 66f440d12..652cc2d5f 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -407,12 +407,6 @@ Sestava nesmí ve svém seznamu materiálů (BOM) odkazovat na podskupinu, která je součástí její vlastní hierarchie.
      - - - assembly.bom_entry.project_already_in_bom - Tento projekt již v této skupině existuje! - - assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index f30dd2116..98d107755 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -383,12 +383,6 @@ En samling må ikke referere til en undergruppe fra sin egen hierarki i BOM-listerne. - - - assembly.bom_entry.project_already_in_bom - Dette projekt eksisterer allerede i gruppen! - - assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 645d72ed1..d7b9591f5 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -407,12 +407,6 @@ Eine Baugruppe darf keine Unterbaugruppe aus seiner eigenen Hierarchie in den BOM-Einträgen referenzieren. - - - assembly.bom_entry.project_already_in_bom - Dieses Projekt existiert bereits in der Gruppe! - - assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index 9464f2886..320155fff 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -49,12 +49,6 @@ Μία συναρμολόγηση δεν πρέπει να αναφέρεται σε μία υποσυναρμολόγηση από την ίδια την ιεραρχία της στη λίστα BOM. - - - assembly.bom_entry.project_already_in_bom - Αυτό το έργο υπάρχει ήδη στην ομάδα! - - assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 53ad4cde6..95a558bff 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -407,12 +407,6 @@ An assembly must not reference a subassembly from its own hierarchy in the BOM entries. - - - assembly.bom_entry.project_already_in_bom - This project already exists in the list! - - assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index 440468295..d8e824af9 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -239,12 +239,6 @@ Un assemblage ne doit pas référencer un sous-assemblage de sa propre hiérarchie dans les entrées de la nomenclature (BOM). - - - assembly.bom_entry.project_already_in_bom - Ce projet existe déjà dans le groupe! - - assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 485cb0e2f..23ea0e849 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -401,12 +401,6 @@ Sklop ne smije referencirati podsklop iz vlastite hijerarhije u unosima BOM-a. - - - assembly.bom_entry.project_already_in_bom - Ovaj projekt već postoji u grupi! - - assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index 74d3969f7..4851ab6eb 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -401,12 +401,6 @@ Un assemblaggio non deve fare riferimento a un sottoassemblaggio nella propria gerarchia nelle voci della distinta base (BOM). - - - assembly.bom_entry.project_already_in_bom - Questo progetto esiste già nel gruppo! - - assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index f9f8a54f8..aed026eac 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -239,12 +239,6 @@ アセンブリは、BOMエントリで自身の階層内のサブアセンブリを参照してはいけません。 - - - assembly.bom_entry.project_already_in_bom - このプロジェクトは既にグループに存在しています! - - assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 9916178ca..68d65a4ad 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -401,12 +401,6 @@ Zespół nie może odwoływać się do podzespołu w swojej własnej hierarchii w wpisach BOM. - - - assembly.bom_entry.project_already_in_bom - Ten projekt już znajduje się w grupie! - - assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index b8029e47f..f80d91d72 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -401,12 +401,6 @@ Сборка не должна ссылаться на подсборку внутри своей собственной иерархии в записях спецификации (BOM). - - - assembly.bom_entry.project_already_in_bom - Этот проект уже находится в группе! - - assembly.bom_entry.name_already_in_bom diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 6e4fc0568..c7425081b 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -389,12 +389,6 @@ Сборка не должна ссылаться на подсборку внутри своей собственной иерархии в записях спецификации (BOM). - - - assembly.bom_entry.project_already_in_bom - 该项目已在组中! - - assembly.bom_entry.name_already_in_bom From 7befa53e8c6317b1b1e27fee02bbf70cecdf7108 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 15 Sep 2025 16:04:00 +0200 Subject: [PATCH 81/83] Erweitere Exportfunktion um lesbare BOM-Option (PDF-Ausgabe). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neue Auswahloption "Lesbarer Export" hinzugefügt, die den Export hierarchischer Baugruppen als PDF ermöglicht. --- .../elements/toggle_visibility_controller.js | 48 +++-- .../Assemblies/AssemblyPartAggregator.php | 184 ++++++++++++++++++ .../ImportExportSystem/EntityExporter.php | 27 ++- templates/admin/_export_form.html.twig | 16 +- templates/assemblies/export_bom_pdf.html.twig | 103 ++++++++++ ...port_bom_referenced_assembly_pdf.html.twig | 55 ++++++ translations/messages.cs.xlf | 14 +- translations/messages.da.xlf | 14 +- translations/messages.de.xlf | 14 +- translations/messages.el.xlf | 14 +- translations/messages.en.xlf | 14 +- translations/messages.es.xlf | 14 +- translations/messages.fr.xlf | 14 +- translations/messages.it.xlf | 14 +- translations/messages.ja.xlf | 14 +- translations/messages.nl.xlf | 14 +- translations/messages.pl.xlf | 14 +- translations/messages.ru.xlf | 14 +- translations/messages.zh.xlf | 14 +- 19 files changed, 572 insertions(+), 43 deletions(-) create mode 100644 templates/assemblies/export_bom_pdf.html.twig create mode 100644 templates/assemblies/export_bom_referenced_assembly_pdf.html.twig diff --git a/assets/controllers/elements/toggle_visibility_controller.js b/assets/controllers/elements/toggle_visibility_controller.js index 4600dfb29..51c9cb338 100644 --- a/assets/controllers/elements/toggle_visibility_controller.js +++ b/assets/controllers/elements/toggle_visibility_controller.js @@ -7,30 +7,41 @@ export default class extends Controller { }; connect() { - this.readableCheckbox = this.element.querySelector("#readable"); + this.displayCheckbox = this.element.querySelector("#display"); + this.displaySelect = this.element.querySelector("select#display"); - if (!this.readableCheckbox) { - return; + if (this.displayCheckbox) { + this.toggleContainers(this.displayCheckbox.checked); + + this.displayCheckbox.addEventListener("change", (event) => { + this.toggleContainers(event.target.checked); + }); } - // Apply the initial visibility state based on the checkbox being checked or not - this.toggleContainers(this.readableCheckbox.checked); + if (this.displaySelect) { + this.toggleContainers(this.hasDisplaySelectValue()); - // Add a change event listener to the 'readable' checkbox - this.readableCheckbox.addEventListener("change", (event) => { - // Toggle container visibility when the checkbox value changes - this.toggleContainers(event.target.checked); - }); + this.displaySelect.addEventListener("change", () => { + this.toggleContainers(this.hasDisplaySelectValue()); + }); + } + + } + + /** + * Check whether a value was selected in the selectbox + * @returns {boolean} True when a value has not been selected that is not empty + */ + hasDisplaySelectValue() { + return this.displaySelect && this.displaySelect.value !== ""; } /** - * Toggles the visibility of containers based on the checkbox state. - * Hides specified containers if the checkbox is checked and shows them otherwise. + * Hides specified containers if the state is active (checkbox checked or select with value). * - * @param {boolean} isChecked - The current state of the checkbox: - * true if checked (hide elements), false if unchecked (show them). + * @param {boolean} isActive - True when the checkbox is activated or the selectbox has a value. */ - toggleContainers(isChecked) { + toggleContainers(isActive) { if (!Array.isArray(this.classesValue) || this.classesValue.length === 0) { return; } @@ -42,11 +53,10 @@ export default class extends Controller { return; } - // Update the visibility for each selected element elements.forEach((element) => { - // If the checkbox is checked, hide the container; otherwise, show it - element.style.display = isChecked ? "none" : ""; + element.style.display = isActive ? "none" : ""; }); }); } -} \ No newline at end of file + +} diff --git a/src/Helpers/Assemblies/AssemblyPartAggregator.php b/src/Helpers/Assemblies/AssemblyPartAggregator.php index 94c102573..2346075a1 100644 --- a/src/Helpers/Assemblies/AssemblyPartAggregator.php +++ b/src/Helpers/Assemblies/AssemblyPartAggregator.php @@ -24,9 +24,16 @@ use App\Entity\AssemblySystem\Assembly; use App\Entity\Parts\Part; +use Dompdf\Dompdf; +use Dompdf\Options; +use Twig\Environment; class AssemblyPartAggregator { + public function __construct(private readonly Environment $twig) + { + } + /** * Aggregate the required parts and their total quantities for an assembly. * @@ -80,4 +87,181 @@ private function processAssembly(Assembly $assembly, float $multiplier, array &$ } } } + + /** + * Exports a hierarchical Bill of Materials (BOM) for assemblies and parts in a readable format, + * including the multiplier for each part and assembly. + * + * @param Assembly $assembly The root assembly to export. + * @param string $indentationSymbol The symbol used for indentation (e.g., ' '). + * @param int $initialDepth The starting depth for formatting (default: 0). + * @return string Human-readable hierarchical BOM list. + */ + public function exportReadableHierarchy(Assembly $assembly, string $indentationSymbol = ' ', int $initialDepth = 0): string + { + // Start building the hierarchy + $output = ''; + $this->processAssemblyHierarchy($assembly, $initialDepth, 1, $indentationSymbol, $output); + + return $output; + } + + public function exportReadableHierarchyForPdf(array $assemblyHierarchies): string + { + $html = $this->twig->render('assemblies/export_bom_pdf.html.twig', [ + 'assemblies' => $assemblyHierarchies, + ]); + + $options = new Options(); + $options->set('isHtml5ParserEnabled', true); + $options->set('isPhpEnabled', true); + + $dompdf = new Dompdf($options); + $dompdf->loadHtml($html); + $dompdf->setPaper('A4'); + $dompdf->render(); + + $canvas = $dompdf->getCanvas(); + $font = $dompdf->getFontMetrics()->getFont('Arial', 'normal'); + + return $dompdf->output(); + } + + /** + * Recursive method to process assemblies and their parts. + * + * @param Assembly $assembly The current assembly to process. + * @param int $depth The current depth in the hierarchy. + * @param float $parentMultiplier The multiplier inherited from the parent (default is 1 for root). + * @param string $indentationSymbol The symbol used for indentation. + * @param string &$output The cumulative output string. + */ + private function processAssemblyHierarchy(Assembly $assembly, int $depth, float $parentMultiplier, string $indentationSymbol, string &$output): void + { + // Add the current assembly to the output + if ($depth === 0) { + $output .= sprintf( + "%sAssembly: %s [IPN: %s]\n\n", + str_repeat($indentationSymbol, $depth), + $assembly->getName(), + $assembly->getIpn(), + ); + } else { + $output .= sprintf( + "%sAssembly: %s [IPN: %s, Multiplier: %.2f]\n\n", + str_repeat($indentationSymbol, $depth), + $assembly->getName(), + $assembly->getIpn(), + $parentMultiplier + ); + } + + // Gruppiere BOM-Einträge in Kategorien + $parts = []; + $referencedAssemblies = []; + $others = []; + + foreach ($assembly->getBomEntries() as $bomEntry) { + if ($bomEntry->getPart() instanceof Part) { + $parts[] = $bomEntry; + } elseif ($bomEntry->getReferencedAssembly() instanceof Assembly) { + $referencedAssemblies[] = $bomEntry; + } else { + $others[] = $bomEntry; + } + } + + if (!empty($parts)) { + // Process each BOM entry for the current assembly + foreach ($parts as $bomEntry) { + $effectiveQuantity = $bomEntry->getQuantity() * $parentMultiplier; + + $output .= sprintf( + "%sPart: %s [IPN: %s, MPNR: %s, Quantity: %.2f%s, EffectiveQuantity: %.2f]\n", + str_repeat($indentationSymbol, $depth + 1), + $bomEntry->getPart()?->getName(), + $bomEntry->getPart()?->getIpn() ?? '-', + $bomEntry->getPart()?->getManufacturerProductNumber() ?? '-', + $bomEntry->getQuantity(), + $parentMultiplier > 1 ? sprintf(", Multiplier: %.2f", $parentMultiplier) : '', + $effectiveQuantity, + ); + } + + $output .= "\n"; + } + + foreach ($referencedAssemblies as $bomEntry) { + // Add referenced assembly details + $referencedQuantity = $bomEntry->getQuantity() * $parentMultiplier; + + $output .= sprintf( + "%sReferenced Assembly: %s [IPN: %s, Quantity: %.2f%s, EffectiveQuantity: %.2f]\n", + str_repeat($indentationSymbol, $depth + 1), + $bomEntry->getReferencedAssembly()->getName(), + $bomEntry->getReferencedAssembly()->getIpn() ?? '-', + $bomEntry->getQuantity(), + $parentMultiplier > 1 ? sprintf(", Multiplier: %.2f", $parentMultiplier) : '', + $referencedQuantity, + ); + + // Recurse into the referenced assembly + $this->processAssemblyHierarchy( + $bomEntry->getReferencedAssembly(), + $depth + 2, // Increase depth for nested assemblies + $referencedQuantity, // Pass the calculated multiplier + $indentationSymbol, + $output + ); + } + + foreach ($others as $bomEntry) { + $output .= sprintf( + "%sOther: %s [Quantity: %.2f, Multiplier: %.2f]\n", + str_repeat($indentationSymbol, $depth + 1), + $bomEntry->getName(), + $bomEntry->getQuantity(), + $parentMultiplier, + ); + } + } + + public function processAssemblyHierarchyForPdf(Assembly $assembly, int $depth, float $quantity, float $parentMultiplier): array + { + $result = [ + 'name' => $assembly->getName(), + 'ipn' => $assembly->getIpn(), + 'quantity' => $quantity, + 'multiplier' => $depth === 0 ? null : $parentMultiplier, + 'parts' => [], + 'referencedAssemblies' => [], + 'others' => [], + ]; + + foreach ($assembly->getBomEntries() as $bomEntry) { + if ($bomEntry->getPart() instanceof Part) { + $result['parts'][] = [ + 'name' => $bomEntry->getPart()->getName(), + 'ipn' => $bomEntry->getPart()->getIpn(), + 'quantity' => $bomEntry->getQuantity(), + 'effectiveQuantity' => $bomEntry->getQuantity() * $parentMultiplier, + ]; + } elseif ($bomEntry->getReferencedAssembly() instanceof Assembly) { + $result['referencedAssemblies'][] = $this->processAssemblyHierarchyForPdf( + $bomEntry->getReferencedAssembly(), + $depth + 1, + $bomEntry->getQuantity(), + $parentMultiplier * $bomEntry->getQuantity() + ); + } else { + $result['others'][] = [ + 'name' => $bomEntry->getName(), + 'quantity' => $bomEntry->getQuantity(), + 'multiplier' => $parentMultiplier, + ]; + } + } + + return $result; + } } diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 3e0aa27cf..f5c827549 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -78,8 +78,9 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setDefault('include_children', false); $resolver->setAllowedTypes('include_children', 'bool'); - $resolver->setDefault('readable', false); - $resolver->setAllowedTypes('readable', 'bool'); + $resolver->setDefault('readableSelect', null); + $resolver->setAllowedValues('readableSelect', [null, 'readable', 'readable_bom']); + } /** @@ -162,7 +163,7 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities, $entities = [$entities]; } - if ($request->get('readable', false)) { + if ($request->get('readableSelect', false) === 'readable') { // Map entity classes to export functions $entityExportMap = [ AttachmentType::class => fn($entities) => $this->exportReadable($entities, AttachmentType::class), @@ -195,6 +196,23 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities, $options['format'] = 'csv'; $options['level'] = 'readable'; + } if ($request->get('readableSelect', false) === 'readable_bom') { + $hierarchies = []; + + foreach ($entities as $entity) { + if (!$entity instanceof Assembly) { + throw new InvalidArgumentException('Only assemblies can be exported in readable BOM format'); + } + + $hierarchies[] = $this->assemblyPartAggregator->processAssemblyHierarchyForPdf($entity, 0, 1, 1); + } + + $pdfContent = $this->assemblyPartAggregator->exportReadableHierarchyForPdf($hierarchies); + + $response = new Response($pdfContent); + + $options['format'] = 'pdf'; + $options['level'] = 'readable_bom'; } else { //Do the serialization with the given options $serialized_data = $this->exportEntities($entities, $options); @@ -221,6 +239,9 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities, case 'json': $content_type = 'application/json'; break; + case 'pdf': + $content_type = 'application/pdf'; + break; } $response->headers->set('Content-Type', $content_type); diff --git a/templates/admin/_export_form.html.twig b/templates/admin/_export_form.html.twig index 527518640..4810f67fd 100644 --- a/templates/admin/_export_form.html.twig +++ b/templates/admin/_export_form.html.twig @@ -35,13 +35,13 @@
      -
      -
      - - -
      + +
      +
      @@ -50,4 +50,4 @@
      - \ No newline at end of file + diff --git a/templates/assemblies/export_bom_pdf.html.twig b/templates/assemblies/export_bom_pdf.html.twig new file mode 100644 index 000000000..15bf5d883 --- /dev/null +++ b/templates/assemblies/export_bom_pdf.html.twig @@ -0,0 +1,103 @@ + + + + Assembly Hierarchy + + + + + +

      Table of Contents

      + + + + + + + + + + + {% for assembly in assemblies %} + + + + + + + {% endfor %} + +
      #Assembly NameIPNSection
      {{ loop.index }}Assembly: {{ assembly.name }}{% if assembly.ipn != '' %}{{ assembly.ipn }}{% else %}-{% endif %}{{ loop.index + 1 }}
      +
      + + +{% for assembly in assemblies %} +
      Assembly: {{ assembly.name }}
      + + + + + + + + + + + + {% for part in assembly.parts %} + + + + + + + + {% endfor %} + {% for other in assembly.others %} + + + + + + + + {% endfor %} + {% for referencedAssembly in assembly.referencedAssemblies %} + + + + + + + + {% endfor %} + +
      NameIPNQuantityMultiplierEffective Quantity
      {{ part.name }}{{ part.ipn }}{{ part.quantity }}{% if assembly.multiplier %}{{ assembly.multiplier }}{% else %}-{% endif %}{{ part.effectiveQuantity }}
      {{ other.name }}{{ other.ipn }}{{ other.quantity }}{{ other.multiplier }}{{ other.effectiveQuantity }}
      {{ referencedAssembly.name }}{{ referencedAssembly.ipn }}{{ referencedAssembly.quantity }}{{ referencedAssembly.quantity }}
      + + {% for refAssembly in assembly.referencedAssemblies %} + {% include 'assemblies/export_bom_referenced_assembly_pdf.html.twig' with {'assembly': refAssembly} only %} + {% endfor %} + + {% if not loop.last %} +
      + {% endif %} + + +{% endfor %} + + diff --git a/templates/assemblies/export_bom_referenced_assembly_pdf.html.twig b/templates/assemblies/export_bom_referenced_assembly_pdf.html.twig new file mode 100644 index 000000000..b5a1324d9 --- /dev/null +++ b/templates/assemblies/export_bom_referenced_assembly_pdf.html.twig @@ -0,0 +1,55 @@ +
      +
      Referenced Assembly: {{ assembly.name }} [IPN: {% if assembly.ipn != '' %}{{ assembly.ipn }}{% else %}-{% endif %}, quantity: {{ assembly.quantity }}]
      + + + + + + + + + + + + + + {% for part in assembly.parts %} + + + + + + + + + {% endfor %} + + {% for other in assembly.others %} + + + + + + + + + {% endfor %} + + {% for referencedAssembly in assembly.referencedAssemblies %} + + + + + + + + + {% endfor %} + +
      TypeNameIPNQuantityMultiplierEffective Quantity
      Part{{ part.name }}{{ part.ipn }}{{ part.quantity }}{% if assembly.multiplier %}{{ assembly.multiplier }}{% else %}-{% endif %}{{ part.effectiveQuantity }}
      Other{{ other.name }}-{{ other.quantity }}{{ other.multiplier }}-
      Referenced assembly{{ referencedAssembly.name }}-{{ referencedAssembly.quantity }}{{ referencedAssembly.multiplier }}
      + + + {% for refAssembly in assembly.referencedAssemblies %} + {% include 'assemblies/export_bom_referenced_assembly_pdf.html.twig' with {'assembly': refAssembly} only %} + {% endfor %} +
      diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 52d009e1b..514f144b8 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -351,10 +351,22 @@ Exportovat všechny prvky + + + export.readable.label + Čitelný export + + export.readable - Čitelné CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 2edc10e9d..bc1bff594 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -351,10 +351,22 @@ Eksportér alle elementer + + + export.readable.label + Læsbar eksport + + export.readable - Læsbar CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 7098a0e8e..8ef5da894 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1010,10 +1010,22 @@ Subelemente werden beim Löschen nach oben verschoben. Unterelemente auch exportieren + + + export.readable.label + Lesbarer Export + + export.readable - Lesbares CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 8da5cd34a..f28c805eb 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -228,10 +228,22 @@ Εξαγωγή όλων των στοιχείων + + + export.readable.label + Αναγνώσιμη εξαγωγή + + export.readable - Αναγνώσιμο CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 6dab8e764..927d661fc 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -351,10 +351,22 @@ Export all elements + + + export.readable.label + Readable Export + + export.readable - Readable CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 75fc0e80a..da66d1141 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -351,10 +351,22 @@ Exportar todos los elementos + + + export.readable.label + Exportación legible + + export.readable - CSV legible + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index b61405dcd..af0e51954 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -320,10 +320,22 @@ Exporter tous les éléments + + + export.readable.label + Export lisible + + export.readable - CSV lisible + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 66d3a63e4..6ef5b528e 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -351,10 +351,22 @@ Esportare tutti gli elementi + + + export.readable.label + Esporta leggibile + + export.readable - CSV leggibile + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 089f2dee9..417ed6716 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -320,10 +320,22 @@ すべてエクスポートする + + + export.readable.label + 読みやすいエクスポート + + export.readable - 読みやすいCSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index b3089b4ac..fbe239922 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -351,10 +351,22 @@ Exporteer alle elementen + + + export.readable.label + Leesbare export + + export.readable - Leesbare CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 9efe857c7..594324fc1 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -351,10 +351,22 @@ Eksportuj wszystkie elementy + + + export.readable.label + Eksport czytelny + + export.readable - Czytelny CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index b2c2556fc..402b1651a 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -351,10 +351,22 @@ Экспортировать всё + + + export.readable.label + Читаемый экспорт + + export.readable - Читаемый CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index c28d53d6f..2bd5e0135 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -351,10 +351,22 @@ 导出所有元素 + + + export.readable.label + 可读导出 + + export.readable - 可读的 CSV + CSV + + + + + export.readable_bom + PDF From bcd5da9279a8dbcb7fa94fe4b17500cf3e873f70 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 17 Sep 2025 09:03:52 +0200 Subject: [PATCH 82/83] =?UTF-8?q?PDF=20Exportm=C3=B6glichkeit=20zun=C3=A4c?= =?UTF-8?q?hst=20nur=20anzeigen,=20wenn=20es=20sich=20um=20ein=20Assembly?= =?UTF-8?q?=20handelt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ImportExportSystem/EntityExporter.php | 2 +- templates/admin/_export_form.html.twig | 30 +++++++++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index f5c827549..50364da1d 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -196,7 +196,7 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities, $options['format'] = 'csv'; $options['level'] = 'readable'; - } if ($request->get('readableSelect', false) === 'readable_bom') { + } elseif ($request->get('readableSelect', false) === 'readable_bom') { $hierarchies = []; foreach ($entities as $entity) { diff --git a/templates/admin/_export_form.html.twig b/templates/admin/_export_form.html.twig index 4810f67fd..b02d4a8e8 100644 --- a/templates/admin/_export_form.html.twig +++ b/templates/admin/_export_form.html.twig @@ -34,16 +34,28 @@
      -
      - -
      - + {% if path is defined and 'assembly' in path %} +
      + +
      + +
      -
      + {% else %} +
      + +
      + + +
      +
      + {% endif %}
      From 8717a07497d92cf4dd8b539675453affffab2a05 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Fri, 19 Sep 2025 08:29:46 +0200 Subject: [PATCH 83/83] =?UTF-8?q?Anzeige=20und=20Logik=20f=C3=BCr=20zugeh?= =?UTF-8?q?=C3=B6rige=20Build-Parts=20sowie=20Build-Tab=20bei=20Assemblies?= =?UTF-8?q?=20entfernen,=20da=20vorerst=20nicht=20ben=C3=B6tigt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/admin/assembly_admin.html.twig | 17 +---------------- templates/assemblies/info/_info.html.twig | 7 +------ templates/assemblies/info/info.html.twig | 12 +----------- 3 files changed, 3 insertions(+), 33 deletions(-) diff --git a/templates/admin/assembly_admin.html.twig b/templates/admin/assembly_admin.html.twig index def4eeb2e..2e68a3da9 100644 --- a/templates/admin/assembly_admin.html.twig +++ b/templates/admin/assembly_admin.html.twig @@ -32,21 +32,6 @@ {{ form_row(form.description) }} {{ form_row(form.status) }} {{ form_row(form.ipn) }} - {% if entity.id %} -
      - -
      - {% if entity.buildPart %} - {{ entity.buildPart.name }} - {% else %} - {% trans %}assembly.edit.associated_build_part.add{% endtrans %} - {% endif %} -

      {% trans %}assembly.edit.associated_build.hint{% endtrans %}

      -
      -
      - {% endif %} - {% endblock %} {% block additional_panes %} @@ -62,4 +47,4 @@ {% endif %}
      -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/assemblies/info/_info.html.twig b/templates/assemblies/info/_info.html.twig index 495072b8a..97da3f708 100644 --- a/templates/assemblies/info/_info.html.twig +++ b/templates/assemblies/info/_info.html.twig @@ -20,11 +20,6 @@ {% endif %}
      {{ assembly.description|format_markdown(true) }}
      - {% if assembly.buildPart %} -
      {% trans %}assembly.edit.associated_build_part{% endtrans %}:
      - {{ assembly.buildPart.name }} - {% endif %} -
      @@ -74,4 +69,4 @@ {{ assembly.comment|format_markdown }}

      {% endif %} -
      \ No newline at end of file +
      diff --git a/templates/assemblies/info/info.html.twig b/templates/assemblies/info/info.html.twig index 5a419e364..667da909f 100644 --- a/templates/assemblies/info/info.html.twig +++ b/templates/assemblies/info/info.html.twig @@ -78,13 +78,6 @@ {{ assembly.bomEntries | length }} - {% if assembly.attachments is not empty %}
      -
      - {% include "assemblies/info/_builds.html.twig" %} -
      {% include "assemblies/info/_attachments_info.html.twig" with {"assembly": assembly} %}
      -{% endblock %} \ No newline at end of file +{% endblock %}