From f0748a212339044a7daaa19eec5469791f58de01 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 19 Mar 2025 08:13:45 +0100 Subject: [PATCH] =?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 | 17 +- src/Controller/TreeController.php | 14 + src/Controller/TypeaheadController.php | 64 +- .../AssemblyBomEntriesDataTable.php | 209 +++++ .../Helpers/AssemblyDataTableHelper.php | 48 ++ src/DataTables/ProjectBomEntriesDataTable.php | 32 +- 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 | 7 + 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 | 230 +++++- .../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, 14096 insertions(+), 98 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 00000000..98702d41 --- /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 0658f4b4..2b658d52 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 2abd3d77..f3e8cb90 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 8cbd60c3..8709fdb7 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 00000000..4e521ade --- /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 00000000..62dcc43c --- /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 00000000..542fcac2 --- /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 201263ff..b0c08392 100644 --- a/src/Command/Migrations/ConvertBBCodeCommand.php +++ b/src/Command/Migrations/ConvertBBCodeCommand.php @@ -22,6 +22,7 @@ declare(strict_types=1); 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 @@ class ConvertBBCodeCommand extends Command 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 00000000..20f64092 --- /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 00000000..9710e9be --- /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 aeb2664e..6b9472fd 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -23,6 +23,7 @@ declare(strict_types=1); 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\LogSystem\TimeTravel; 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; @@ -204,15 +206,18 @@ final class PartController extends AbstractController #[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')] + #[Route(path: '/new_build_part_project/{project_id}', name: 'part_new_build_part')] + #[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: ['project_id' => 'id'])] ?Project $project = null, + #[MapEntity(mapping: ['assembly_id' => 'id'])] ?Assembly $assembly = null ): Response { if ($part instanceof Part) { @@ -226,6 +231,14 @@ final class PartController extends AbstractController 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 71f8ba5c..0ba3a158 100644 --- a/src/Controller/TreeController.php +++ b/src/Controller/TreeController.php @@ -22,6 +22,7 @@ declare(strict_types=1); 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 @@ class TreeController extends AbstractController 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 89eac7ff..09792951 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -22,7 +22,9 @@ declare(strict_types=1); 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\Routing\Attribute\Route; 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 @@ use Symfony\Component\Serializer\Serializer; #[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 @@ class TypeaheadController extends AbstractController '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 @@ class TypeaheadController extends AbstractController 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 00000000..7149ed5f --- /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 00000000..36f7836b --- /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 fcb06984..1c7a09e4 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -25,7 +25,9 @@ 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\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 @@ use Symfony\Contracts\Translation\TranslatorInterface; 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 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface '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 + if ($context->getPart() !== null) { + $tmp = $this->partDataTableHelper->renderName($context->getPart()); + $tmp = $this->translator->trans('part.table.name.value.for_part', ['%value%' => $tmp]); - $tmp = $this->partDataTableHelper->renderName($context->getPart()); - if($context->getName() !== null && $context->getName() !== '') { - $tmp .= '
'.htmlspecialchars($context->getName()).''; + 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 00000000..17a6868f --- /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 00000000..375fef04 --- /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 00000000..bb9a11c8 --- /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 00cf581a..808c6062 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -97,7 +97,7 @@ use function in_array; #[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 9fb5d648..ad9c534b 100644 --- a/src/Entity/Base/AbstractDBElement.php +++ b/src/Entity/Base/AbstractDBElement.php @@ -22,6 +22,9 @@ declare(strict_types=1); 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 @@ use Symfony\Component\Serializer\Annotation\Groups; * 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 16bf33f5..be19bb0c 100644 --- a/src/Entity/LogSystem/CollectionElementDeleted.php +++ b/src/Entity/LogSystem/CollectionElementDeleted.php @@ -41,6 +41,8 @@ declare(strict_types=1); 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\Attachments\UserAttachment; 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 @@ class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventU { 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 @@ class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventU 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 61a2b081..d5bc9ce0 100644 --- a/src/Entity/LogSystem/LogTargetType.php +++ b/src/Entity/LogSystem/LogTargetType.php @@ -22,6 +22,8 @@ declare(strict_types=1); */ 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\InfoProviderSystem\BulkInfoProviderImportJob; @@ -72,6 +74,9 @@ enum LogTargetType: int case BULK_INFO_PROVIDER_IMPORT_JOB = 21; case BULK_INFO_PROVIDER_IMPORT_JOB_PART = 22; + case ASSEMBLY = 23; + case ASSEMBLY_BOM_ENTRY = 24; + /** * Returns the class name of the target type or null if the target type is NONE. * @return string|null @@ -86,6 +91,8 @@ enum LogTargetType: int 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 39f333da..b6ef0412 100644 --- a/src/Entity/Parameters/AbstractParameter.php +++ b/src/Entity/Parameters/AbstractParameter.php @@ -73,7 +73,7 @@ use function sprintf; #[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 00000000..349fa790 --- /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 2f274a8a..689e7ce7 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Entity\Parts; +use App\Entity\Parts\PartTraits\AssemblyTrait; use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; use ApiPlatform\Doctrine\Orm\Filter\DateFilter; @@ -125,6 +126,7 @@ class Part extends AttachmentContainingDBElement use OrderTrait; use ParametersTrait; use ProjectTrait; + use AssemblyTrait; use AssociationTrait; use EDATrait; @@ -186,6 +188,7 @@ class Part extends AttachmentContainingDBElement $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 00000000..57f78d35 --- /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 a103d694..36a96377 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 2a7862ec..69240773 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -35,6 +35,7 @@ use ApiPlatform\Metadata\Post; 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 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte return $this; } - - public function getPart(): ?Part { return $this->part; @@ -225,6 +239,16 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte 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 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte 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 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte 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 00000000..be1564d2 --- /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 5a4ef5bc..e5d69b35 100644 --- a/src/Form/AdminPages/BaseEntityAdminForm.php +++ b/src/Form/AdminPages/BaseEntityAdminForm.php @@ -22,6 +22,7 @@ declare(strict_types=1); 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 @@ class BaseEntityAdminForm extends AbstractType ); } - 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 00000000..4d84881f --- /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 00000000..04293f4e --- /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 00000000..9addccb3 --- /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 00000000..8838706d --- /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 ff80bd38..a4458895 100644 --- a/src/Form/Filters/AttachmentFilterType.php +++ b/src/Form/Filters/AttachmentFilterType.php @@ -23,6 +23,7 @@ declare(strict_types=1); 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 @@ class AttachmentFilterType extends AbstractType '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 61f72c41..c5dbe99f 100644 --- a/src/Form/ProjectSystem/ProjectAddPartsType.php +++ b/src/Form/ProjectSystem/ProjectAddPartsType.php @@ -59,6 +59,7 @@ class ProjectAddPartsType extends AbstractType ], '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 cac362fb..de8eb789 100644 --- a/src/Form/ProjectSystem/ProjectBOMEntryType.php +++ b/src/Form/ProjectSystem/ProjectBOMEntryType.php @@ -5,6 +5,7 @@ declare(strict_types=1); 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 @@ class ProjectBOMEntryType extends AbstractType }); $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 @@ class ProjectBOMEntryType extends AbstractType '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 2b7b52e2..d0d4e343 100644 --- a/src/Form/ProjectSystem/ProjectBuildType.php +++ b/src/Form/ProjectSystem/ProjectBuildType.php @@ -22,6 +22,7 @@ declare(strict_types=1); */ 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\FormBuilderInterface; 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 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface //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 00000000..ee6cf7c2 --- /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 34b8fc7c..c41d6b8f 100644 --- a/src/Form/Type/PartSelectType.php +++ b/src/Form/Type/PartSelectType.php @@ -50,7 +50,7 @@ class PartSelectType extends AbstractType implements DataMapperInterface $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 @@ class PartSelectType extends AbstractType implements DataMapperInterface 'data-autocomplete' => $this->urlGenerator->generate('typeahead_parts', ['query' => '__QUERY__']), //Disable browser autocomplete 'autocomplete' => 'off', - ], ]); @@ -103,7 +102,7 @@ class PartSelectType extends AbstractType implements DataMapperInterface } 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 00000000..c33a6f61 --- /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 430d37b5..3254565a 100644 --- a/src/Helpers/Projects/ProjectBuildRequest.php +++ b/src/Helpers/Projects/ProjectBuildRequest.php @@ -22,10 +22,13 @@ declare(strict_types=1); */ 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 @@ final class ProjectBuildRequest //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 @@ final class ProjectBuildRequest $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 @@ final class ProjectBuildRequest { $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 @@ final class ProjectBuildRequest 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 @@ final class ProjectBuildRequest $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 00000000..031e6e82 --- /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 2437e848..23ad296a 100644 --- a/src/Repository/DBElementRepository.php +++ b/src/Repository/DBElementRepository.php @@ -154,4 +154,14 @@ class DBElementRepository extends EntityRepository $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 bd7ae4df..766aaeac 100644 --- a/src/Security/Voter/AttachmentVoter.php +++ b/src/Security/Voter/AttachmentVoter.php @@ -22,6 +22,7 @@ declare(strict_types=1); 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 @@ final class AttachmentVoter extends Voter $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 ad0299a7..8079757c 100644 --- a/src/Security/Voter/StructureVoter.php +++ b/src/Security/Voter/StructureVoter.php @@ -22,6 +22,7 @@ declare(strict_types=1); 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 00000000..8c95a4b6 --- /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 00000000..9a550350 --- /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 00000000..9ecbbd07 --- /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 9fbc3fe3..4d0da57c 100644 --- a/src/Services/Attachments/AttachmentSubmitHandler.php +++ b/src/Services/Attachments/AttachmentSubmitHandler.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Services\Attachments; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentType; @@ -84,6 +85,7 @@ class AttachmentSubmitHandler 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 326707b7..130cc53d 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -22,6 +22,8 @@ declare(strict_types=1); namespace App\Services; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentType; @@ -66,6 +68,8 @@ class ElementTypeNameGenerator 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'), @@ -182,6 +186,8 @@ class ElementTypeNameGenerator $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 78db06f0..8e1704b4 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -22,6 +22,7 @@ declare(strict_types=1); 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 @@ class EntityURLGenerator 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 @@ class EntityURLGenerator 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 @@ class EntityURLGenerator 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 @@ class EntityURLGenerator 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 @@ class EntityURLGenerator 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 @@ class EntityURLGenerator { $map = [ Project::class => 'project_info', + Assembly::class => 'assembly_info', Category::class => 'part_list_category', Footprint::class => 'part_list_footprint', @@ -341,6 +348,7 @@ class EntityURLGenerator 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 862fa463..fd32f065 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -22,15 +22,25 @@ declare(strict_types=1); */ 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']); @@ -88,12 +102,29 @@ class BOMImporter } /** - * Converts the given file into an array of BOM entries using the given options. - * @return ProjectBOMEntry[] + * 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 fileToBOMEntries(File $file, array $options): array + public function importFileIntoAssembly(File $file, Assembly $assembly, array $options): array { - return $this->stringToBOMEntries($file->getContent(), $options); + $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[]|AssemblyBOMEntry[] + */ + public function fileToBOMEntries(File $file, array $options, string $objectType = ProjectBOMEntry::class): array + { + return $this->stringToBOMEntries($file->getContent(), $options, $objectType); } /** @@ -117,22 +148,22 @@ class BOMImporter * 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 @@ class BOMImporter 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 @@ class BOMImporter 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 @@ class BOMImporter } //@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 269c7e4c..d7ba9e6c 100644 --- a/src/Services/ProjectSystem/ProjectBuildHelper.php +++ b/src/Services/ProjectSystem/ProjectBuildHelper.php @@ -22,10 +22,13 @@ declare(strict_types=1); */ 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 @@ use App\Services\Parts\PartLotWithdrawAddHelper; */ 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 @@ class ProjectBuildHelper $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 @@ class ProjectBuildHelper } /** - * 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 @@ class ProjectBuildHelper 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 @@ class ProjectBuildHelper * 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); } } } - if ($buildRequest->getAddBuildsToBuildsPart()) { - $this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $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 ($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 036797f6..f16431b3 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Services\Trees; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\AttachmentType; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; @@ -175,6 +176,12 @@ class ToolsTreeBuilder $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 73ffa5ba..fa9935c8 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -22,6 +22,7 @@ declare(strict_types=1); 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 @@ class TreeViewGenerator $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 @@ class TreeViewGenerator 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 @@ class TreeViewGenerator 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 762ebb09..086b21c5 100644 --- a/src/Twig/EntityExtension.php +++ b/src/Twig/EntityExtension.php @@ -22,6 +22,7 @@ declare(strict_types=1); */ 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 @@ final class EntityExtension extends AbstractExtension 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 00000000..dd3bc19e --- /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 00000000..9d8c2e56 --- /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 00000000..d8b3ab25 --- /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 1a995069..dcf8c64c 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 00000000..d8d8e657 --- /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 00000000..0123ab01 --- /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 00000000..8f01607c --- /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 00000000..53168b43 --- /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 00000000..6a2ca3e0 --- /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 00000000..780c8c60 --- /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 00000000..495072b8 --- /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 00000000..508b2b06 --- /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 00000000..8c92c5e9 --- /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 00000000..f5dac1e6 --- /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 00000000..d59005e0 --- /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 366d42fe..2e55147a 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 96b71bf0..def23500 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 00000000..c5acebda --- /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 bd1d2aa7..3ddb4f7f 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 a8f772e9..340b8670 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 00a68d7d..699648eb 100644 --- a/tests/Entity/Attachments/AttachmentTest.php +++ b/tests/Entity/Attachments/AttachmentTest.php @@ -24,6 +24,8 @@ namespace App\Tests\Entity\Attachments; 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 @@ class AttachmentTest extends TestCase 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 00000000..210e3301 --- /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 00000000..c513ed8d --- /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 00000000..b8aa0ddc --- /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 1f234450..f5823e8d 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 d7258986..f231163a 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 06326a21..37fb8b0d 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 @@ -9860,6 +9872,18 @@ Element 1 -> Element 1.2 Bauteil + + + project.bom.assembly + Baugruppe + + + + + project.bom.partOrAssembly + Auswahl + + project.bom.add_entry @@ -9938,6 +9962,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 @@ -10214,6 +10274,12 @@ Element 1 -> Element 1.2 vorhanden + + + project.builds.no_stock + kein Lager angegeben + + project.builds.needed @@ -10286,6 +10352,12 @@ Element 1 -> Element 1.2 Ziel-Bestand + + + project.build.builds_part_lot_label + %name% (%quantity% benötigt) + + project.builds.number_of_builds @@ -12929,6 +13001,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 cc17d9be..8fdb801b 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 a2ec2f65..210d8192 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 @@ -9861,6 +9873,18 @@ Element 1 -> Element 1.2 Part + + + project.bom.assembly + Assembly + + + + + project.bom.partOrAssembly + Selection + + project.bom.add_entry @@ -9939,6 +9963,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 @@ -10215,6 +10275,12 @@ Element 1 -> Element 1.2 stocked + + + project.builds.no_stock + no stock specified + + project.builds.needed @@ -10287,6 +10353,12 @@ Element 1 -> Element 1.2 Target lot + + + project.build.builds_part_lot_label + %name% (%quantity% needed) + + project.builds.number_of_builds @@ -12930,6 +13002,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 fce38e52..c3bf9636 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 292dbafa..5362f939 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 828304eb..0ea57d9f 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 4becc319..5de5f83e 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 760533d7..b7392b4d 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 b769e273..7290e5fe 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 62570acb..540b9e35 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 668c32f2..bf924444 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 c298266a..06354533 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 21149f0e..9a9dea4c 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 5cccd388..203382c8 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, ob die Authenticator App korrekt eingerichtet ist und ob 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 9ef5b3de..ee27863c 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 6ad14460..86525b6a 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 e86ab9cc..e9bf3259 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 29e32a16..9c9c3960 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 7043f4f3..2f747bc5 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 01cc3f77..0156ffef 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 6c997798..2cc4aef4 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 0f97c478..4049b453 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 08c9f014..3eab5c4e 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 + 必须选择零件或为非零件指定名称! + +