diff --git a/.env b/.env index 982d4bbd0..f5a2d16de 100644 --- a/.env +++ b/.env @@ -35,6 +35,13 @@ DATABASE_EMULATE_NATURAL_SORT=0 # This must end with a slash! DEFAULT_URI="https://partdb.changeme.invalid/" +# Use an %%ipn%% placeholder in the name of a assembly. Placeholder is replaced with the ipn input while saving. +CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME=0 +# Set this to 0 to allow to enter already available IPN. In this case a unique increment is appended to the user input. +ENFORCE_UNIQUE_IPN=1 +# Define the number of digits used for the incremental numbering of parts in the IPN (Internal Part Number) autocomplete system. +AUTOCOMPLETE_PART_DIGITS=4 + ################################################################################### # Email settings ################################################################################### @@ -60,7 +67,6 @@ ERROR_PAGE_ADMIN_EMAIL='' # If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them... ERROR_PAGE_SHOW_HELP=1 - ################################################################################### # SAML Single sign on-settings ################################################################################### diff --git a/assets/controllers/elements/assembly_select_controller.js b/assets/controllers/elements/assembly_select_controller.js new file mode 100644 index 000000000..98702d419 --- /dev/null +++ b/assets/controllers/elements/assembly_select_controller.js @@ -0,0 +1,70 @@ +import {Controller} from "@hotwired/stimulus"; + +import "tom-select/dist/css/tom-select.bootstrap5.css"; +import '../../css/components/tom-select_extensions.css'; +import TomSelect from "tom-select"; +import {marked} from "marked"; + +export default class extends Controller { + _tomSelect; + + connect() { + + let settings = { + allowEmptyOption: true, + plugins: ['dropdown_input', 'clear_button'], + searchField: ["name", "description", "category", "footprint"], + valueField: "id", + labelField: "name", + preload: "focus", + render: { + item: (data, escape) => { + return '' + (data.image ? "" : "") + escape(data.name) + ''; + }, + option: (data, escape) => { + if(data.text) { + return '' + escape(data.text) + ''; + } + + let tmp = '
' + + "
" + + (data.image ? "" : "") + + "
" + + "
" + + '
' + escape(data.name) + '
' + + (data.description ? '

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

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

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

' + + '
'; + } + } + }; + + + if (this.element.dataset.autocomplete) { + const base_url = this.element.dataset.autocomplete; + settings.valueField = "id"; + settings.load = (query, callback) => { + const url = base_url.replace('__QUERY__', encodeURIComponent(query)); + + fetch(url) + .then(response => response.json()) + .then(json => {callback(json);}) + .catch(() => { + callback() + }); + }; + + + this._tomSelect = new TomSelect(this.element, settings); + //this._tomSelect.clearOptions(); + } + } + + disconnect() { + super.disconnect(); + //Destroy the TomSelect instance + this._tomSelect.destroy(); + } +} \ No newline at end of file diff --git a/assets/controllers/elements/ckeditor_controller.js b/assets/controllers/elements/ckeditor_controller.js index 62a48b151..7f55dd5ca 100644 --- a/assets/controllers/elements/ckeditor_controller.js +++ b/assets/controllers/elements/ckeditor_controller.js @@ -78,6 +78,15 @@ export default class extends Controller { editor_div.classList.add(...new_classes.split(",")); } + // Automatic synchronization of source input + editor.model.document.on("change:data", () => { + editor.updateSourceElement(); + + // Dispatch the input event for further treatment + const event = new Event("input"); + this.element.dispatchEvent(event); + }); + //This return is important! Otherwise we get mysterious errors in the console //See: https://github.com/ckeditor/ckeditor5/issues/5897#issuecomment-628471302 return editor; diff --git a/assets/controllers/elements/ipn_suggestion_controller.js b/assets/controllers/elements/ipn_suggestion_controller.js new file mode 100644 index 000000000..e7289a91e --- /dev/null +++ b/assets/controllers/elements/ipn_suggestion_controller.js @@ -0,0 +1,250 @@ +import { Controller } from "@hotwired/stimulus"; +import "../../css/components/autocomplete_bootstrap_theme.css"; + +export default class extends Controller { + static targets = ["input"]; + static values = { + partId: Number, + partCategoryId: Number, + partDescription: String, + suggestions: Object, + commonSectionHeader: String, // Dynamic header for common Prefixes + partIncrementHeader: String, // Dynamic header for new possible part increment + suggestUrl: String, + }; + + connect() { + this.configureAutocomplete(); + this.watchCategoryChanges(); + this.watchDescriptionChanges(); + } + + templates = { + commonSectionHeader({ title, html }) { + return html` +
+
+ ${title} +
+
+
+ `; + }, + partIncrementHeader({ title, html }) { + return html` +
+
+ ${title} +
+
+
+ `; + }, + list({ html }) { + return html` + + `; + }, + item({ suggestion, description, html }) { + return html` +
  • +
    +
    +
    + + + +
    +
    +
    ${suggestion}
    +
    ${description}
    +
    +
    +
    +
  • + `; + }, + }; + + configureAutocomplete() { + const inputField = this.inputTarget; + const commonPrefixes = this.suggestionsValue.commonPrefixes || []; + const prefixesPartIncrement = this.suggestionsValue.prefixesPartIncrement || []; + const commonHeader = this.commonSectionHeaderValue; + const partIncrementHeader = this.partIncrementHeaderValue; + + if (!inputField || (!commonPrefixes.length && !prefixesPartIncrement.length)) return; + + // Check whether the panel should be created at the update + if (this.isPanelInitialized) { + const existingPanel = inputField.parentNode.querySelector(".aa-Panel"); + if (existingPanel) { + // Only remove the panel in the update phase + + existingPanel.remove(); + } + } + + // Create panel + const panel = document.createElement("div"); + panel.classList.add("aa-Panel"); + panel.style.display = "none"; + + // Create panel layout + const panelLayout = document.createElement("div"); + panelLayout.classList.add("aa-PanelLayout", "aa-Panel--scrollable"); + + // Section for prefixes part increment + if (prefixesPartIncrement.length) { + const partIncrementSection = document.createElement("section"); + partIncrementSection.classList.add("aa-Source"); + + const partIncrementHeaderHtml = this.templates.partIncrementHeader({ + title: partIncrementHeader, + html: String.raw, + }); + partIncrementSection.innerHTML += partIncrementHeaderHtml; + + const partIncrementList = document.createElement("ul"); + partIncrementList.classList.add("aa-List"); + partIncrementList.setAttribute("role", "listbox"); + + prefixesPartIncrement.forEach((prefix) => { + const itemHTML = this.templates.item({ + suggestion: prefix.title, + description: prefix.description, + html: String.raw, + }); + partIncrementList.innerHTML += itemHTML; + }); + + partIncrementSection.appendChild(partIncrementList); + panelLayout.appendChild(partIncrementSection); + } + + // Section for common prefixes + if (commonPrefixes.length) { + const commonSection = document.createElement("section"); + commonSection.classList.add("aa-Source"); + + const commonSectionHeader = this.templates.commonSectionHeader({ + title: commonHeader, + html: String.raw, + }); + commonSection.innerHTML += commonSectionHeader; + + const commonList = document.createElement("ul"); + commonList.classList.add("aa-List"); + commonList.setAttribute("role", "listbox"); + + commonPrefixes.forEach((prefix) => { + const itemHTML = this.templates.item({ + suggestion: prefix.title, + description: prefix.description, + html: String.raw, + }); + commonList.innerHTML += itemHTML; + }); + + commonSection.appendChild(commonList); + panelLayout.appendChild(commonSection); + } + + panel.appendChild(panelLayout); + inputField.parentNode.appendChild(panel); + + inputField.addEventListener("focus", () => { + panel.style.display = "block"; + }); + + inputField.addEventListener("blur", () => { + setTimeout(() => { + panel.style.display = "none"; + }, 100); + }); + + // Selection of an item + panelLayout.addEventListener("mousedown", (event) => { + const target = event.target.closest("li"); + + if (target) { + inputField.value = target.dataset.suggestion; + panel.style.display = "none"; + } + }); + + this.isPanelInitialized = true; + }; + + watchCategoryChanges() { + const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]'); + const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]'); + this.previousCategoryId = Number(this.partCategoryIdValue); + + if (categoryField) { + categoryField.addEventListener("change", () => { + const categoryId = Number(categoryField.value); + const description = String(descriptionField.value); + + // Check whether the category has changed compared to the previous ID + if (categoryId !== this.previousCategoryId) { + this.fetchNewSuggestions(categoryId, description); + this.previousCategoryId = categoryId; + } + }); + } + } + + watchDescriptionChanges() { + const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]'); + const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]'); + this.previousDescription = String(this.partDescriptionValue); + + if (descriptionField) { + descriptionField.addEventListener("input", () => { + const categoryId = Number(categoryField.value); + const description = String(descriptionField.value); + + // Check whether the description has changed compared to the previous one + if (description !== this.previousDescription) { + this.fetchNewSuggestions(categoryId, description); + this.previousDescription = description; + } + }); + } + } + + fetchNewSuggestions(categoryId, description) { + const baseUrl = this.suggestUrlValue; + const partId = this.partIdValue; + const truncatedDescription = description.length > 150 ? description.substring(0, 150) : description; + const encodedDescription = this.base64EncodeUtf8(truncatedDescription); + const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}&description=${encodedDescription}`; + + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error(`Error when calling up the IPN-suggestions: ${response.status}`); + } + return response.json(); + }) + .then((data) => { + this.suggestionsValue = data; + this.configureAutocomplete(); + }) + .catch((error) => { + console.error("Errors when loading the new IPN-suggestions:", error); + }); + }; + + base64EncodeUtf8(text) { + const utf8Bytes = new TextEncoder().encode(text); + return btoa(String.fromCharCode(...utf8Bytes)); + }; +} \ No newline at end of file diff --git a/assets/controllers/elements/part_select_controller.js b/assets/controllers/elements/part_select_controller.js index 0658f4b46..2b658d526 100644 --- a/assets/controllers/elements/part_select_controller.js +++ b/assets/controllers/elements/part_select_controller.js @@ -12,7 +12,7 @@ export default class extends Controller { let settings = { allowEmptyOption: true, - plugins: ['dropdown_input'], + plugins: ['dropdown_input', 'clear_button'], searchField: ["name", "description", "category", "footprint"], valueField: "id", labelField: "name", diff --git a/assets/controllers/elements/project_select_controller.js b/assets/controllers/elements/project_select_controller.js new file mode 100644 index 000000000..98702d419 --- /dev/null +++ b/assets/controllers/elements/project_select_controller.js @@ -0,0 +1,70 @@ +import {Controller} from "@hotwired/stimulus"; + +import "tom-select/dist/css/tom-select.bootstrap5.css"; +import '../../css/components/tom-select_extensions.css'; +import TomSelect from "tom-select"; +import {marked} from "marked"; + +export default class extends Controller { + _tomSelect; + + connect() { + + let settings = { + allowEmptyOption: true, + plugins: ['dropdown_input', 'clear_button'], + searchField: ["name", "description", "category", "footprint"], + valueField: "id", + labelField: "name", + preload: "focus", + render: { + item: (data, escape) => { + return '' + (data.image ? "" : "") + escape(data.name) + ''; + }, + option: (data, escape) => { + if(data.text) { + return '' + escape(data.text) + ''; + } + + let tmp = '
    ' + + "
    " + + (data.image ? "" : "") + + "
    " + + "
    " + + '
    ' + escape(data.name) + '
    ' + + (data.description ? '

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

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

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

    ' + + '
    '; + } + } + }; + + + if (this.element.dataset.autocomplete) { + const base_url = this.element.dataset.autocomplete; + settings.valueField = "id"; + settings.load = (query, callback) => { + const url = base_url.replace('__QUERY__', encodeURIComponent(query)); + + fetch(url) + .then(response => response.json()) + .then(json => {callback(json);}) + .catch(() => { + callback() + }); + }; + + + this._tomSelect = new TomSelect(this.element, settings); + //this._tomSelect.clearOptions(); + } + } + + disconnect() { + super.disconnect(); + //Destroy the TomSelect instance + this._tomSelect.destroy(); + } +} \ No newline at end of file diff --git a/assets/controllers/elements/toggle_visibility_controller.js b/assets/controllers/elements/toggle_visibility_controller.js new file mode 100644 index 000000000..51c9cb338 --- /dev/null +++ b/assets/controllers/elements/toggle_visibility_controller.js @@ -0,0 +1,62 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + + static values = { + classes: Array + }; + + connect() { + this.displayCheckbox = this.element.querySelector("#display"); + this.displaySelect = this.element.querySelector("select#display"); + + if (this.displayCheckbox) { + this.toggleContainers(this.displayCheckbox.checked); + + this.displayCheckbox.addEventListener("change", (event) => { + this.toggleContainers(event.target.checked); + }); + } + + if (this.displaySelect) { + this.toggleContainers(this.hasDisplaySelectValue()); + + this.displaySelect.addEventListener("change", () => { + this.toggleContainers(this.hasDisplaySelectValue()); + }); + } + + } + + /** + * Check whether a value was selected in the selectbox + * @returns {boolean} True when a value has not been selected that is not empty + */ + hasDisplaySelectValue() { + return this.displaySelect && this.displaySelect.value !== ""; + } + + /** + * Hides specified containers if the state is active (checkbox checked or select with value). + * + * @param {boolean} isActive - True when the checkbox is activated or the selectbox has a value. + */ + toggleContainers(isActive) { + if (!Array.isArray(this.classesValue) || this.classesValue.length === 0) { + return; + } + + this.classesValue.forEach((cssClass) => { + const elements = document.querySelectorAll(`.${cssClass}`); + + if (!elements.length) { + return; + } + + elements.forEach((element) => { + element.style.display = isActive ? "none" : ""; + }); + }); + } + +} diff --git a/assets/controllers/pages/dont_check_quantity_checkbox_controller.js b/assets/controllers/pages/dont_check_quantity_checkbox_controller.js index 2abd3d77b..f3e8cb900 100644 --- a/assets/controllers/pages/dont_check_quantity_checkbox_controller.js +++ b/assets/controllers/pages/dont_check_quantity_checkbox_controller.js @@ -38,7 +38,7 @@ export default class extends Controller { connect() { //Add event listener to the checkbox - this.getCheckbox().addEventListener('change', this.toggleInputLimits.bind(this)); + this.getCheckbox()?.addEventListener('change', this.toggleInputLimits.bind(this)); } toggleInputLimits() { diff --git a/assets/css/app/images.css b/assets/css/app/images.css index 0212a85b7..132cab99b 100644 --- a/assets/css/app/images.css +++ b/assets/css/app/images.css @@ -61,3 +61,8 @@ .object-fit-cover { object-fit: cover; } + +.assembly-table-image { + max-height: 40px; + object-fit: contain; +} diff --git a/assets/js/lib/datatables.js b/assets/js/lib/datatables.js index 67bab02db..7c82439cc 100644 --- a/assets/js/lib/datatables.js +++ b/assets/js/lib/datatables.js @@ -13,11 +13,16 @@ * Initializes the datatable dynamically. */ $.fn.initDataTables = function(config, options) { - //Update default used url, so it reflects the current location (useful on single side apps) //CHANGED jbtronics: Preserve the get parameters (needed so we can pass additional params to query) $.fn.initDataTables.defaults.url = window.location.origin + window.location.pathname + window.location.search; + $.fn.dataTable.ext.errMode = function(settings, helpPage, message) { + if (message.includes('ColReorder')) { + console.warn('ColReorder does not fit the number of columns', message); + } + }; + var root = this, config = $.extend({}, $.fn.initDataTables.defaults, config), state = '' @@ -105,7 +110,6 @@ } } - root.html(data.template); dt = $('table', root).DataTable(dtOpts); if (config.state !== 'none') { diff --git a/config/parameters.yaml b/config/parameters.yaml index 154fbd8a5..9239d3400 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -9,16 +9,35 @@ parameters: # This is used as workaround for places where we can not access the settings directly (like the 2FA application names) partdb.title: '%env(string:settings:customization:instanceName)%' # The title shown inside of Part-DB (e.g. in the navbar and on homepage) partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl'] # The languages that are shown in user drop down menu + partdb.enforce_change_comments_for: '%env(csv:ENFORCE_CHANGE_COMMENTS_FOR)%' # The actions for which a change comment is required (e.g. "part_edit", "part_create", etc.). If this is empty, change comments are not required at all. + partdb.autocomplete_part_digits: '%env(trim:string:AUTOCOMPLETE_PART_DIGITS)%' # The number of digits used for the incremental numbering of parts in the IPN (Internal Part Number) autocomplete system. partdb.default_uri: '%env(string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails partdb.db.emulate_natural_sort: '%env(bool:DATABASE_EMULATE_NATURAL_SORT)%' # If this is set to true, natural sorting is emulated on platforms that do not support it natively. This can be slow on large datasets. + partdb.create_assembly_use_ipn_placeholder_in_name: '%env(bool:CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME)%' # Use an %%ipn%% placeholder in the name of an assembly. Placeholder is replaced with the ipn input while saving. + + partdb.data_sources.synonyms: # Define your own synonyms for the given data sources + # Possible datasources: category, storagelocation, footprint, manufacturer, supplier, project, assembly + # Possible locales like the ones in 'partdb.locale_menu': en, de, it, fr, ru, ja, cs, da, zh, pl + #category: + #de: 'Bauteil Kategorien' + #en: 'Part categories' + #project: + #de: 'Geräte' + #en: 'Devices' + #assembly: + #de: 'Zusammengestellte Baugruppe' + #en: 'Combined assembly' + + ###################################################################################################################### # Users and Privacy ###################################################################################################################### partdb.gdpr_compliance: true # If this option is activated, IP addresses are anonymized to be GDPR compliant partdb.users.email_pw_reset: '%env(bool:ALLOW_EMAIL_PW_RESET)%' # Config if users are able, to reset their password by email. By default this enabled, when a mail server is configured. + partdb.users.enforce_unique_ipn: '%env(bool:ENFORCE_UNIQUE_IPN)%' # Config if users are able, to enter an already available IPN. In this case a unique increment is appended to the user input. ###################################################################################################################### # Mail settings @@ -43,7 +62,6 @@ parameters: ###################################################################################################################### partdb.saml.enabled: '%env(bool:SAML_ENABLED)%' # If this is set to true, SAML authentication is enabled - ###################################################################################################################### # Miscellaneous ###################################################################################################################### diff --git a/config/permissions.yaml b/config/permissions.yaml index 8cbd60c3f..2423177da 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -24,7 +24,7 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co label: "perm.read" # If a part can be read by a user, he can also see all the datastructures (except devices) alsoSet: ['storelocations.read', 'footprints.read', 'categories.read', 'suppliers.read', 'manufacturers.read', - 'currencies.read', 'attachment_types.read', 'measurement_units.read'] + 'currencies.read', 'attachment_types.read', 'measurement_units.read', 'part_custom_states.read'] apiTokenRole: ROLE_API_READ_ONLY edit: label: "perm.edit" @@ -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" @@ -133,6 +137,10 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co <<: *PART_CONTAINING label: "perm.measurement_units" + part_custom_states: + <<: *PART_CONTAINING + label: "perm.part_custom_states" + tools: label: "perm.part.tools" operations: diff --git a/config/services.yaml b/config/services.yaml index 17611ceab..beef782cf 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -164,6 +164,9 @@ services: arguments: $saml_enabled: '%partdb.saml.enabled%' + App\Validator\Constraints\AssemblySystem\AssemblyCycleValidator: + tags: [ 'validator.constraint_validator' ] + #################################################################################################################### # Table settings #################################################################################################################### @@ -188,6 +191,25 @@ services: $fontDirectory: '%kernel.project_dir%/var/dompdf/fonts/' $tmpDirectory: '%kernel.project_dir%/var/dompdf/tmp/' + #################################################################################################################### + # Trees + #################################################################################################################### + App\Services\Trees\TreeViewGenerator: + arguments: + $dataSourceSynonyms: '%partdb.data_sources.synonyms%' + App\Services\Trees\ToolsTreeBuilder: + arguments: + $dataSourceSynonyms: '%partdb.data_sources.synonyms%' + + #################################################################################################################### + # Twig Extensions + #################################################################################################################### + + App\Twig\DataSourceNameExtension: + arguments: + $dataSourceSynonyms: '%partdb.data_sources.synonyms%' + tags: [ 'twig.extension' ] + #################################################################################################################### # Part info provider system #################################################################################################################### @@ -231,6 +253,30 @@ services: tags: - { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' } + App\Controller\PartController: + bind: + $autocompletePartDigits: '%partdb.autocomplete_part_digits%' + + App\Controller\TypeaheadController: + bind: + $autocompletePartDigits: '%partdb.autocomplete_part_digits%' + + App\Repository\PartRepository: + arguments: + $translator: '@translator' + tags: ['doctrine.repository_service'] + + App\EventSubscriber\UserSystem\PartUniqueIpnSubscriber: + arguments: + $enforceUniqueIpn: '%partdb.users.enforce_unique_ipn%' + tags: + - { name: doctrine.event_subscriber } + + App\Validator\Constraints\UniquePartIpnValidator: + arguments: + $enforceUniqueIpn: '%partdb.users.enforce_unique_ipn%' + tags: [ 'validator.constraint_validator' ] + # We are needing this service inside a migration, where only the container is injected. So we need to define it as public, to access it from the container. App\Services\UserSystem\PermissionPresetsHelper: public: true diff --git a/docs/configuration.md b/docs/configuration.md index d4b217816..ea02b227f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -116,6 +116,10 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept value should be handled as confidential data and not shared publicly. * `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the part image gallery +* `AUTOCOMPLETE_PART_DIGITS`: Defines the fixed number of digits used as the increment at the end of an IPN (Internal Part Number). + IPN prefixes, maintained within part categories and their hierarchy, form the foundation for suggesting complete IPNs. + These suggestions become accessible during IPN input of a part. The constant specifies the digits used to calculate and assign + unique increments for parts within a category hierarchy, ensuring consistency and uniqueness in IPN generation. ### E-Mail settings (all env only) @@ -128,6 +132,8 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept sent from. * `ALLOW_EMAIL_PW_RESET`: Set this value to true, if you want to allow users to reset their password via an email notification. You have to configure the mail provider first before via the MAILER_DSN setting. +* `ENFORCE_UNIQUE_IPN`: Set this value to false, if you want to allow users to enter a already available IPN for a part entry. + In this case a unique increment is appended to the user input. ### Table related settings @@ -136,7 +142,14 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept * `TABLE_PARTS_DEFAULT_COLUMNS`: The columns in parts tables, which are visible by default (when loading table for first time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns - are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`. + are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `partCustomState`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`. +* `TABLE_ASSEMBLIES_DEFAULT_COLUMNS`: The columns in assemblies tables, which are visible by default (when loading table for first time). + Also specify the default order of the columns. This is a comma separated list of column names. Available columns + are: `name`, `id`, `ipn`, `description`, `referencedAssemblies`, `edit`, `addedDate`, `lastModified`. +* `TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS`: The columns in assemblies bom tables, which are visible by default (when loading table for first time). + Also specify the default order of the columns. This is a comma separated list of column names. Available columns + are: `quantity`, `name`, `id`, `ipn`, `description`, `addedDate`, `lastModified`. +* `CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME`: Use an %%ipn%% placeholder in the name of a assembly. Placeholder is replaced with the ipn input while saving. ### History/Eventlog-related settings diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php new file mode 100644 index 000000000..0c64e2507 --- /dev/null +++ b/migrations/Version20250304081039.php @@ -0,0 +1,282 @@ +addSql(<<<'SQL' + CREATE TABLE assemblies ( + id INT AUTO_INCREMENT NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, + name VARCHAR(255) NOT NULL, + comment LONGTEXT NOT NULL, + not_selectable TINYINT(1) NOT NULL, + alternative_names LONGTEXT DEFAULT NULL, + order_quantity INT NOT NULL, + status VARCHAR(64) DEFAULT NULL, + order_only_missing_parts TINYINT(1) NOT NULL, + description LONGTEXT NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + INDEX IDX_5F3832C0727ACA70 (parent_id), + INDEX IDX_5F3832C0EA7100A1 (id_preview_attachment), + PRIMARY KEY(id) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE assembly_bom_entries ( + id INT AUTO_INCREMENT NOT NULL, + id_assembly INT DEFAULT NULL, + id_part INT DEFAULT NULL, + id_project INT DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames LONGTEXT NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment LONGTEXT NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + price_currency_id INT DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + INDEX IDX_8C74887E2F180363 (id_assembly), + INDEX IDX_8C74887EC22F6CC4 (id_part), + INDEX IDX_8C74887EF12E799E (id_project), + INDEX IDX_8C74887E3FFDCD60 (price_currency_id), + PRIMARY KEY(id) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES `attachments` (id) ON DELETE SET NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E2F180363 FOREIGN KEY (id_assembly) REFERENCES assemblies (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES `parts` (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EF12E799E FOREIGN KEY (id_project) REFERENCES `projects` (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0727ACA70 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP FOREIGN KEY FK_5F3832C0EA7100A1 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E2F180363 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EC22F6CC4 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887EF12E799E + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E3FFDCD60 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assemblies + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assembly_bom_entries + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE assemblies ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + order_quantity INTEGER NOT NULL, + order_only_missing_parts BOOLEAN NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + status VARCHAR(64) DEFAULT NULL, + ipn VARCHAR(100) DEFAULT NULL, + description CLOB NOT NULL, + alternative_names CLOB DEFAULT NULL, + CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_5F3832C0727ACA70 ON assemblies (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_5F3832C0EA7100A1 ON assemblies (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_5F3832C03D721C14 ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX assembly_idx_ipn ON assemblies (ipn) + SQL); + + $this->addSql(<<<'SQL' + CREATE TABLE assembly_bom_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_assembly INTEGER DEFAULT NULL, + id_part INTEGER DEFAULT NULL, + id_referenced_assembly INTEGER DEFAULT NULL, + price_currency_id INTEGER DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames CLOB NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment CLOB NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT FK_8C74887E4AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887E22522999 FOREIGN KEY (id_referenced_assembly) REFERENCES assemblies (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E4AD2039E ON assembly_bom_entries (id_assembly) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887EC22F6CC4 ON assembly_bom_entries (id_part) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E22522999 ON assembly_bom_entries (id_referenced_assembly) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E3FFDCD60 ON assembly_bom_entries (price_currency_id) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP TABLE assembly_bom_entries + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assemblies + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE assemblies ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + comment TEXT NOT NULL, + not_selectable BOOLEAN NOT NULL, + alternative_names TEXT DEFAULT NULL, + order_quantity INT NOT NULL, + status VARCHAR(64) DEFAULT NULL, + order_only_missing_parts BOOLEAN NOT NULL, + description TEXT NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, + PRIMARY KEY(id) + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_5F3832C0727ACA70 ON assemblies (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_5F3832C0EA7100A1 ON assemblies (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE assembly_bom_entries ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + id_assembly INT DEFAULT NULL, + id_part INT DEFAULT NULL, + quantity DOUBLE PRECISION NOT NULL, + mountnames TEXT NOT NULL, + name VARCHAR(255) DEFAULT NULL, + comment TEXT NOT NULL, + price NUMERIC(11, 5) DEFAULT NULL, + price_currency_id INT DEFAULT NULL, + last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY(id) + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E4AD2039E ON assembly_bom_entries (id_assembly) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887EC22F6CC4 ON assembly_bom_entries (id_part) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E3FFDCD60 ON assembly_bom_entries (price_currency_id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD CONSTRAINT FK_5F3832C0EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E4AD2039E FOREIGN KEY (id_assembly) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887EC22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E3FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP CONSTRAINT FK_5F3832C0727ACA70 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP CONSTRAINT FK_5F3832C0EA7100A1 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887E4AD2039E + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887EC22F6CC4 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887EF12E799E + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887E3FFDCD60 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assemblies + SQL); + $this->addSql(<<<'SQL' + DROP TABLE assembly_bom_entries + SQL); + } +} diff --git a/migrations/Version20250304154507.php b/migrations/Version20250304154507.php new file mode 100644 index 000000000..bb25b8022 --- /dev/null +++ b/migrations/Version20250304154507.php @@ -0,0 +1,424 @@ +addSql(<<<'SQL' + ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL AFTER built_project_id + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON parts (built_assembly_id) + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE parts DROP FOREIGN KEY FK_6940A7FECC660B3C + SQL); + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_6940A7FECC660B3C ON parts + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE `parts` DROP built_assembly_id + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__parts AS + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM parts + SQL); + $this->addSql('DROP TABLE parts'); + + $this->addSql(<<<'SQL' + CREATE TABLE "parts" + ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + id_category INTEGER NOT NULL, + id_footprint INTEGER DEFAULT NULL, + id_part_unit INTEGER DEFAULT NULL, + id_manufacturer INTEGER DEFAULT NULL, + order_orderdetails_id INTEGER DEFAULT NULL, + built_project_id INTEGER DEFAULT NULL, + built_assembly_id INTEGER DEFAULT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + needs_review BOOLEAN NOT NULL, + tags CLOB NOT NULL, + mass DOUBLE PRECISION DEFAULT NULL, + description CLOB NOT NULL, + comment CLOB NOT NULL, + visible BOOLEAN NOT NULL, + favorite BOOLEAN NOT NULL, + minamount DOUBLE PRECISION NOT NULL, + manufacturer_product_url CLOB NOT NULL, + manufacturer_product_number VARCHAR(255) NOT NULL, + manufacturing_status VARCHAR(255) DEFAULT NULL, + order_quantity INTEGER NOT NULL, + manual_order BOOLEAN NOT NULL, + ipn VARCHAR(100) DEFAULT NULL, + provider_reference_provider_key VARCHAR(255) DEFAULT NULL, + provider_reference_provider_id VARCHAR(255) DEFAULT NULL, + provider_reference_provider_url VARCHAR(255) DEFAULT NULL, + provider_reference_last_updated DATETIME DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_value VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO parts ( + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + ) SELECT * FROM __temp__parts + SQL); + $this->addSql('DROP TABLE __temp__parts'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON "parts" (built_assembly_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_ipn ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_name ON "parts" (name) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__parts AS + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM parts + SQL); + + $this->addSql('DROP TABLE parts'); + + $this->addSql(<<<'SQL' + CREATE TABLE "parts" + ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + id_category INTEGER NOT NULL, + id_footprint INTEGER DEFAULT NULL, + id_part_unit INTEGER DEFAULT NULL, + id_manufacturer INTEGER DEFAULT NULL, + order_orderdetails_id INTEGER DEFAULT NULL, + built_project_id INTEGER DEFAULT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + needs_review BOOLEAN NOT NULL, + tags CLOB NOT NULL, + mass DOUBLE PRECISION DEFAULT NULL, + description CLOB NOT NULL, + comment CLOB NOT NULL, + visible BOOLEAN NOT NULL, + favorite BOOLEAN NOT NULL, + minamount DOUBLE PRECISION NOT NULL, + manufacturer_product_url CLOB NOT NULL, + manufacturer_product_number VARCHAR(255) NOT NULL, + manufacturing_status VARCHAR(255) DEFAULT NULL, + order_quantity INTEGER NOT NULL, + manual_order BOOLEAN NOT NULL, + ipn VARCHAR(100) DEFAULT NULL, + provider_reference_provider_key VARCHAR(255) DEFAULT NULL, + provider_reference_provider_id VARCHAR(255) DEFAULT NULL, + provider_reference_provider_url VARCHAR(255) DEFAULT NULL, + provider_reference_last_updated DATETIME DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_value VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO parts ( + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + ) SELECT * FROM __temp__parts + SQL); + + $this->addSql('DROP TABLE __temp__parts'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_ipn ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_name ON "parts" (name) + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD built_assembly_id INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON parts (built_assembly_id) + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FECC660B3C + SQL); + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_6940A7FECC660B3C + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE "parts" DROP built_assembly_id + SQL); + } +} diff --git a/migrations/Version20250321075747.php b/migrations/Version20250321075747.php new file mode 100644 index 000000000..b236796dc --- /dev/null +++ b/migrations/Version20250321075747.php @@ -0,0 +1,125 @@ +addSql(<<<'SQL' + CREATE TABLE part_custom_states ( + id INT AUTO_INCREMENT NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, + name VARCHAR(255) NOT NULL, + comment LONGTEXT NOT NULL, + not_selectable TINYINT(1) NOT NULL, + alternative_names LONGTEXT DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + INDEX IDX_F552745D727ACA70 (parent_id), + INDEX IDX_F552745DEA7100A1 (id_preview_attachment), + INDEX part_custom_state_name (name), + PRIMARY KEY(id) + ) + DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE part_custom_states ADD CONSTRAINT FK_F552745D727ACA70 FOREIGN KEY (parent_id) REFERENCES part_custom_states (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE part_custom_states ADD CONSTRAINT FK_F552745DEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON DELETE SET NULL + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE part_custom_states DROP FOREIGN KEY FK_F552745D727ACA70 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE part_custom_states DROP FOREIGN KEY FK_F552745DEA7100A1 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE part_custom_states + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE "part_custom_states" ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + name VARCHAR(255) NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + alternative_names CLOB DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT FK_F552745D727ACA70 FOREIGN KEY (parent_id) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_F5AF83CFEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_F552745D727ACA70 ON "part_custom_states" (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX part_custom_state_name ON "part_custom_states" (name) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP TABLE "part_custom_states" + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE "part_custom_states" ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id), + name VARCHAR(255) NOT NULL, + comment TEXT NOT NULL, + not_selectable BOOLEAN NOT NULL, + alternative_names TEXT DEFAULT NULL, + last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_F552745D727ACA70 ON "part_custom_states" (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_F552745DEA7100A1 ON "part_custom_states" (id_preview_attachment) + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE "part_custom_states" DROP CONSTRAINT FK_F552745D727ACA70 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE "part_custom_states" DROP CONSTRAINT FK_F552745DEA7100A1 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE "part_custom_states" + SQL); + } +} diff --git a/migrations/Version20250321141740.php b/migrations/Version20250321141740.php new file mode 100644 index 000000000..cdf0e17aa --- /dev/null +++ b/migrations/Version20250321141740.php @@ -0,0 +1,517 @@ +addSql(<<<'SQL' + ALTER TABLE parts ADD id_part_custom_state INT DEFAULT NULL AFTER id_part_unit + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES part_custom_states (id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state) + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE parts DROP FOREIGN KEY FK_6940A7FEA3ED1215 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_6940A7FEA3ED1215 ON parts + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts DROP id_part_custom_state + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__parts AS + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + built_assembly_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM parts + SQL); + + $this->addSql(<<<'SQL' + DROP TABLE parts + SQL); + + $this->addSql(<<<'SQL' + CREATE TABLE parts ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + id_category INTEGER NOT NULL, + id_footprint INTEGER DEFAULT NULL, + id_part_unit INTEGER DEFAULT NULL, + id_manufacturer INTEGER DEFAULT NULL, + id_part_custom_state INTEGER DEFAULT NULL, + order_orderdetails_id INTEGER DEFAULT NULL, + built_project_id INTEGER DEFAULT NULL, + built_assembly_id INTEGER DEFAULT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + needs_review BOOLEAN NOT NULL, + tags CLOB NOT NULL, + mass DOUBLE PRECISION DEFAULT NULL, + description CLOB NOT NULL, + comment CLOB NOT NULL, + visible BOOLEAN NOT NULL, + favorite BOOLEAN NOT NULL, + minamount DOUBLE PRECISION NOT NULL, + manufacturer_product_url CLOB NOT NULL, + manufacturer_product_number VARCHAR(255) NOT NULL, + manufacturing_status VARCHAR(255) DEFAULT NULL, + order_quantity INTEGER NOT NULL, + manual_order BOOLEAN NOT NULL, + ipn VARCHAR(100) DEFAULT NULL, + provider_reference_provider_key VARCHAR(255) DEFAULT NULL, + provider_reference_provider_id VARCHAR(255) DEFAULT NULL, + provider_reference_provider_url VARCHAR(255) DEFAULT NULL, + provider_reference_last_updated DATETIME DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_value VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO parts ( + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint) + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM __temp__parts + SQL); + + $this->addSql(<<<'SQL' + DROP TABLE __temp__parts + SQL); + + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_name ON parts (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_ipn ON parts (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_datet_name_last_id_needs ON parts (datetime_added, name, last_modified, id, needs_review) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON "parts" (built_assembly_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON parts (order_orderdetails_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEEA7100A1 ON parts (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE7E371A10 ON parts (id_footprint) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE5697F554 ON parts (id_category) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE2626CEF9 ON parts (id_part_unit) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__parts AS + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + built_assembly_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM "parts" + SQL); + $this->addSql(<<<'SQL' + DROP TABLE "parts" + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE "parts" ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + id_category INTEGER NOT NULL, + id_footprint INTEGER DEFAULT NULL, + id_part_unit INTEGER DEFAULT NULL, + id_manufacturer INTEGER DEFAULT NULL, + order_orderdetails_id INTEGER DEFAULT NULL, + built_project_id INTEGER DEFAULT NULL, + built_assembly_id INTEGER DEFAULT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + needs_review BOOLEAN NOT NULL, + tags CLOB NOT NULL, + mass DOUBLE PRECISION DEFAULT NULL, + description CLOB NOT NULL, + comment CLOB NOT NULL, + visible BOOLEAN NOT NULL, + favorite BOOLEAN NOT NULL, + minamount DOUBLE PRECISION NOT NULL, + manufacturer_product_url CLOB NOT NULL, + manufacturer_product_number VARCHAR(255) NOT NULL, + manufacturing_status VARCHAR(255) DEFAULT NULL, + order_quantity INTEGER NOT NULL, + manual_order BOOLEAN NOT NULL, + ipn VARCHAR(100) DEFAULT NULL, + provider_reference_provider_key VARCHAR(255) DEFAULT NULL, + provider_reference_provider_id VARCHAR(255) DEFAULT NULL, + provider_reference_provider_url VARCHAR(255) DEFAULT NULL, + provider_reference_last_updated DATETIME DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_value VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FECC660B3C FOREIGN KEY (built_assembly_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + $this->addSql(<<<'SQL' + INSERT INTO "parts" ( + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + built_assembly_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + ) SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + built_assembly_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM __temp__parts + SQL); + + $this->addSql(<<<'SQL' + DROP TABLE __temp__parts + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FECC660B3C ON "parts" (built_assembly_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_name ON "parts" (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_ipn ON "parts" (ipn) + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD id_part_custom_state INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state) + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FEA3ED1215 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_6940A7FEA3ED1215 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE "parts" DROP id_part_custom_state + SQL); + } +} diff --git a/migrations/Version20250325073036.php b/migrations/Version20250325073036.php new file mode 100644 index 000000000..0070bcbe3 --- /dev/null +++ b/migrations/Version20250325073036.php @@ -0,0 +1,327 @@ +addSql(<<<'SQL' + ALTER TABLE categories ADD COLUMN part_ipn_prefix VARCHAR(255) NOT NULL DEFAULT '' + SQL); + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_6940A7FE3D721C14 ON parts + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE categories DROP part_ipn_prefix + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn) + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__categories AS + SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM categories + SQL); + + $this->addSql('DROP TABLE categories'); + + $this->addSql(<<<'SQL' + CREATE TABLE categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + partname_hint CLOB NOT NULL, + partname_regex CLOB NOT NULL, + part_ipn_prefix VARCHAR(255) DEFAULT '' NOT NULL, + disable_footprints BOOLEAN NOT NULL, + disable_manufacturers BOOLEAN NOT NULL, + disable_autodatasheets BOOLEAN NOT NULL, + disable_properties BOOLEAN NOT NULL, + default_description CLOB NOT NULL, + default_comment CLOB NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + alternative_names CLOB DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO categories ( + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + ) SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM __temp__categories + SQL); + + $this->addSql('DROP TABLE __temp__categories'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668727ACA70 ON categories (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668EA7100A1 ON categories (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_name ON categories (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_parent_name ON categories (parent_id, name) + SQL); + + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_6940A7FE3D721C14 + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__categories AS + SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM categories + SQL); + + $this->addSql('DROP TABLE categories'); + + $this->addSql(<<<'SQL' + CREATE TABLE categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + partname_hint CLOB NOT NULL, + partname_regex CLOB NOT NULL, + disable_footprints BOOLEAN NOT NULL, + disable_manufacturers BOOLEAN NOT NULL, + disable_autodatasheets BOOLEAN NOT NULL, + disable_properties BOOLEAN NOT NULL, + default_description CLOB NOT NULL, + default_comment CLOB NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + alternative_names CLOB DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO categories ( + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + ) SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM __temp__categories + SQL); + + $this->addSql('DROP TABLE __temp__categories'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668727ACA70 ON categories (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668EA7100A1 ON categories (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_name ON categories (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_parent_name ON categories (parent_id, name) + SQL); + + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn) + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE categories ADD part_ipn_prefix VARCHAR(255) DEFAULT '' NOT NULL + SQL); + $this->addSql(<<<'SQL' + DROP INDEX uniq_6940a7fe3d721c14 + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE "categories" DROP part_ipn_prefix + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX uniq_6940a7fe3d721c14 ON "parts" (ipn) + SQL); + } +} diff --git a/migrations/Version20250624095045.php b/migrations/Version20250624095045.php new file mode 100644 index 000000000..d875f1d80 --- /dev/null +++ b/migrations/Version20250624095045.php @@ -0,0 +1,84 @@ +addSql(<<<'SQL' + ALTER TABLE assemblies ADD ipn VARCHAR(100) DEFAULT NULL AFTER status + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_5F3832C03D721C14 ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX assembly_idx_ipn ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e2f180363 TO IDX_8C74887E4AD2039E + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_5F3832C03D721C14 ON assemblies + SQL); + $this->addSql(<<<'SQL' + DROP INDEX assembly_idx_ipn ON assemblies + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP ipn + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e4ad2039e TO IDX_8C74887E2F180363 + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function sqLiteDown(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD ipn VARCHAR(100) DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_5F3832C03D721C14 ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX assembly_idx_ipn ON assemblies (ipn) + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_5F3832C03D721C14 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX assembly_idx_ipn + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP ipn + SQL); + } +} diff --git a/migrations/Version20250627130848.php b/migrations/Version20250627130848.php new file mode 100644 index 000000000..6223de13b --- /dev/null +++ b/migrations/Version20250627130848.php @@ -0,0 +1,78 @@ +addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD id_referenced_assembly INT DEFAULT NULL AFTER id_part + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E22522999 FOREIGN KEY (id_referenced_assembly) REFERENCES assemblies (id) ON DELETE SET NULL + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E22522999 ON assembly_bom_entries (id_referenced_assembly) + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP FOREIGN KEY FK_8C74887E22522999 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_8C74887E22522999 ON assembly_bom_entries + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP id_referenced_assembly + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function sqLiteDown(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD id_referenced_assembly INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries ADD CONSTRAINT FK_8C74887E22522999 FOREIGN KEY (id_referenced_assembly) REFERENCES assemblies (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_8C74887E22522999 ON assembly_bom_entries (id_referenced_assembly) + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP CONSTRAINT FK_8C74887E22522999 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_8C74887E22522999 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries DROP id_referenced_assembly + SQL); + } +} diff --git a/migrations/Version20250910113423.php b/migrations/Version20250910113423.php new file mode 100644 index 000000000..0e65e4aba --- /dev/null +++ b/migrations/Version20250910113423.php @@ -0,0 +1,51 @@ +addSql('ALTER TABLE assembly_bom_entries DROP FOREIGN KEY `FK_8C74887EF12E799E`'); + $this->addSql('DROP INDEX IDX_8C74887EF12E799E ON assembly_bom_entries'); + $this->addSql('ALTER TABLE assembly_bom_entries DROP id_project'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE assembly_bom_entries ADD id_project INT DEFAULT NULL'); + $this->addSql('ALTER TABLE assembly_bom_entries ADD CONSTRAINT `FK_8C74887EF12E799E` FOREIGN KEY (id_project) REFERENCES projects (id)'); + $this->addSql('CREATE INDEX IDX_8C74887EF12E799E ON assembly_bom_entries (id_project)'); + } + + public function sqLiteUp(Schema $schema): void + { + //nothing to do. Already removed from AssemblyBOMEntry and Version20250304081039 + } + + public function sqLiteDown(Schema $schema): void + { + //nothing to do. + } + + public function postgreSQLUp(Schema $schema): void + { + //nothing to do. Already removed from AssemblyBOMEntry and Version20250304081039 + } + + public function postgreSQLDown(Schema $schema): void + { + //nothing to do. + } +} diff --git a/src/Command/Migrations/ConvertBBCodeCommand.php b/src/Command/Migrations/ConvertBBCodeCommand.php index 201263ffd..b0c083921 100644 --- a/src/Command/Migrations/ConvertBBCodeCommand.php +++ b/src/Command/Migrations/ConvertBBCodeCommand.php @@ -22,6 +22,7 @@ namespace App\Command\Migrations; +use App\Entity\AssemblySystem\Assembly; use Symfony\Component\Console\Attribute\AsCommand; use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractNamedDBElement; @@ -88,6 +89,7 @@ protected function getTargetsLists(): array AttachmentType::class => ['comment'], StorageLocation::class => ['comment'], Project::class => ['comment'], + Assembly::class => ['comment'], Category::class => ['comment'], Manufacturer::class => ['comment'], MeasurementUnit::class => ['comment'], diff --git a/src/Command/Migrations/ImportPartKeeprCommand.php b/src/Command/Migrations/ImportPartKeeprCommand.php index aee71afe7..429f018d5 100644 --- a/src/Command/Migrations/ImportPartKeeprCommand.php +++ b/src/Command/Migrations/ImportPartKeeprCommand.php @@ -121,6 +121,11 @@ private function doImport(SymfonyStyle $io, array $data): void $count = $this->datastructureImporter->importPartUnits($data); $io->success('Imported '.$count.' measurement units.'); + //Import the custom states + $io->info('Importing custom states...'); + $count = $this->datastructureImporter->importPartCustomStates($data); + $io->success('Imported '.$count.' custom states.'); + //Import manufacturers $io->info('Importing manufacturers...'); $count = $this->datastructureImporter->importManufacturers($data); diff --git a/src/Controller/AdminPages/AssemblyAdminController.php b/src/Controller/AdminPages/AssemblyAdminController.php new file mode 100644 index 000000000..20f640923 --- /dev/null +++ b/src/Controller/AdminPages/AssemblyAdminController.php @@ -0,0 +1,80 @@ +. + */ + +declare(strict_types=1); + +namespace App\Controller\AdminPages; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\AssemblyAttachment; +use App\Entity\Parameters\AssemblyParameter; +use App\Form\AdminPages\AssemblyAdminForm; +use App\Services\ImportExportSystem\EntityExporter; +use App\Services\ImportExportSystem\EntityImporter; +use App\Services\Trees\StructuralElementRecursionHelper; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +#[Route(path: '/assembly')] +class AssemblyAdminController extends BaseAdminController +{ + protected string $entity_class = Assembly::class; + protected string $twig_template = 'admin/assembly_admin.html.twig'; + protected string $form_class = AssemblyAdminForm::class; + protected string $route_base = 'assembly'; + protected string $attachment_class = AssemblyAttachment::class; + protected ?string $parameter_class = AssemblyParameter::class; + + #[Route(path: '/{id}', name: 'assembly_delete', methods: ['DELETE'])] + public function delete(Request $request, Assembly $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse + { + return $this->_delete($request, $entity, $recursionHelper); + } + + #[Route(path: '/{id}/edit/{timestamp}', name: 'assembly_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}/edit', requirements: ['id' => '\d+'])] + public function edit(Assembly $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response + { + return $this->_edit($entity, $request, $em, $timestamp); + } + + #[Route(path: '/new', name: 'assembly_new')] + #[Route(path: '/{id}/clone', name: 'assembly_clone')] + #[Route(path: '/')] + public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Assembly $entity = null): Response + { + return $this->_new($request, $em, $importer, $entity); + } + + #[Route(path: '/export', name: 'assembly_export_all')] + public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response + { + return $this->_exportAll($em, $exporter, $request); + } + + #[Route(path: '/{id}/export', name: 'assembly_export')] + public function exportEntity(Assembly $entity, EntityExporter $exporter, Request $request): Response + { + return $this->_exportEntity($entity, $exporter, $request); + } +} diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index edc5917ac..a45b7ad39 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -23,6 +23,8 @@ namespace App\Controller\AdminPages; use App\DataTables\LogDataTable; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentUpload; @@ -193,6 +195,15 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit $entity->setMasterPictureAttachment(null); } + if ($entity instanceof Assembly) { + /* Replace ipn placeholder with the IPN information if applicable. + * The '%%ipn%%' placeholder is automatically inserted into the Name property, + * depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one, + * to avoid having to insert it manually */ + + $entity->setName(str_ireplace('%%ipn%%', $entity->getIpn() ?? '', $entity->getName())); + } + $this->commentHelper->setMessage($form['log_comment']->getData()); $em->persist($entity); @@ -232,6 +243,7 @@ protected function _edit(AbstractNamedDBElement $entity, Request $request, Entit 'timeTravel' => $timeTravel_timestamp, 'repo' => $repo, 'partsContainingElement' => $repo instanceof PartsContainingRepositoryInterface, + 'showParameters' => !($this instanceof PartCustomStateController), ]); } @@ -286,6 +298,15 @@ protected function _new(Request $request, EntityManagerInterface $em, EntityImpo $new_entity->setMasterPictureAttachment(null); } + if ($new_entity instanceof Assembly) { + /* Replace ipn placeholder with the IPN information if applicable. + * The '%%ipn%%' placeholder is automatically inserted into the Name property, + * depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one, + * to avoid having to insert it manually */ + + $new_entity->setName(str_ireplace('%%ipn%%', $new_entity->getIpn() ?? '', $new_entity->getName())); + } + $this->commentHelper->setMessage($form['log_comment']->getData()); $em->persist($new_entity); $em->flush(); @@ -382,6 +403,7 @@ protected function _new(Request $request, EntityManagerInterface $em, EntityImpo 'import_form' => $import_form, 'mass_creation_form' => $mass_creation_form, 'route_base' => $this->route_base, + 'showParameters' => !($this instanceof PartCustomStateController), ]); } @@ -434,6 +456,10 @@ protected function _delete(Request $request, AbstractNamedDBElement $entity, Str return $this->redirectToRoute($this->route_base.'_edit', ['id' => $entity->getID()]); } } else { + if ($entity instanceof Assembly) { + $this->markReferencedBomEntry($entity); + } + if ($entity instanceof AbstractStructuralDBElement) { $parent = $entity->getParent(); @@ -481,4 +507,16 @@ protected function _exportEntity(AbstractNamedDBElement $entity, EntityExporter return $exporter->exportEntityFromRequest($entity, $request); } + + private function markReferencedBomEntry(Assembly $referencedAssembly): void + { + $bomEntries = $this->entityManager->getRepository(AssemblyBOMEntry::class)->findBy(['referencedAssembly' => $referencedAssembly]); + + foreach ($bomEntries as $entry) { + $entry->setReferencedAssembly(null); + $entry->setName($referencedAssembly->getName(). ' DELETED'); + + $this->entityManager->persist($entry); + } + } } diff --git a/src/Controller/AdminPages/PartCustomStateController.php b/src/Controller/AdminPages/PartCustomStateController.php new file mode 100644 index 000000000..60f63abf3 --- /dev/null +++ b/src/Controller/AdminPages/PartCustomStateController.php @@ -0,0 +1,83 @@ +. + */ + +declare(strict_types=1); + +namespace App\Controller\AdminPages; + +use App\Entity\Attachments\PartCustomStateAttachment; +use App\Entity\Parameters\PartCustomStateParameter; +use App\Entity\Parts\PartCustomState; +use App\Form\AdminPages\PartCustomStateAdminForm; +use App\Services\ImportExportSystem\EntityExporter; +use App\Services\ImportExportSystem\EntityImporter; +use App\Services\Trees\StructuralElementRecursionHelper; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +/** + * @see \App\Tests\Controller\AdminPages\PartCustomStateControllerTest + */ +#[Route(path: '/part_custom_state')] +class PartCustomStateController extends BaseAdminController +{ + protected string $entity_class = PartCustomState::class; + protected string $twig_template = 'admin/part_custom_state_admin.html.twig'; + protected string $form_class = PartCustomStateAdminForm::class; + protected string $route_base = 'part_custom_state'; + protected string $attachment_class = PartCustomStateAttachment::class; + protected ?string $parameter_class = PartCustomStateParameter::class; + + #[Route(path: '/{id}', name: 'part_custom_state_delete', methods: ['DELETE'])] + public function delete(Request $request, PartCustomState $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse + { + return $this->_delete($request, $entity, $recursionHelper); + } + + #[Route(path: '/{id}/edit/{timestamp}', name: 'part_custom_state_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}', requirements: ['id' => '\d+'])] + public function edit(PartCustomState $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response + { + return $this->_edit($entity, $request, $em, $timestamp); + } + + #[Route(path: '/new', name: 'part_custom_state_new')] + #[Route(path: '/{id}/clone', name: 'part_custom_state_clone')] + #[Route(path: '/')] + public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?PartCustomState $entity = null): Response + { + return $this->_new($request, $em, $importer, $entity); + } + + #[Route(path: '/export', name: 'part_custom_state_export_all')] + public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response + { + return $this->_exportAll($em, $exporter, $request); + } + + #[Route(path: '/{id}/export', name: 'part_custom_state_export')] + public function exportEntity(PartCustomState $entity, EntityExporter $exporter, Request $request): Response + { + return $this->_exportEntity($entity, $exporter, $request); + } +} diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php new file mode 100644 index 000000000..5d8211216 --- /dev/null +++ b/src/Controller/AssemblyController.php @@ -0,0 +1,366 @@ +. + */ +namespace App\Controller; + +use App\DataTables\AssemblyBomEntriesDataTable; +use App\DataTables\AssemblyDataTable; +use App\DataTables\ErrorDataTable; +use App\DataTables\Filters\AssemblyFilter; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Parts\Part; +use App\Exceptions\InvalidRegexException; +use App\Form\AssemblySystem\AssemblyAddPartsType; +use App\Form\AssemblySystem\AssemblyBuildType; +use App\Form\Filters\AssemblyFilterType; +use App\Helpers\Assemblies\AssemblyBuildRequest; +use App\Services\ImportExportSystem\BOMImporter; +use App\Services\AssemblySystem\AssemblyBuildHelper; +use App\Services\Trees\NodesListBuilder; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\DBAL\Exception\DriverException; +use Doctrine\ORM\EntityManagerInterface; +use League\Csv\SyntaxError; +use Omines\DataTablesBundle\DataTableFactory; +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 +{ + public function __construct( + private readonly DataTableFactory $dataTableFactory, + private readonly TranslatorInterface $translator, + private readonly NodesListBuilder $nodesListBuilder + ) { + } + + #[Route(path: '/list', name: 'assemblies_list')] + public function showAll(Request $request): Response + { + return $this->showListWithFilter($request,'assemblies/lists/all_list.html.twig'); + } + + /** + * Common implementation for the part list pages. + * @param Request $request The request to parse + * @param string $template The template that should be rendered + * @param callable|null $filter_changer A function that is called with the filter object as parameter. This function can be used to customize the filter + * @param callable|null $form_changer A function that is called with the form object as parameter. This function can be used to customize the form + * @param array $additonal_template_vars Any additional template variables that should be passed to the template + * @param array $additional_table_vars Any additional variables that should be passed to the table creation + */ + protected function showListWithFilter(Request $request, string $template, ?callable $filter_changer = null, ?callable $form_changer = null, array $additonal_template_vars = [], array $additional_table_vars = []): Response + { + $this->denyAccessUnlessGranted('@assemblies.read'); + + $formRequest = clone $request; + $formRequest->setMethod('GET'); + $filter = new AssemblyFilter($this->nodesListBuilder); + if($filter_changer !== null){ + $filter_changer($filter); + } + + $filterForm = $this->createForm(AssemblyFilterType::class, $filter, ['method' => 'GET']); + if($form_changer !== null) { + $form_changer($filterForm); + } + + $filterForm->handleRequest($formRequest); + + $table = $this->dataTableFactory->createFromType( + AssemblyDataTable::class, + array_merge(['filter' => $filter], $additional_table_vars), + ['lengthMenu' => AssemblyDataTable::LENGTH_MENU] + ) + ->handleRequest($request); + + if ($table->isCallback()) { + try { + try { + return $table->getResponse(); + } catch (DriverException $driverException) { + if ($driverException->getCode() === 1139) { + //Convert the driver exception to InvalidRegexException so it has the same handler as for SQLite + throw InvalidRegexException::fromDriverException($driverException); + } else { + throw $driverException; + } + } + } catch (InvalidRegexException $exception) { + $errors = $this->translator->trans('assembly.table.invalid_regex').': '.$exception->getReason(); + $request->request->set('order', []); + + return ErrorDataTable::errorTable($this->dataTableFactory, $request, $errors); + } + } + + return $this->render($template, array_merge([ + 'datatable' => $table, + 'filterForm' => $filterForm->createView(), + ], $additonal_template_vars)); + } + + #[Route(path: '/{id}/info', name: 'assembly_info', requirements: ['id' => '\d+'])] + public function info(Assembly $assembly, Request $request, AssemblyBuildHelper $buildHelper): Response + { + $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.csv' => 'csv', + 'assembly.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew', + 'assembly.bom_import.type.kicad_schematic' => 'kicad_schematic', + ] + ]); + $builder->add('clear_existing_bom', CheckboxType::class, [ + '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 entries if requested + if ($form->get('clear_existing_bom')->getData()) { + $assembly->getBomEntries()->clear(); + $entityManager->flush(); + } + + try { + $importerResult = $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 && $importerResult->getViolations()->count() === 0) { + $entries = $importerResult->getBomEntries(); + + $this->addFlash('success', t('assembly.bom_import.flash.success', ['%count%' => count($entries)])); + $entityManager->flush(); + + return $this->redirectToRoute('assembly_edit', ['id' => $assembly->getID()]); + } + + //Show validation errors + $this->addFlash('error', t('assembly.bom_import.flash.invalid_entries')); + } catch (\UnexpectedValueException|\RuntimeException|SyntaxError $e) { + $this->addFlash('error', t('assembly.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()])); + } + } + + $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, + 'validationErrors' => $errors ?? null, + 'importerErrors' => isset($importerResult) ? $importerResult->getViolations() : null, + ]); + } + + #[Route(path: '/add_parts', name: 'assembly_add_parts_no_id')] + #[Route(path: '/{id}/add_parts', name: 'assembly_add_parts', requirements: ['id' => '\d+'])] + public function addPart(Request $request, EntityManagerInterface $entityManager, ?Assembly $assembly): Response + { + if($assembly instanceof Assembly) { + $this->denyAccessUnlessGranted('edit', $assembly); + } else { + $this->denyAccessUnlessGranted('@assemblies.edit'); + } + + $form = $this->createForm(AssemblyAddPartsType::class, null, [ + 'assembly' => $assembly, + ]); + + //Preset the BOM entries with the selected parts, when the form was not submitted yet + $preset_data = new ArrayCollection(); + foreach (explode(',', (string) $request->get('parts', '')) as $part_id) { + //Skip empty part IDs. Postgres seems to be especially sensitive to empty strings, as it does not allow them in integer columns + if ($part_id === '') { + continue; + } + + $part = $entityManager->getRepository(Part::class)->find($part_id); + if (null !== $part) { + //If there is already a BOM entry for this part, we use this one (we edit it then) + $bom_entry = $entityManager->getRepository(AssemblyBOMEntry::class)->findOneBy([ + 'assembly' => $assembly, + 'part' => $part + ]); + if ($bom_entry !== null) { + $preset_data->add($bom_entry); + } else { //Otherwise create an empty one + $entry = new AssemblyBOMEntry(); + $entry->setAssembly($assembly); + $entry->setPart($part); + $preset_data->add($entry); + } + } + } + $form['bom_entries']->setData($preset_data); + + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $target_assembly = $assembly ?? $form->get('assembly')->getData(); + + //Ensure that we really have acces to the selected assembly + $this->denyAccessUnlessGranted('edit', $target_assembly); + + $data = $form->getData(); + $bom_entries = $data['bom_entries']; + foreach ($bom_entries as $bom_entry){ + $target_assembly->addBOMEntry($bom_entry); + } + + $entityManager->flush(); + + //If a redirect query parameter is set, redirect to this page + if ($request->query->get('_redirect')) { + return $this->redirect($request->query->get('_redirect')); + } + //Otherwise just show the assembly info page + return $this->redirectToRoute('assembly_info', ['id' => $target_assembly->getID()]); + } + + return $this->render('assemblies/add_parts.html.twig', [ + 'assembly' => $assembly, + 'form' => $form, + ]); + } +} diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 6708ed4cd..ba5c2054e 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -23,6 +23,7 @@ namespace App\Controller; use App\DataTables\LogDataTable; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\AttachmentUpload; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -45,6 +46,7 @@ use App\Services\Parameters\ParameterExtractor; use App\Services\Parts\PartLotWithdrawAddHelper; use App\Services\Parts\PricedetailHelper; +use App\Services\AssemblySystem\AssemblyBuildPartHelper; use App\Services\ProjectSystem\ProjectBuildPartHelper; use App\Settings\BehaviorSettings\PartInfoSettings; use DateTime; @@ -66,12 +68,16 @@ #[Route(path: '/part')] class PartController extends AbstractController { - public function __construct(protected PricedetailHelper $pricedetailHelper, + public function __construct( + protected PricedetailHelper $pricedetailHelper, protected PartPreviewGenerator $partPreviewGenerator, private readonly TranslatorInterface $translator, - private readonly AttachmentSubmitHandler $attachmentSubmitHandler, private readonly EntityManagerInterface $em, - protected EventCommentHelper $commentHelper, private readonly PartInfoSettings $partInfoSettings) - { + private readonly AttachmentSubmitHandler $attachmentSubmitHandler, + private readonly EntityManagerInterface $em, + protected EventCommentHelper $commentHelper, + private readonly PartInfoSettings $partInfoSettings, + private readonly int $autocompletePartDigits + ) { } /** @@ -158,11 +164,15 @@ public function delete(Request $request, Part $part): RedirectResponse #[Route(path: '/new', name: 'part_new')] #[Route(path: '/{id}/clone', name: 'part_clone')] - #[Route(path: '/new_build_part/{project_id}', name: 'part_new_build_part')] - public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator, - AttachmentSubmitHandler $attachmentSubmitHandler, ProjectBuildPartHelper $projectBuildPartHelper, - #[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null, - #[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null): Response + #[Route(path: '/new_build_part_project/{project_id}', name: 'part_new_build_part_project')] + #[Route(path: '/new_build_part_assembly/{assembly_id}', name: 'part_new_build_part_assembly')] + public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator, + AttachmentSubmitHandler $attachmentSubmitHandler, + ProjectBuildPartHelper $projectBuildPartHelper, + AssemblyBuildPartHelper $assemblyBuildPartHelper, + #[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null, + #[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null, + #[MapEntity(mapping: ['assembly_id' => 'id'])] ?Assembly $assembly = null): Response { if ($part instanceof Part) { @@ -176,6 +186,14 @@ public function new(Request $request, EntityManagerInterface $em, TranslatorInte return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]); } $new_part = $projectBuildPartHelper->getPartInitialization($project); + } elseif ($assembly instanceof Assembly) { + //Initialize a new part for a build part from the given assembly + //Ensure that the assembly has not already a build part + if ($assembly->getBuildPart() instanceof Part) { + $this->addFlash('error', 'part.new_build_part.error.build_part_already_exists'); + return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]); + } + $new_part = $assemblyBuildPartHelper->getPartInitialization($assembly); } else { //Create an empty part from scratch $new_part = new Part(); } @@ -371,16 +389,18 @@ private function renderPartForm(string $mode, Request $request, Part $data, arra $template = 'parts/edit/update_from_ip.html.twig'; } + $partRepository = $this->em->getRepository(Part::class); + return $this->render($template, [ 'part' => $new_part, + 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, base64_encode($data->getDescription()), $this->autocompletePartDigits), 'form' => $form, 'merge_old_name' => $merge_infos['tname_before'] ?? null, 'merge_other' => $merge_infos['other_part'] ?? null ]); } - #[Route(path: '/{id}/add_withdraw', name: 'part_add_withdraw', methods: ['POST'])] public function withdrawAddHandler(Part $part, Request $request, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper): Response { diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 2a6d19ee2..e510506f3 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -46,14 +46,16 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Validator\Validator\ValidatorInterface; - +use Symfony\Contracts\Translation\TranslatorInterface; use function Symfony\Component\Translation\t; #[Route(path: '/project')] class ProjectController extends AbstractController { - public function __construct(private readonly DataTableFactory $dataTableFactory) - { + public function __construct( + private readonly DataTableFactory $dataTableFactory, + private readonly TranslatorInterface $translator, + ) { } #[Route(path: '/{id}/info', name: 'project_info', requirements: ['id' => '\d+'])] @@ -147,6 +149,8 @@ public function importBOM( 'label' => 'project.bom_import.type', 'required' => true, 'choices' => [ + 'project.bom_import.type.json' => 'json', + 'project.bom_import.type.csv' => 'csv', 'project.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew', 'project.bom_import.type.kicad_schematic' => 'kicad_schematic', 'project.bom_import.type.generic_csv' => 'generic_csv', @@ -189,17 +193,20 @@ public function importBOM( } // For PCB imports, proceed directly - $entries = $BOMImporter->importFileIntoProject($form->get('file')->getData(), $project, [ + $importerResult = $BOMImporter->importFileIntoProject($form->get('file')->getData(), $project, [ 'type' => $import_type, ]); // Validate the project entries $errors = $validator->validateProperty($project, 'bom_entries'); - // If no validation errors occurred, save the changes and redirect to edit page - if (count($errors) === 0) { + //If no validation errors occurred, save the changes and redirect to edit page + if (count($errors) === 0 && $importerResult->getViolations()->count() === 0) { + $entries = $importerResult->getBomEntries(); + $this->addFlash('success', t('project.bom_import.flash.success', ['%count%' => count($entries)])); $entityManager->flush(); + return $this->redirectToRoute('project_edit', ['id' => $project->getID()]); } @@ -211,10 +218,29 @@ public function importBOM( } } + $jsonTemplate = [ + [ + "quantity" => 1.0, + "name" => $this->translator->trans('project.bom_import.template.entry.name'), + "part" => [ + "id" => null, + "ipn" => $this->translator->trans('project.bom_import.template.entry.part.ipn'), + "mpnr" => $this->translator->trans('project.bom_import.template.entry.part.mpnr'), + "name" => $this->translator->trans('project.bom_import.template.entry.part.name'), + "manufacturer" => [ + "id" => null, + "name" => $this->translator->trans('project.bom_import.template.entry.part.manufacturer.name') + ], + ] + ] + ]; + return $this->render('projects/import_bom.html.twig', [ 'project' => $project, + 'jsonTemplate' => $jsonTemplate, 'form' => $form, - 'errors' => $errors ?? null, + 'validationErrors' => $errors ?? null, + 'importerErrors' => isset($importerResult) ? $importerResult->getViolations() : null, ]); } @@ -395,7 +421,7 @@ public function importBOMMapFields( } // Import with field mapping and priorities (validation already passed) - $entries = $BOMImporter->stringToBOMEntries($file_content, [ + $entries = $BOMImporter->stringToBOMEntries($project, $file_content, [ 'type' => 'kicad_schematic', 'field_mapping' => $field_mapping, 'field_priorities' => $field_priorities, diff --git a/src/Controller/TreeController.php b/src/Controller/TreeController.php index 71f8ba5c6..0ba3a1584 100644 --- a/src/Controller/TreeController.php +++ b/src/Controller/TreeController.php @@ -22,6 +22,7 @@ namespace App\Controller; +use App\Entity\AssemblySystem\Assembly; use Symfony\Component\HttpFoundation\Response; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; @@ -129,4 +130,17 @@ public function deviceTree(?Project $device = null): JsonResponse return new JsonResponse($tree); } + + #[Route(path: '/assembly/{id}', name: 'tree_assembly')] + #[Route(path: '/assemblies', name: 'tree_assembly_root')] + public function assemblyTree(?Assembly $assembly = null): JsonResponse + { + if ($this->isGranted('@assemblies.read')) { + $tree = $this->treeGenerator->getTreeView(Assembly::class, $assembly, 'assemblies'); + } else { + return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN); + } + + return new JsonResponse($tree); + } } diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 89eac7ff7..fd08128ca 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -22,7 +22,11 @@ namespace App\Controller; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Parameters\AbstractParameter; +use App\Entity\ProjectSystem\Project; +use App\Services\Attachments\AssemblyPreviewGenerator; +use App\Services\Attachments\ProjectPreviewGenerator; use Symfony\Component\HttpFoundation\Response; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Category; @@ -53,6 +57,8 @@ use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; +use Symfony\Contracts\Translation\TranslatorInterface; +use InvalidArgumentException; /** * In this controller the endpoints for the typeaheads are collected. @@ -60,8 +66,12 @@ #[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, + protected int $autocompletePartDigits, + ) { } #[Route(path: '/builtInResources/search', name: 'typeahead_builtInRessources')] @@ -109,19 +119,22 @@ private function typeToParameterClass(string $type): string 'group' => GroupParameter::class, 'measurement_unit' => MeasurementUnitParameter::class, 'currency' => Currency::class, - default => throw new \InvalidArgumentException('Invalid parameter type: '.$type), + default => throw new InvalidArgumentException('Invalid parameter type: '.$type), }; } #[Route(path: '/parts/search/{query}', name: 'typeahead_parts')] - public function parts(EntityManagerInterface $entityManager, PartPreviewGenerator $previewGenerator, - AttachmentURLGenerator $attachmentURLGenerator, string $query = ""): JsonResponse - { + public function parts( + EntityManagerInterface $entityManager, + PartPreviewGenerator $previewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" + ): JsonResponse { $this->denyAccessUnlessGranted('@parts.read'); - $repo = $entityManager->getRepository(Part::class); + $partRepository = $entityManager->getRepository(Part::class); - $parts = $repo->autocompleteSearch($query, 100); + $parts = $partRepository->autocompleteSearch($query, 100); $data = []; foreach ($parts as $part) { @@ -141,12 +154,88 @@ public function parts(EntityManagerInterface $entityManager, PartPreviewGenerato 'footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '', 'description' => mb_strimwidth($part->getDescription(), 0, 127, '...'), 'image' => $preview_url, - ]; + ]; } return new JsonResponse($data); } + #[Route(path: '/projects/search/{query}', name: 'typeahead_projects')] + public function projects( + EntityManagerInterface $entityManager, + ProjectPreviewGenerator $projectPreviewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" + ): JsonResponse { + $this->denyAccessUnlessGranted('@projects.read'); + + $result = []; + + $projectRepository = $entityManager->getRepository(Project::class); + + $projects = $projectRepository->autocompleteSearch($query, 100); + + foreach ($projects as $project) { + $preview_attachment = $projectPreviewGenerator->getTablePreviewAttachment($project); + + if($preview_attachment instanceof Attachment) { + $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); + } else { + $preview_url = ''; + } + + /** @var Project $project */ + $result[] = [ + 'id' => $project->getID(), + 'name' => $project->getName(), + 'category' => '', + 'footprint' => '', + 'description' => mb_strimwidth($project->getDescription(), 0, 127, '...'), + 'image' => $preview_url, + ]; + } + + return new JsonResponse($result); + } + + #[Route(path: '/assemblies/search/{query}', name: 'typeahead_assemblies')] + public function assemblies( + EntityManagerInterface $entityManager, + AssemblyPreviewGenerator $assemblyPreviewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" + ): JsonResponse { + $this->denyAccessUnlessGranted('@assemblies.read'); + + $result = []; + + $assemblyRepository = $entityManager->getRepository(Assembly::class); + + $assemblies = $assemblyRepository->autocompleteSearch($query, 100); + + foreach ($assemblies as $assembly) { + $preview_attachment = $assemblyPreviewGenerator->getTablePreviewAttachment($assembly); + + if($preview_attachment instanceof Attachment) { + $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); + } else { + $preview_url = ''; + } + + /** @var Assembly $assembly */ + $result[] = [ + 'id' => $assembly->getID(), + 'name' => $assembly->getName(), + 'category' => '', + 'footprint' => '', + 'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'), + 'image' => $preview_url, + ]; + } + + return new JsonResponse($result); + } + #[Route(path: '/parameters/{type}/search/{query}', name: 'typeahead_parameters', requirements: ['type' => '.+'])] public function parameters(string $type, EntityManagerInterface $entityManager, string $query = ""): JsonResponse { @@ -183,4 +272,29 @@ public function tags(string $query, TagFinder $finder): JsonResponse return new JsonResponse($data, Response::HTTP_OK, [], true); } + + #[Route(path: '/parts/ipn-suggestions', name: 'ipn_suggestions', methods: ['GET'])] + public function ipnSuggestions( + Request $request, + EntityManagerInterface $entityManager + ): JsonResponse { + $partId = $request->query->get('partId'); + if ($partId === '0' || $partId === 'undefined' || $partId === 'null') { + $partId = null; + } + $categoryId = $request->query->getInt('categoryId'); + $description = $request->query->getString('description'); + + /** @var Part $part */ + $part = $partId !== null ? $entityManager->getRepository(Part::class)->find($partId) : new Part(); + $category = $entityManager->getRepository(Category::class)->find($categoryId); + + $clonedPart = clone $part; + $clonedPart->setCategory($category); + + $partRepository = $entityManager->getRepository(Part::class); + $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $description, $this->autocompletePartDigits); + + return new JsonResponse($ipnSuggestions); + } } diff --git a/src/DataTables/AssemblyBomEntriesDataTable.php b/src/DataTables/AssemblyBomEntriesDataTable.php new file mode 100644 index 000000000..ddeedba22 --- /dev/null +++ b/src/DataTables/AssemblyBomEntriesDataTable.php @@ -0,0 +1,233 @@ +. + */ +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\ColumnSortHelper; +use App\DataTables\Helpers\PartDataTableHelper; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\Attachment; +use App\Entity\Parts\Part; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Services\Formatters\AmountFormatter; +use App\Settings\BehaviorSettings\TableSettings; +use Doctrine\ORM\QueryBuilder; +use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; +use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; +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( + private readonly TranslatorInterface $translator, + private readonly PartDataTableHelper $partDataTableHelper, + private readonly AssemblyDataTableHelper $assemblyDataTableHelper, + private readonly AmountFormatter $amountFormatter, + private readonly ColumnSortHelper $csh, + private readonly TableSettings $tableSettings, + ) { + } + + public function configure(DataTable $dataTable, array $options): void + { + $this->csh + ->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'), + ]) + ->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 && !$context->getReferencedAssembly() instanceof Assembly) { + return htmlspecialchars((string) $context->getName()); + } + + if ($context->getPart() !== null) { + $tmp = $this->partDataTableHelper->renderName($context->getPart()); + $tmp = $this->translator->trans('part.table.name.value.for_part', ['%value%' => $tmp]); + + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
    '.htmlspecialchars($context->getName()).''; + } + } elseif ($context->getReferencedAssembly() !== null) { + $tmp = $this->assemblyDataTableHelper->renderName($context->getReferencedAssembly()); + $tmp = $this->translator->trans('part.table.name.value.for_assembly', ['%value%' => $tmp]); + + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
    '.htmlspecialchars($context->getName()).''; + } + } + + return $tmp; + }, + + ]) + ->add('ipn', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.ipn'), + 'orderField' => 'NATSORT(part.ipn)', + 'render' => function ($value, AssemblyBOMEntry $context) { + if($context->getPart() instanceof Part) { + return $context->getPart()->getIpn(); + } elseif($context->getReferencedAssembly() instanceof Assembly) { + return $context->getReferencedAssembly()->getIpn(); + } + + return ''; + } + ]) + ->add('description', MarkdownColumn::class, [ + 'label' => $this->translator->trans('part.table.description'), + 'data' => function (AssemblyBOMEntry $context) { + if ($context->getPart() instanceof Part) { + return $context->getPart()->getDescription(); + } elseif ($context->getReferencedAssembly() instanceof Assembly) { + return $context->getReferencedAssembly()->getDescription(); + } + //For non-part BOM entries show the comment field + return $context->getComment(); + }, + ]) + ->add('category', EntityColumn::class, [ + 'label' => $this->translator->trans('part.table.category'), + 'property' => 'part.category', + 'orderField' => 'NATSORT(category.name)', + ]) + ->add('footprint', EntityColumn::class, [ + 'property' => 'part.footprint', + 'label' => $this->translator->trans('part.table.footprint'), + 'orderField' => 'NATSORT(footprint.name)', + ]) + ->add('manufacturer', EntityColumn::class, [ + 'property' => 'part.manufacturer', + 'label' => $this->translator->trans('part.table.manufacturer'), + 'orderField' => 'NATSORT(manufacturer.name)', + ]) + ->add('mountnames', TextColumn::class, [ + 'label' => 'assembly.bom.mountnames', + 'render' => function ($value, AssemblyBOMEntry $context) { + $html = ''; + + foreach (explode(',', $context->getMountnames()) as $mountname) { + $html .= sprintf('%s ', htmlspecialchars($mountname)); + } + return $html; + }, + ]) + ->add('instockAmount', TextColumn::class, [ + 'label' => 'assembly.bom.instockAmount', + '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'), + ]) + ->add('lastModified', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('part.table.lastModified'), + ]); + + //Apply the user configured order and visibility and add the columns to the table + $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->tableSettings->assembliesBomDefaultColumns, + "TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS"); + + $dataTable->addOrderBy('name'); + + $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('bom_entry.referencedAssembly', 'referencedAssembly') + ->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/AssemblyDataTable.php b/src/DataTables/AssemblyDataTable.php new file mode 100644 index 000000000..aaad2e45e --- /dev/null +++ b/src/DataTables/AssemblyDataTable.php @@ -0,0 +1,250 @@ +. + */ + +declare(strict_types=1); + +namespace App\DataTables; + +use App\DataTables\Adapters\TwoStepORMAdapter; +use App\DataTables\Column\IconLinkColumn; +use App\DataTables\Column\LocaleDateTimeColumn; +use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Column\SelectColumn; +use App\DataTables\Filters\AssemblyFilter; +use App\DataTables\Filters\AssemblySearchFilter; +use App\DataTables\Helpers\AssemblyDataTableHelper; +use App\DataTables\Helpers\ColumnSortHelper; +use App\Doctrine\Helpers\FieldHelper; +use App\Entity\AssemblySystem\Assembly; +use App\Services\EntityURLGenerator; +use App\Settings\BehaviorSettings\TableSettings; +use Doctrine\ORM\AbstractQuery; +use Doctrine\ORM\QueryBuilder; +use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; +use Omines\DataTablesBundle\Column\TextColumn; +use Omines\DataTablesBundle\DataTable; +use Omines\DataTablesBundle\DataTableTypeInterface; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; + +final class AssemblyDataTable implements DataTableTypeInterface +{ + const LENGTH_MENU = [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]; + + public function __construct( + private readonly EntityURLGenerator $urlGenerator, + private readonly TranslatorInterface $translator, + private readonly AssemblyDataTableHelper $assemblyDataTableHelper, + private readonly Security $security, + private readonly ColumnSortHelper $csh, + private readonly TableSettings $tableSettings, + ) { + } + + public function configureOptions(OptionsResolver $optionsResolver): void + { + $optionsResolver->setDefaults([ + 'filter' => null, + 'search' => null + ]); + + $optionsResolver->setAllowedTypes('filter', [AssemblyFilter::class, 'null']); + $optionsResolver->setAllowedTypes('search', [AssemblySearchFilter::class, 'null']); + } + + public function configure(DataTable $dataTable, array $options): void + { + $resolver = new OptionsResolver(); + $this->configureOptions($resolver); + $options = $resolver->resolve($options); + + $this->csh + ->add('select', SelectColumn::class, visibility_configurable: false) + ->add('picture', TextColumn::class, [ + 'label' => '', + 'className' => 'no-colvis', + 'render' => fn($value, Assembly $context) => $this->assemblyDataTableHelper->renderPicture($context), + ], visibility_configurable: false) + ->add('name', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.table.name'), + 'render' => fn($value, Assembly $context) => $this->assemblyDataTableHelper->renderName($context), + 'orderField' => 'NATSORT(assembly.name)' + ]) + ->add('id', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.table.id'), + ]) + ->add('ipn', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.table.ipn'), + 'orderField' => 'NATSORT(assembly.ipn)' + ]) + ->add('description', MarkdownColumn::class, [ + 'label' => $this->translator->trans('assembly.table.description'), + ]) + ->add('addedDate', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('assembly.table.addedDate'), + ]) + ->add('lastModified', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('assembly.table.lastModified'), + ]); + + //Add a assembly column to list where the assembly is used as referenced assembly as bom-entry, when the user has the permission to see the assemblies + if ($this->security->isGranted('read', Assembly::class)) { + $this->csh->add('referencedAssemblies', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.referencedAssembly.labelp'), + 'render' => function ($value, Assembly $context): string { + $assemblies = $context->getAllReferencedAssembliesRecursive($context); + + $max = 5; + $tmp = ""; + + for ($i = 0; $i < min($max, count($assemblies)); $i++) { + $tmp .= $this->assemblyDataTableHelper->renderName($assemblies[$i]); + if ($i < count($assemblies) - 1) { + $tmp .= ", "; + } + } + + if (count($assemblies) > $max) { + $tmp .= ", + ".(count($assemblies) - $max); + } + + return $tmp; + } + ]); + } + + $this->csh + ->add('edit', IconLinkColumn::class, [ + 'label' => $this->translator->trans('assembly.table.edit'), + 'href' => fn($value, Assembly $context) => $this->urlGenerator->editURL($context), + 'disabled' => fn($value, Assembly $context) => !$this->security->isGranted('edit', $context), + 'title' => $this->translator->trans('assembly.table.edit.title'), + ]); + + //Apply the user configured order and visibility and add the columns to the table + $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->tableSettings->assembliesDefaultColumns, + "TABLE_ASSEMBLIES_DEFAULT_COLUMNS"); + + $dataTable->addOrderBy('name') + ->createAdapter(TwoStepORMAdapter::class, [ + 'filter_query' => $this->getFilterQuery(...), + 'detail_query' => $this->getDetailQuery(...), + 'entity' => Assembly::class, + 'hydrate' => AbstractQuery::HYDRATE_OBJECT, + //Use the simple total query, as we just want to get the total number of assemblies without any conditions + //For this the normal query would be pretty slow + 'simple_total_query' => true, + 'criteria' => [ + function (QueryBuilder $builder) use ($options): void { + $this->buildCriteria($builder, $options); + }, + new SearchCriteriaProvider(), + ], + 'query_modifier' => $this->addJoins(...), + ]); + } + + + private function getFilterQuery(QueryBuilder $builder): void + { + /* In the filter query we only select the IDs. The fetching of the full entities is done in the detail query. + * We only need to join the entities here, so we can filter by them. + * The filter conditions are added to this QB in the buildCriteria method. + * + * The amountSum field and the joins are dynamically added by the addJoins method, if the fields are used in the query. + * This improves the performance, as we do not need to join all tables, if we do not need them. + */ + $builder + ->select('assembly.id') + ->from(Assembly::class, 'assembly') + + //The other group by fields, are dynamically added by the addJoins method + ->addGroupBy('assembly'); + } + + private function getDetailQuery(QueryBuilder $builder, array $filter_results): void + { + $ids = array_map(static fn($row) => $row['id'], $filter_results); + + /* + * In this query we take the IDs which were filtered, paginated and sorted in the filter query, and fetch the + * full entities. + * We can do complex fetch joins, as we do not need to filter or sort here (which would kill the performance). + * The only condition should be for the IDs. + * It is important that elements are ordered the same way, as the IDs are passed, or ordering will be wrong. + * + * We do not require the subqueries like amountSum here, as it is not used to render the table (and only for sorting) + */ + $builder + ->select('assembly') + ->addSelect('master_picture_attachment') + ->addSelect('attachments') + ->from(Assembly::class, 'assembly') + ->leftJoin('assembly.master_picture_attachment', 'master_picture_attachment') + ->leftJoin('assembly.attachments', 'attachments') + ->where('assembly.id IN (:ids)') + ->setParameter('ids', $ids) + ->addGroupBy('assembly') + ->addGroupBy('master_picture_attachment') + ->addGroupBy('attachments'); + + //Get the results in the same order as the IDs were passed + FieldHelper::addOrderByFieldParam($builder, 'assembly.id', 'ids'); + } + + /** + * This function is called right before the filter query is executed. + * We use it to dynamically add joins to the query, if the fields are used in the query. + * @param QueryBuilder $builder + * @return QueryBuilder + */ + private function addJoins(QueryBuilder $builder): QueryBuilder + { + //Check if the query contains certain conditions, for which we need to add additional joins + //The join fields get prefixed with an underscore, so we can check if they are used in the query easy without confusing them for a assembly subfield + $dql = $builder->getDQL(); + + if (str_contains($dql, '_master_picture_attachment')) { + $builder->leftJoin('assembly.master_picture_attachment', '_master_picture_attachment'); + $builder->addGroupBy('_master_picture_attachment'); + } + if (str_contains($dql, '_attachments')) { + $builder->leftJoin('assembly.attachments', '_attachments'); + } + + return $builder; + } + + private function buildCriteria(QueryBuilder $builder, array $options): void + { + //Apply the search criterias first + if ($options['search'] instanceof AssemblySearchFilter) { + $search = $options['search']; + $search->apply($builder); + } + + //We do the most stuff here in the filter class + if ($options['filter'] instanceof AssemblyFilter) { + $filter = $options['filter']; + $filter->apply($builder); + } + } +} diff --git a/src/DataTables/Filters/AssemblyFilter.php b/src/DataTables/Filters/AssemblyFilter.php new file mode 100644 index 000000000..d8d07a1ec --- /dev/null +++ b/src/DataTables/Filters/AssemblyFilter.php @@ -0,0 +1,68 @@ +. + */ +namespace App\DataTables\Filters; + +use App\DataTables\Filters\Constraints\DateTimeConstraint; +use App\DataTables\Filters\Constraints\EntityConstraint; +use App\DataTables\Filters\Constraints\IntConstraint; +use App\DataTables\Filters\Constraints\TextConstraint; +use App\Entity\Attachments\AttachmentType; +use App\Services\Trees\NodesListBuilder; +use Doctrine\ORM\QueryBuilder; + +class AssemblyFilter implements FilterInterface +{ + + use CompoundFilterTrait; + + public readonly IntConstraint $dbId; + public readonly TextConstraint $ipn; + public readonly TextConstraint $name; + public readonly TextConstraint $description; + public readonly TextConstraint $comment; + public readonly DateTimeConstraint $lastModified; + public readonly DateTimeConstraint $addedDate; + + public readonly IntConstraint $attachmentsCount; + public readonly EntityConstraint $attachmentType; + public readonly TextConstraint $attachmentName; + + public function __construct(NodesListBuilder $nodesListBuilder) + { + $this->name = new TextConstraint('assembly.name'); + $this->description = new TextConstraint('assembly.description'); + $this->comment = new TextConstraint('assembly.comment'); + $this->dbId = new IntConstraint('assembly.id'); + $this->ipn = new TextConstraint('assembly.ipn'); + $this->addedDate = new DateTimeConstraint('assembly.addedDate'); + $this->lastModified = new DateTimeConstraint('assembly.lastModified'); + $this->attachmentsCount = new IntConstraint('COUNT(_attachments)'); + $this->attachmentType = new EntityConstraint($nodesListBuilder, AttachmentType::class, '_attachments.attachment_type'); + $this->attachmentName = new TextConstraint('_attachments.name'); + } + + public function apply(QueryBuilder $queryBuilder): void + { + $this->applyAllChildFilters($queryBuilder); + } +} diff --git a/src/DataTables/Filters/AssemblySearchFilter.php b/src/DataTables/Filters/AssemblySearchFilter.php new file mode 100644 index 000000000..1627cc612 --- /dev/null +++ b/src/DataTables/Filters/AssemblySearchFilter.php @@ -0,0 +1,183 @@ +. + */ +namespace App\DataTables\Filters; +use Doctrine\ORM\QueryBuilder; + +class AssemblySearchFilter implements FilterInterface +{ + + /** @var boolean Whether to use regex for searching */ + protected bool $regex = false; + + /** @var bool Use name field for searching */ + protected bool $name = true; + + /** @var bool Use description for searching */ + protected bool $description = true; + + /** @var bool Use comment field for searching */ + protected bool $comment = true; + + /** @var bool Use ordernr for searching */ + protected bool $ordernr = true; + + /** @var bool Use Internal part number for searching */ + protected bool $ipn = true; + + public function __construct( + /** @var string The string to query for */ + protected string $keyword + ) + { + } + + protected function getFieldsToSearch(): array + { + $fields_to_search = []; + + if($this->name) { + $fields_to_search[] = 'assembly.name'; + } + if($this->description) { + $fields_to_search[] = 'assembly.description'; + } + if ($this->comment) { + $fields_to_search[] = 'assembly.comment'; + } + if ($this->ipn) { + $fields_to_search[] = 'assembly.ipn'; + } + + return $fields_to_search; + } + + public function apply(QueryBuilder $queryBuilder): void + { + $fields_to_search = $this->getFieldsToSearch(); + + //If we have nothing to search for, do nothing + if ($fields_to_search === [] || $this->keyword === '') { + return; + } + + //Convert the fields to search to a list of expressions + $expressions = array_map(function (string $field): string { + if ($this->regex) { + return sprintf("REGEXP(%s, :search_query) = TRUE", $field); + } + + return sprintf("ILIKE(%s, :search_query) = TRUE", $field); + }, $fields_to_search); + + //Add Or concatenation of the expressions to our query + $queryBuilder->andWhere( + $queryBuilder->expr()->orX(...$expressions) + ); + + //For regex, we pass the query as is, for like we add % to the start and end as wildcards + if ($this->regex) { + $queryBuilder->setParameter('search_query', $this->keyword); + } else { + $queryBuilder->setParameter('search_query', '%' . $this->keyword . '%'); + } + } + + public function getKeyword(): string + { + return $this->keyword; + } + + public function setKeyword(string $keyword): AssemblySearchFilter + { + $this->keyword = $keyword; + return $this; + } + + public function isRegex(): bool + { + return $this->regex; + } + + public function setRegex(bool $regex): AssemblySearchFilter + { + $this->regex = $regex; + return $this; + } + + public function isName(): bool + { + return $this->name; + } + + public function setName(bool $name): AssemblySearchFilter + { + $this->name = $name; + return $this; + } + + public function isCategory(): bool + { + return $this->category; + } + + public function setCategory(bool $category): AssemblySearchFilter + { + $this->category = $category; + return $this; + } + + public function isDescription(): bool + { + return $this->description; + } + + public function setDescription(bool $description): AssemblySearchFilter + { + $this->description = $description; + return $this; + } + + public function isIPN(): bool + { + return $this->ipn; + } + + public function setIPN(bool $ipn): AssemblySearchFilter + { + $this->ipn = $ipn; + return $this; + } + + public function isComment(): bool + { + return $this->comment; + } + + public function setComment(bool $comment): AssemblySearchFilter + { + $this->comment = $comment; + return $this; + } + + +} diff --git a/src/DataTables/Filters/PartFilter.php b/src/DataTables/Filters/PartFilter.php index 8dcbd6b34..5a4706e33 100644 --- a/src/DataTables/Filters/PartFilter.php +++ b/src/DataTables/Filters/PartFilter.php @@ -38,6 +38,7 @@ use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\PartLot; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; @@ -83,6 +84,7 @@ class PartFilter implements FilterInterface public readonly EntityConstraint $lotOwner; public readonly EntityConstraint $measurementUnit; + public readonly EntityConstraint $partCustomState; public readonly TextConstraint $manufacturer_product_url; public readonly TextConstraint $manufacturer_product_number; public readonly IntConstraint $attachmentsCount; @@ -117,6 +119,7 @@ public function __construct(NodesListBuilder $nodesListBuilder) $this->favorite = new BooleanConstraint('part.favorite'); $this->needsReview = new BooleanConstraint('part.needs_review'); $this->measurementUnit = new EntityConstraint($nodesListBuilder, MeasurementUnit::class, 'part.partUnit'); + $this->partCustomState = new EntityConstraint($nodesListBuilder, PartCustomState::class, 'part.partCustomState'); $this->mass = new NumberConstraint('part.mass'); $this->dbId = new IntConstraint('part.id'); $this->ipn = new TextConstraint('part.ipn'); diff --git a/src/DataTables/Helpers/AssemblyDataTableHelper.php b/src/DataTables/Helpers/AssemblyDataTableHelper.php new file mode 100644 index 000000000..dda563ea4 --- /dev/null +++ b/src/DataTables/Helpers/AssemblyDataTableHelper.php @@ -0,0 +1,77 @@ +. + */ + +namespace App\DataTables\Helpers; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\Attachment; +use App\Services\Attachments\AssemblyPreviewGenerator; +use App\Services\Attachments\AttachmentURLGenerator; +use App\Services\EntityURLGenerator; + +/** + * A helper service which contains common code to render columns for assembly related tables + */ +class AssemblyDataTableHelper +{ + public function __construct( + private readonly EntityURLGenerator $entityURLGenerator, + private readonly AssemblyPreviewGenerator $previewGenerator, + private readonly AttachmentURLGenerator $attachmentURLGenerator + ) { + } + + public function renderName(Assembly $context): string + { + $icon = ''; + + return sprintf( + '%s%s', + $this->entityURLGenerator->infoURL($context), + $icon, + htmlspecialchars($context->getName()) + ); + } + + public function renderPicture(Assembly $context): string + { + $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($context); + if (!$preview_attachment instanceof Attachment) { + return ''; + } + + $title = htmlspecialchars($preview_attachment->getName()); + if ($preview_attachment->getFilename()) { + $title .= ' ('.htmlspecialchars($preview_attachment->getFilename()).')'; + } + + return sprintf( + '%s', + 'Assembly image', + $this->attachmentURLGenerator->getThumbnailURL($preview_attachment), + $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_md'), + 'hoverpic assembly-table-image', + $title + ); + } +} diff --git a/src/DataTables/Helpers/ProjectDataTableHelper.php b/src/DataTables/Helpers/ProjectDataTableHelper.php new file mode 100644 index 000000000..baa0e24e1 --- /dev/null +++ b/src/DataTables/Helpers/ProjectDataTableHelper.php @@ -0,0 +1,48 @@ +. + */ + +namespace App\DataTables\Helpers; + +use App\Entity\ProjectSystem\Project; +use App\Services\EntityURLGenerator; + +/** + * A helper service which contains common code to render columns for project related tables + */ +class ProjectDataTableHelper +{ + public function __construct(private readonly EntityURLGenerator $entityURLGenerator) { + } + + public function renderName(Project $context): string + { + $icon = ''; + + return sprintf( + '%s%s', + $this->entityURLGenerator->infoURL($context), + $icon, + htmlspecialchars($context->getName()) + ); + } +} diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index f0decf271..e628d4f0a 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -39,6 +39,7 @@ use App\DataTables\Helpers\ColumnSortHelper; use App\DataTables\Helpers\PartDataTableHelper; use App\Doctrine\Helpers\FieldHelper; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; @@ -172,6 +173,19 @@ public function configure(DataTable $dataTable, array $options): void return $tmp; } ]) + ->add('partCustomState', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.partCustomState'), + 'orderField' => 'NATSORT(_partCustomState.name)', + 'render' => function($value, Part $context): string { + $partCustomState = $context->getPartCustomState(); + + if ($partCustomState === null) { + return ''; + } + + return htmlspecialchars($partCustomState->getName()); + } + ]) ->add('addedDate', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.addedDate'), ]) @@ -238,6 +252,34 @@ public function configure(DataTable $dataTable, array $options): void ]); } + //Add a assembly column to list where the part is used, when the user has the permission to see the assemblies + if ($this->security->isGranted('read', Assembly::class)) { + $this->csh->add('assemblies', TextColumn::class, [ + 'label' => $this->translator->trans('assembly.labelp'), + 'render' => function ($value, Part $context): string { + //Only show the first 5 assembly names + $assemblies = $context->getAssemblies(); + $tmp = ""; + + $max = 5; + + for ($i = 0; $i < min($max, count($assemblies)); $i++) { + $url = $this->urlGenerator->infoURL($assemblies[$i]); + $tmp .= sprintf('%s', $url, htmlspecialchars($assemblies[$i]->getName())); + if ($i < count($assemblies) - 1) { + $tmp .= ", "; + } + } + + if (count($assemblies) > $max) { + $tmp .= ", + ".(count($assemblies) - $max); + } + + return $tmp; + } + ]); + } + $this->csh ->add('edit', IconLinkColumn::class, [ 'label' => $this->translator->trans('part.table.edit'), @@ -307,6 +349,7 @@ private function getDetailQuery(QueryBuilder $builder, array $filter_results): v ->addSelect('footprint') ->addSelect('manufacturer') ->addSelect('partUnit') + ->addSelect('partCustomState') ->addSelect('master_picture_attachment') ->addSelect('footprint_attachment') ->addSelect('partLots') @@ -325,6 +368,7 @@ private function getDetailQuery(QueryBuilder $builder, array $filter_results): v ->leftJoin('orderdetails.supplier', 'suppliers') ->leftJoin('part.attachments', 'attachments') ->leftJoin('part.partUnit', 'partUnit') + ->leftJoin('part.partCustomState', 'partCustomState') ->leftJoin('part.parameters', 'parameters') ->where('part.id IN (:ids)') ->setParameter('ids', $ids) @@ -342,6 +386,7 @@ private function getDetailQuery(QueryBuilder $builder, array $filter_results): v ->addGroupBy('suppliers') ->addGroupBy('attachments') ->addGroupBy('partUnit') + ->addGroupBy('partCustomState') ->addGroupBy('parameters'); //Get the results in the same order as the IDs were passed @@ -413,6 +458,10 @@ private function addJoins(QueryBuilder $builder): QueryBuilder $builder->leftJoin('part.partUnit', '_partUnit'); $builder->addGroupBy('_partUnit'); } + if (str_contains($dql, '_partCustomState')) { + $builder->leftJoin('part.partCustomState', '_partCustomState'); + $builder->addGroupBy('_partCustomState'); + } if (str_contains($dql, '_parameters')) { $builder->leftJoin('part.parameters', '_parameters'); //Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1 diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index fcb069844..89572a8ab 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -41,11 +41,14 @@ 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 EntityURLGenerator $entityURLGenerator, + protected AmountFormatter $amountFormatter + ) { } - public function configure(DataTable $dataTable, array $options): void { $dataTable @@ -105,6 +108,8 @@ public function configure(DataTable $dataTable, array $options): void if($context->getPart() instanceof Part) { return $context->getPart()->getIpn(); } + + return ''; } ]) ->add('description', MarkdownColumn::class, [ diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php new file mode 100644 index 000000000..fde61cd90 --- /dev/null +++ b/src/Entity/AssemblySystem/Assembly.php @@ -0,0 +1,430 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\AssemblySystem; + +use App\Repository\AssemblyRepository; +use App\Validator\Constraints\AssemblySystem\AssemblyCycle; +use App\Validator\Constraints\AssemblySystem\AssemblyInvalidBomEntry; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +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 App\Validator\Constraints\AssemblySystem\UniqueReferencedAssembly; +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\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Context\ExecutionContextInterface; + +/** + * This class represents a assembly in the database. + * + * @extends AbstractStructuralDBElement + */ +#[ORM\Entity(repositoryClass: AssemblyRepository::class)] +#[ORM\Table(name: 'assemblies')] +#[UniqueEntity(fields: ['ipn'], message: 'assembly.ipn.must_be_unique')] +#[ORM\Index(columns: ['ipn'], name: 'assembly_idx_ipn')] +#[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", "ipn"])] +#[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] + #[AssemblyCycle] + #[AssemblyInvalidBomEntry] + #[UniqueReferencedAssembly] + #[Groups(['extended', 'full', 'import'])] + #[ORM\OneToMany(targetEntity: AssemblyBOMEntry::class, mappedBy: 'assembly', cascade: ['persist', 'remove'], orphanRemoval: true)] + #[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])] + #[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 string|null The internal ipn number of the assembly + */ + #[Assert\Length(max: 100)] + #[Groups(['extended', 'full', 'assembly:read', 'assembly:write', 'import'])] + #[ORM\Column(type: Types::STRING, length: 100, unique: true, nullable: true)] + #[Length(max: 100)] + protected ?string $ipn = null; + + /** + * @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; + } + + /** + * Returns the internal part number of the assembly. + * @return string + */ + public function getIpn(): ?string + { + return $this->ipn; + } + + /** + * Sets the internal part number of the assembly. + * @param string $ipn The new IPN of the assembly + */ + public function setIpn(?string $ipn): Assembly + { + $this->ipn = $ipn; + return $this; + } + + /** + * Checks if this assembly has an associated part representing the builds of this assembly in the stock. + */ + 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(); + } + } + } + + /** + * Get all assemblies and sub-assemblies recursive that are referenced in the assembly bom entries. + * + * @param Assembly $assembly Assembly, which is to be processed recursively. + * @param array $processedAssemblies (optional) a list of the already edited assemblies to avoid circulatory references. + * @return Assembly[] A flat list of all recursively found assemblies. + */ + public function getAllReferencedAssembliesRecursive(Assembly $assembly, array &$processedAssemblies = []): array + { + $assemblies = []; + + // Avoid circular references + if (in_array($assembly, $processedAssemblies, true)) { + return $assemblies; + } + + // Add the current assembly to the processed + $processedAssemblies[] = $assembly; + + // Iterate by the bom entries of the current assembly + foreach ($assembly->getBomEntries() as $bomEntry) { + if ($bomEntry->getReferencedAssembly() !== null) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + $assemblies[] = $referencedAssembly; + + // Continue recursively to process sub-assemblies + $assemblies = array_merge($assemblies, $this->getAllReferencedAssembliesRecursive($referencedAssembly, $processedAssemblies)); + } + } + + return $assemblies; + } + +} diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php new file mode 100644 index 000000000..9620e4891 --- /dev/null +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -0,0 +1,338 @@ +. + */ + +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\Repository\DBElementRepository; +use App\Validator\Constraints\AssemblySystem\AssemblyCycle; +use App\Validator\Constraints\AssemblySystem\AssemblyInvalidBomEntry; +use App\Validator\UniqueValidatableInterface; +use Doctrine\DBAL\Types\Types; +use App\Entity\Base\AbstractDBElement; +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.getReferencedAssembly() !== null or this.getName() !== null', message: 'validator.assembly.bom_entry.name_or_part_needed')] + #[ORM\Column(type: Types::STRING, nullable: true)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] + protected ?string $name = null; + + /** + * @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 Assembly|null The associated assembly + */ + #[Assert\Expression( + '(this.getPart() === null or this.getReferencedAssembly() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))', + message: 'validator.assembly.bom_entry.only_part_or_assembly_allowed' + )] + #[AssemblyCycle] + #[AssemblyInvalidBomEntry] + #[ORM\ManyToOne(targetEntity: Assembly::class)] + #[ORM\JoinColumn(name: 'id_referenced_assembly', nullable: true, onDelete: 'SET NULL')] + #[Groups(['bom_entry:read', 'bom_entry:write'])] + protected ?Assembly $referencedAssembly = 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 trim($this->name ?? '') === '' ? null : $this->name; + } + + /** + * @param string $name + */ + public function setName(?string $name): AssemblyBOMEntry + { + $this->name = trim($name ?? '') === '' ? null : $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; + } + + public function getReferencedAssembly(): ?Assembly + { + return $this->referencedAssembly; + } + + public function setReferencedAssembly(?Assembly $referencedAssembly): AssemblyBOMEntry + { + $this->referencedAssembly = $referencedAssembly; + 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; + } + + /** + * Checks whether this BOM entry is a assembly associated BOM entry or not. + * @return bool True if this BOM entry is a assembly associated BOM entry, false otherwise. + */ + public function isAssemblyBomEntry(): bool + { + return $this->referencedAssembly !== null; + } + + #[Assert\Callback] + public function validate(ExecutionContextInterface $context, $payload): void + { + //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(), + 'referencedAssembly' => $this->getReferencedAssembly()?->getID(), + ]; + } +} diff --git a/src/Entity/Attachments/AssemblyAttachment.php b/src/Entity/Attachments/AssemblyAttachment.php new file mode 100644 index 000000000..bb9a11c8f --- /dev/null +++ b/src/Entity/Attachments/AssemblyAttachment.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Attachments; + +use App\Entity\AssemblySystem\Assembly; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; + +/** + * A attachment attached to a device element. + * @extends Attachment + */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] +class AssemblyAttachment extends Attachment +{ + final public const ALLOWED_ELEMENT_CLASS = Assembly::class; + /** + * @var Assembly|null the element this attachment is associated with + */ + #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AttachmentContainingDBElement $element = null; +} diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index 00cf581a8..70ffbaa01 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -97,7 +97,7 @@ #[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)] abstract class Attachment extends AbstractNamedDBElement { - private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class, + private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'PartCustomState' => PartCustomStateAttachment::class, 'Device' => ProjectAttachment::class, 'Assembly' => AssemblyAttachment::class, 'AttachmentType' => AttachmentTypeAttachment::class, 'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class, 'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class, @@ -107,7 +107,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, "PartCustomState" => PartCustomStateAttachment::class, "Project" => ProjectAttachment::class, "Assembly" => AssemblyAttachment::class, + "AttachmentType" => AttachmentTypeAttachment::class, "Category" => CategoryAttachment::class, "Footprint" => FootprintAttachment::class, "Manufacturer" => ManufacturerAttachment::class, "Currency" => CurrencyAttachment::class, "Group" => GroupAttachment::class, "MeasurementUnit" => MeasurementUnitAttachment::class, "StorageLocation" => StorageLocationAttachment::class, "Supplier" => SupplierAttachment::class, "User" => UserAttachment::class, "LabelProfile" => LabelAttachment::class]; @@ -550,8 +551,8 @@ public function setAttachmentType(AttachmentType $attachement_type): self */ #[Groups(['attachment:write'])] #[SerializedName('url')] - #[ApiProperty(description: 'Set the path of the attachment here. - Provide either an external URL, a path to a builtin file (like %FOOTPRINTS%/Active/ICs/IC_DFS.png) or an empty + #[ApiProperty(description: 'Set the path of the attachment here. + Provide either an external URL, a path to a builtin file (like %FOOTPRINTS%/Active/ICs/IC_DFS.png) or an empty string if the attachment has an internal file associated and you\'d like to reset the external source. If you set a new (nonempty) file path any associated internal file will be removed!')] public function setURL(?string $url): self diff --git a/src/Entity/Attachments/PartCustomStateAttachment.php b/src/Entity/Attachments/PartCustomStateAttachment.php new file mode 100644 index 000000000..3a561b136 --- /dev/null +++ b/src/Entity/Attachments/PartCustomStateAttachment.php @@ -0,0 +1,45 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Attachments; + +use App\Entity\Parts\PartCustomState; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; + +/** + * An attachment attached to a part custom state element. + * @extends Attachment + */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] +class PartCustomStateAttachment extends Attachment +{ + final public const ALLOWED_ELEMENT_CLASS = PartCustomState::class; + + #[ORM\ManyToOne(targetEntity: PartCustomState::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AttachmentContainingDBElement $element = null; +} diff --git a/src/Entity/Base/AbstractDBElement.php b/src/Entity/Base/AbstractDBElement.php index 9fb5d6489..27d60c865 100644 --- a/src/Entity/Base/AbstractDBElement.php +++ b/src/Entity/Base/AbstractDBElement.php @@ -22,6 +22,9 @@ namespace App\Entity\Base; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentTypeAttachment; @@ -33,6 +36,7 @@ use App\Entity\Attachments\ManufacturerAttachment; use App\Entity\Attachments\MeasurementUnitAttachment; use App\Entity\Attachments\PartAttachment; +use App\Entity\Attachments\PartCustomStateAttachment; use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\StorageLocationAttachment; use App\Entity\Attachments\SupplierAttachment; @@ -40,6 +44,7 @@ use App\Entity\Parameters\AbstractParameter; use App\Entity\Parts\Category; use App\Entity\PriceInformations\Pricedetail; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Entity\Parts\Footprint; @@ -68,7 +73,41 @@ * Every database table which are managed with this class (or a subclass of it) * must have the table row "id"!! The ID is the unique key to identify the elements. */ -#[DiscriminatorMap(typeProperty: 'type', mapping: ['attachment_type' => AttachmentType::class, 'attachment' => Attachment::class, 'attachment_type_attachment' => AttachmentTypeAttachment::class, 'category_attachment' => CategoryAttachment::class, 'currency_attachment' => CurrencyAttachment::class, 'footprint_attachment' => FootprintAttachment::class, 'group_attachment' => GroupAttachment::class, 'label_attachment' => LabelAttachment::class, 'manufacturer_attachment' => ManufacturerAttachment::class, 'measurement_unit_attachment' => MeasurementUnitAttachment::class, 'part_attachment' => PartAttachment::class, 'project_attachment' => ProjectAttachment::class, '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, + 'part_custom_state_attachment' => PartCustomStateAttachment::class, + 'project_attachment' => ProjectAttachment::class, + 'storelocation_attachment' => StorageLocationAttachment::class, + 'supplier_attachment' => SupplierAttachment::class, + 'user_attachment' => UserAttachment::class, + 'category' => Category::class, + 'project' => Project::class, + 'project_bom_entry' => ProjectBOMEntry::class, + 'footprint' => Footprint::class, + 'group' => Group::class, + 'manufacturer' => Manufacturer::class, + 'orderdetail' => Orderdetail::class, + 'part' => Part::class, + 'part_custom_state' => PartCustomState::class, + 'pricedetail' => Pricedetail::class, + 'storelocation' => StorageLocation::class, + 'part_lot' => PartLot::class, + 'currency' => Currency::class, + 'measurement_unit' => MeasurementUnit::class, + 'parameter' => AbstractParameter::class, + 'supplier' => Supplier::class, + 'user' => User::class] +)] #[ORM\MappedSuperclass(repositoryClass: DBElementRepository::class)] abstract class AbstractDBElement implements JsonSerializable { diff --git a/src/Entity/LogSystem/CollectionElementDeleted.php b/src/Entity/LogSystem/CollectionElementDeleted.php index 16bf33f59..15e0001ed 100644 --- a/src/Entity/LogSystem/CollectionElementDeleted.php +++ b/src/Entity/LogSystem/CollectionElementDeleted.php @@ -41,11 +41,14 @@ 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; use App\Entity\Attachments\CategoryAttachment; use App\Entity\Attachments\CurrencyAttachment; +use App\Entity\Attachments\PartCustomStateAttachment; use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\FootprintAttachment; use App\Entity\Attachments\GroupAttachment; @@ -58,6 +61,9 @@ use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\LogWithEventUndoInterface; use App\Entity\Contracts\NamedElementInterface; +use App\Entity\Parameters\PartCustomStateParameter; +use App\Entity\Parts\PartCustomState; +use App\Entity\Parameters\AssemblyParameter; use App\Entity\ProjectSystem\Project; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parameters\AttachmentTypeParameter; @@ -147,6 +153,7 @@ private function resolveAbstractClassToInstantiableClass(string $abstract_class) { if (is_a($abstract_class, AbstractParameter::class, true)) { return match ($this->getTargetClass()) { + Assembly::class => AssemblyParameter::class, AttachmentType::class => AttachmentTypeParameter::class, Category::class => CategoryParameter::class, Currency::class => CurrencyParameter::class, @@ -158,6 +165,7 @@ private function resolveAbstractClassToInstantiableClass(string $abstract_class) Part::class => PartParameter::class, StorageLocation::class => StorageLocationParameter::class, Supplier::class => SupplierParameter::class, + PartCustomState::class => PartCustomStateParameter::class, default => throw new \RuntimeException('Unknown target class for parameter: '.$this->getTargetClass()), }; } @@ -168,11 +176,13 @@ private function resolveAbstractClassToInstantiableClass(string $abstract_class) Category::class => CategoryAttachment::class, Currency::class => CurrencyAttachment::class, Project::class => ProjectAttachment::class, + Assembly::class => AssemblyAttachment::class, Footprint::class => FootprintAttachment::class, Group::class => GroupAttachment::class, Manufacturer::class => ManufacturerAttachment::class, MeasurementUnit::class => MeasurementUnitAttachment::class, Part::class => PartAttachment::class, + PartCustomState::class => PartCustomStateAttachment::class, StorageLocation::class => StorageLocationAttachment::class, Supplier::class => SupplierAttachment::class, User::class => UserAttachment::class, diff --git a/src/Entity/LogSystem/LogTargetType.php b/src/Entity/LogSystem/LogTargetType.php index 1c6e4f8c0..e25e35dad 100644 --- a/src/Entity/LogSystem/LogTargetType.php +++ b/src/Entity/LogSystem/LogTargetType.php @@ -22,6 +22,8 @@ */ namespace App\Entity\LogSystem; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\LabelSystem\LabelProfile; @@ -32,6 +34,7 @@ use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; use App\Entity\Parts\PartAssociation; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\PartLot; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; @@ -67,6 +70,9 @@ enum LogTargetType: int case LABEL_PROFILE = 19; case PART_ASSOCIATION = 20; + case ASSEMBLY = 21; + case ASSEMBLY_BOM_ENTRY = 22; + case PART_CUSTOM_STATE = 23; /** * Returns the class name of the target type or null if the target type is NONE. @@ -82,6 +88,8 @@ public function toClass(): ?string self::CATEGORY => Category::class, self::PROJECT => Project::class, self::BOM_ENTRY => ProjectBOMEntry::class, + self::ASSEMBLY => Assembly::class, + self::ASSEMBLY_BOM_ENTRY => AssemblyBOMEntry::class, self::FOOTPRINT => Footprint::class, self::GROUP => Group::class, self::MANUFACTURER => Manufacturer::class, @@ -96,6 +104,7 @@ public function toClass(): ?string self::PARAMETER => AbstractParameter::class, self::LABEL_PROFILE => LabelProfile::class, self::PART_ASSOCIATION => PartAssociation::class, + self::PART_CUSTOM_STATE => PartCustomState::class }; } diff --git a/src/Entity/Parameters/AbstractParameter.php b/src/Entity/Parameters/AbstractParameter.php index 39f333dad..01b211511 100644 --- a/src/Entity/Parameters/AbstractParameter.php +++ b/src/Entity/Parameters/AbstractParameter.php @@ -73,7 +73,8 @@ #[ORM\DiscriminatorMap([0 => CategoryParameter::class, 1 => CurrencyParameter::class, 2 => ProjectParameter::class, 3 => FootprintParameter::class, 4 => GroupParameter::class, 5 => ManufacturerParameter::class, 6 => MeasurementUnitParameter::class, 7 => PartParameter::class, 8 => StorageLocationParameter::class, - 9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class])] + 9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class, 11 => AssemblyParameter::class, + 12 => PartCustomStateParameter::class])] #[ORM\Table('parameters')] #[ORM\Index(columns: ['name'], name: 'parameter_name_idx')] #[ORM\Index(columns: ['param_group'], name: 'parameter_group_idx')] @@ -105,7 +106,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu "AttachmentType" => AttachmentTypeParameter::class, "Category" => CategoryParameter::class, "Currency" => CurrencyParameter::class, "Project" => ProjectParameter::class, "Footprint" => FootprintParameter::class, "Group" => GroupParameter::class, "Manufacturer" => ManufacturerParameter::class, "MeasurementUnit" => MeasurementUnitParameter::class, - "StorageLocation" => StorageLocationParameter::class, "Supplier" => SupplierParameter::class]; + "StorageLocation" => StorageLocationParameter::class, "Supplier" => SupplierParameter::class, "PartCustomState" => PartCustomStateParameter::class]; /** * @var string The class of the element that can be passed to this attachment. Must be overridden in subclasses. @@ -460,7 +461,7 @@ protected function formatWithUnit(float $value, string $format = '%g', bool $wit return $str; } - + /** * Returns the class of the element that is allowed to be associated with this attachment. * @return string diff --git a/src/Entity/Parameters/AssemblyParameter.php b/src/Entity/Parameters/AssemblyParameter.php new file mode 100644 index 000000000..349fa7906 --- /dev/null +++ b/src/Entity/Parameters/AssemblyParameter.php @@ -0,0 +1,65 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace App\Entity\Parameters; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Base\AbstractDBElement; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; + +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] +class AssemblyParameter extends AbstractParameter +{ + final public const ALLOWED_ELEMENT_CLASS = Assembly::class; + + /** + * @var Assembly the element this para is associated with + */ + #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; +} diff --git a/src/Entity/Parameters/PartCustomStateParameter.php b/src/Entity/Parameters/PartCustomStateParameter.php new file mode 100644 index 000000000..ceedf7b4d --- /dev/null +++ b/src/Entity/Parameters/PartCustomStateParameter.php @@ -0,0 +1,65 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace App\Entity\Parameters; + +use App\Entity\Base\AbstractDBElement; +use App\Entity\Parts\PartCustomState; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; + +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] +class PartCustomStateParameter extends AbstractParameter +{ + final public const ALLOWED_ELEMENT_CLASS = PartCustomState::class; + + /** + * @var PartCustomState the element this para is associated with + */ + #[ORM\ManyToOne(targetEntity: PartCustomState::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; +} diff --git a/src/Entity/Parts/Category.php b/src/Entity/Parts/Category.php index 99ed3c6d0..7fca81bc2 100644 --- a/src/Entity/Parts/Category.php +++ b/src/Entity/Parts/Category.php @@ -118,6 +118,13 @@ class Category extends AbstractPartsContainingDBElement #[ORM\Column(type: Types::TEXT)] protected string $partname_regex = ''; + /** + * @var string The prefix for ipn generation for created parts in this category. + */ + #[Groups(['full', 'import', 'category:read', 'category:write'])] + #[ORM\Column(type: Types::STRING, length: 255, nullable: false, options: ['default' => ''])] + protected string $part_ipn_prefix = ''; + /** * @var bool Set to true, if the footprints should be disabled for parts this category (not implemented yet). */ @@ -225,6 +232,16 @@ public function setPartnameRegex(string $partname_regex): self return $this; } + public function getPartIpnPrefix(): string + { + return $this->part_ipn_prefix; + } + + public function setPartIpnPrefix(string $part_ipn_prefix): void + { + $this->part_ipn_prefix = $part_ipn_prefix; + } + public function isDisableFootprints(): bool { return $this->disable_footprints; diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index 14a7903fc..3a582edd7 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -23,6 +23,7 @@ namespace App\Entity\Parts; use App\ApiPlatform\Filter\TagFilter; +use App\Entity\Parts\PartTraits\AssemblyTrait; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; @@ -60,7 +61,6 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -74,7 +74,6 @@ * @extends AttachmentContainingDBElement * @template-use ParametersTrait */ -#[UniqueEntity(fields: ['ipn'], message: 'part.ipn.must_be_unique')] #[ORM\Entity(repositoryClass: PartRepository::class)] #[ORM\EntityListeners([TreeCacheInvalidationListener::class])] #[ORM\Table('`parts`')] @@ -96,7 +95,7 @@ denormalizationContext: ['groups' => ['part:write', 'api:basic:write', 'eda_info:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], )] #[ApiFilter(PropertyFilter::class)] -#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])] +#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit", "partCustomState"])] #[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "manufacturer_product_number"])] #[ApiFilter(TagFilter::class, properties: ["tags"])] @@ -114,6 +113,7 @@ class Part extends AttachmentContainingDBElement use OrderTrait; use ParametersTrait; use ProjectTrait; + use AssemblyTrait; use AssociationTrait; use EDATrait; @@ -169,6 +169,7 @@ public function __construct() $this->orderdetails = new ArrayCollection(); $this->parameters = new ArrayCollection(); $this->project_bom_entries = new ArrayCollection(); + $this->assembly_bom_entries = new ArrayCollection(); $this->associated_parts_as_owner = new ArrayCollection(); $this->associated_parts_as_other = new ArrayCollection(); diff --git a/src/Entity/Parts/PartCustomState.php b/src/Entity/Parts/PartCustomState.php new file mode 100644 index 000000000..e8429199c --- /dev/null +++ b/src/Entity/Parts/PartCustomState.php @@ -0,0 +1,127 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Parts; + +use ApiPlatform\Metadata\ApiProperty; +use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\PartCustomStateAttachment; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Base\AbstractPartsContainingDBElement; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Parameters\PartCustomStateParameter; +use App\Repository\Parts\PartCustomStateRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\Criteria; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * This entity represents a custom part state. + * If an organisation uses Part-DB and has its custom part states, this is useful. + * + * @extends AbstractPartsContainingDBElement + */ +#[ORM\Entity(repositoryClass: PartCustomStateRepository::class)] +#[ORM\Table('`part_custom_states`')] +#[ORM\Index(columns: ['name'], name: 'part_custom_state_name')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@part_custom_states.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['part_custom_state:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['part_custom_state:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] +class PartCustomState extends AbstractPartsContainingDBElement +{ + /** + * @var string The comment info for this element as markdown + */ + #[Groups(['part_custom_state:read', 'part_custom_state:write', 'full', 'import'])] + protected string $comment = ''; + + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class, cascade: ['persist'])] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; + + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['part_custom_state:read', 'part_custom_state:write'])] + #[ApiProperty(readableLink: false, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; + + /** + * @var Collection + */ + #[Assert\Valid] + #[ORM\OneToMany(targetEntity: PartCustomStateAttachment::class, mappedBy: 'element', cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + #[Groups(['part_custom_state:read', 'part_custom_state:write'])] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: PartCustomStateAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['part_custom_state:read', 'part_custom_state:write'])] + protected ?Attachment $master_picture_attachment = null; + + /** @var Collection + */ + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: PartCustomStateAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => 'ASC'])] + #[Groups(['part_custom_state:read', 'part_custom_state:write'])] + protected Collection $parameters; + + #[Groups(['part_custom_state:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['part_custom_state:read'])] + protected ?\DateTimeImmutable $lastModified = null; + + public function __construct() + { + parent::__construct(); + $this->children = new ArrayCollection(); + $this->attachments = new ArrayCollection(); + $this->parameters = new ArrayCollection(); + } +} diff --git a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php index 230ba7b76..b4138f934 100644 --- a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php @@ -23,12 +23,14 @@ namespace App\Entity\Parts\PartTraits; use App\Entity\Parts\InfoProviderReference; +use App\Entity\Parts\PartCustomState; use Doctrine\DBAL\Types\Types; use App\Entity\Parts\Part; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints\Length; +use App\Validator\Constraints\UniquePartIpnConstraint; /** * Advanced properties of a part, not related to a more specific group. @@ -62,8 +64,9 @@ trait AdvancedPropertyTrait */ #[Assert\Length(max: 100)] #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] - #[ORM\Column(type: Types::STRING, length: 100, unique: true, nullable: true)] + #[ORM\Column(type: Types::STRING, length: 100, nullable: true)] #[Length(max: 100)] + #[UniquePartIpnConstraint] protected ?string $ipn = null; /** @@ -73,6 +76,14 @@ trait AdvancedPropertyTrait #[Groups(['full', 'part:read'])] protected InfoProviderReference $providerReference; + /** + * @var ?PartCustomState the custom state for the part + */ + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\ManyToOne(targetEntity: PartCustomState::class)] + #[ORM\JoinColumn(name: 'id_part_custom_state')] + protected ?PartCustomState $partCustomState = null; + /** * Checks if this part is marked, for that it needs further review. */ @@ -180,7 +191,24 @@ public function setProviderReference(InfoProviderReference $providerReference): return $this; } + /** + * Gets the custom part state for the part + * Returns null if no specific part state is set. + */ + public function getPartCustomState(): ?PartCustomState + { + return $this->partCustomState; + } + /** + * Sets the custom part state. + * + * @return $this + */ + public function setPartCustomState(?PartCustomState $partCustomState): self + { + $this->partCustomState = $partCustomState; - + return $this; + } } diff --git a/src/Entity/Parts/PartTraits/AssemblyTrait.php b/src/Entity/Parts/PartTraits/AssemblyTrait.php new file mode 100644 index 000000000..57f78d352 --- /dev/null +++ b/src/Entity/Parts/PartTraits/AssemblyTrait.php @@ -0,0 +1,83 @@ + $assembly_bom_entries + */ + #[ORM\OneToMany(mappedBy: 'part', targetEntity: AssemblyBOMEntry::class, cascade: ['remove'], orphanRemoval: true)] + protected Collection $assembly_bom_entries; + + /** + * @var Assembly|null If a assembly is set here, then this part is special and represents the builds of an assembly. + */ + #[ORM\OneToOne(inversedBy: 'build_part', targetEntity: Assembly::class)] + #[ORM\JoinColumn] + protected ?Assembly $built_assembly = null; + + /** + * Returns all AssemblyBOMEntry that use this part. + * + * @phpstan-return Collection + */ + public function getAssemblyBomEntries(): Collection + { + return $this->assembly_bom_entries; + } + + /** + * Checks whether this part represents the builds of a assembly + * @return bool True if it represents the builds, false if not + */ + #[Groups(['part:read'])] + public function isAssemblyBuildPart(): bool + { + return $this->built_assembly !== null; + } + + /** + * Returns the assembly that this part represents the builds of, or null if it doesn't + */ + public function getBuiltAssembly(): ?Assembly + { + return $this->built_assembly; + } + + + /** + * Sets the assembly that this part represents the builds of + * @param Assembly|null $built_assembly The assembly that this part represents the builds of, or null if it is not a build part + */ + public function setBuiltAssembly(?Assembly $built_assembly): self + { + $this->built_assembly = $built_assembly; + return $this; + } + + + /** + * Get all assemblies which uses this part. + * + * @return Assembly[] all assemblies which uses this part as a one-dimensional array of Assembly objects + */ + public function getAssemblies(): array + { + $assemblies = []; + + foreach($this->assembly_bom_entries as $entry) { + $assemblies[] = $entry->getAssembly(); + } + + return $assemblies; + } +} diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index 2a7862ec5..b2a3b2e95 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -36,6 +36,7 @@ use ApiPlatform\Serializer\Filter\PropertyFilter; use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Contracts\TimeStampableInterface; +use App\Repository\DBElementRepository; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; @@ -54,7 +55,7 @@ * The ProjectBOMEntry class represents an entry in a project's BOM. */ #[ORM\HasLifecycleCallbacks] -#[ORM\Entity] +#[ORM\Entity(repositoryClass: DBElementRepository::class)] #[ORM\Table('project_bom_entries')] #[ApiResource( operations: [ @@ -212,8 +213,6 @@ public function setProject(?Project $project): ProjectBOMEntry return $this; } - - public function getPart(): ?Part { return $this->part; diff --git a/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php new file mode 100644 index 000000000..c5d22dbef --- /dev/null +++ b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php @@ -0,0 +1,73 @@ +getObject(); + + if ($entity instanceof Part) { + $this->ensureUniqueIpn($entity); + } + } + + public function preUpdate(LifecycleEventArgs $args): void + { + $entity = $args->getObject(); + + if ($entity instanceof Part) { + $this->ensureUniqueIpn($entity); + } + } + + private function ensureUniqueIpn(Part $part): void + { + if ($part->getIpn() === null || $part->getIpn() === '') { + return; + } + + $existingPart = $this->entityManager + ->getRepository(Part::class) + ->findOneBy(['ipn' => $part->getIpn()]); + + if ($existingPart && $existingPart->getId() !== $part->getId()) { + if ($this->enforceUniqueIpn) { + return; + } + + // Anhang eines Inkrements bis ein einzigartiger Wert gefunden wird + $increment = 1; + $originalIpn = $part->getIpn(); + + while ($this->entityManager + ->getRepository(Part::class) + ->findOneBy(['ipn' => $originalIpn . "_$increment"])) { + $increment++; + } + + $part->setIpn($originalIpn . "_$increment"); + } + } +} diff --git a/src/Form/AdminPages/AssemblyAdminForm.php b/src/Form/AdminPages/AssemblyAdminForm.php new file mode 100644 index 000000000..2c2f3cc48 --- /dev/null +++ b/src/Form/AdminPages/AssemblyAdminForm.php @@ -0,0 +1,82 @@ +. + */ +namespace App\Form\AdminPages; + +use App\Entity\Base\AbstractNamedDBElement; +use App\Form\AssemblySystem\AssemblyBOMEntryCollectionType; +use App\Form\Type\RichTextEditorType; +use App\Services\LogSystem\EventCommentNeededHelper; +use App\Settings\MiscSettings\AssemblySettings; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; + +class AssemblyAdminForm extends BaseEntityAdminForm +{ + public function __construct( + protected Security $security, + protected EventCommentNeededHelper $eventCommentNeededHelper, + protected AssemblySettings $assemblySettings, + ) { + parent::__construct($security, $eventCommentNeededHelper, $assemblySettings); + } + + 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', + ], + ]); + + $builder->add('ipn', TextType::class, [ + 'required' => false, + 'empty_data' => null, + 'label' => 'assembly.edit.ipn', + ]); + } +} diff --git a/src/Form/AdminPages/BaseEntityAdminForm.php b/src/Form/AdminPages/BaseEntityAdminForm.php index 5a4ef5bce..5ffd7f4df 100644 --- a/src/Form/AdminPages/BaseEntityAdminForm.php +++ b/src/Form/AdminPages/BaseEntityAdminForm.php @@ -22,10 +22,12 @@ namespace App\Form\AdminPages; +use App\Entity\AssemblySystem\Assembly; use App\Entity\PriceInformations\Currency; use App\Entity\ProjectSystem\Project; use App\Entity\UserSystem\Group; use App\Services\LogSystem\EventCommentType; +use App\Settings\MiscSettings\AssemblySettings; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; @@ -47,8 +49,11 @@ class BaseEntityAdminForm extends AbstractType { - public function __construct(protected Security $security, protected EventCommentNeededHelper $eventCommentNeededHelper) - { + public function __construct( + protected Security $security, + protected EventCommentNeededHelper $eventCommentNeededHelper, + protected AssemblySettings $assemblySettings, + ) { } public function configureOptions(OptionsResolver $resolver): void @@ -69,6 +74,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('name', TextType::class, [ 'empty_data' => '', 'label' => 'name.label', + 'data' => $is_new && $entity instanceof Assembly && $this->assemblySettings->useIpnPlaceholderInName ? '%%ipn%%' : $entity->getName(), 'attr' => [ 'placeholder' => 'part.name.placeholder', ], @@ -114,7 +120,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ); } - if ($entity instanceof AbstractStructuralDBElement && !($entity instanceof Group || $entity instanceof Project || $entity instanceof Currency)) { + if ($entity instanceof AbstractStructuralDBElement && !($entity instanceof Group || $entity instanceof Project || $entity instanceof Assembly || $entity instanceof Currency)) { $builder->add('alternative_names', TextType::class, [ 'required' => false, 'label' => 'entity.edit.alternative_names.label', diff --git a/src/Form/AdminPages/CategoryAdminForm.php b/src/Form/AdminPages/CategoryAdminForm.php index 44c1dede7..489649ede 100644 --- a/src/Form/AdminPages/CategoryAdminForm.php +++ b/src/Form/AdminPages/CategoryAdminForm.php @@ -84,6 +84,17 @@ protected function additionalFormElements(FormBuilderInterface $builder, array $ 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); + $builder->add('part_ipn_prefix', TextType::class, [ + 'required' => false, + 'empty_data' => '', + 'label' => 'category.edit.part_ipn_prefix', + 'help' => 'category.edit.part_ipn_prefix.help', + 'attr' => [ + 'placeholder' => 'category.edit.part_ipn_prefix.placeholder', + ], + 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), + ]); + $builder->add('default_description', RichTextEditorType::class, [ 'required' => false, 'empty_data' => '', diff --git a/src/Form/AdminPages/PartCustomStateAdminForm.php b/src/Form/AdminPages/PartCustomStateAdminForm.php new file mode 100644 index 000000000..b8bb2815e --- /dev/null +++ b/src/Form/AdminPages/PartCustomStateAdminForm.php @@ -0,0 +1,27 @@ +. + */ + +declare(strict_types=1); + +namespace App\Form\AdminPages; + +class PartCustomStateAdminForm extends BaseEntityAdminForm +{ +} diff --git a/src/Form/AssemblySystem/AssemblyAddPartsType.php b/src/Form/AssemblySystem/AssemblyAddPartsType.php new file mode 100644 index 000000000..1fa671266 --- /dev/null +++ b/src/Form/AssemblySystem/AssemblyAddPartsType.php @@ -0,0 +1,91 @@ +. + */ +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'], message: 'assembly.bom_entry.part_already_in_bom', + entityClass: AssemblyBOMEntry::class), + new UniqueEntity(fields: ['referencedAssembly'], message: 'assembly.bom_entry.assembly_already_in_bom', + entityClass: AssemblyBOMEntry::class), + new UniqueEntity(fields: ['name'], message: 'assembly.bom_entry.name_already_in_bom', + entityClass: AssemblyBOMEntry::class, ignoreNull: true), + ] + ], + 'constraints' => [ + new UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part']), + new UniqueObjectCollection(message: 'assembly.bom_entry.assembly_already_in_bom', fields: ['referencedAssembly']), + new UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name']), + ] + ]); + $builder->add('submit', SubmitType::class, ['label' => 'save']); + + //After submit set the assembly for all bom entries, so that it can be validated properly + $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) { + $form = $event->getForm(); + /** @var Assembly $assembly */ + $assembly = $form->get('assembly')->getData(); + $bom_entries = $form->get('bom_entries')->getData(); + + foreach ($bom_entries as $bom_entry) { + $bom_entry->setAssembly($assembly); + } + }); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'assembly' => null, + ]); + + $resolver->setAllowedTypes('assembly', ['null', Assembly::class]); + } +} diff --git a/src/Form/AssemblySystem/AssemblyBOMEntryCollectionType.php b/src/Form/AssemblySystem/AssemblyBOMEntryCollectionType.php new file mode 100644 index 000000000..04293f4e0 --- /dev/null +++ b/src/Form/AssemblySystem/AssemblyBOMEntryCollectionType.php @@ -0,0 +1,32 @@ +setDefaults([ + 'entry_type' => AssemblyBOMEntryType::class, + 'entry_options' => [ + 'label' => false, + ], + 'allow_add' => true, + 'allow_delete' => true, + 'by_reference' => false, + 'reindex_enable' => true, + 'label' => false, + ]); + } +} diff --git a/src/Form/AssemblySystem/AssemblyBOMEntryType.php b/src/Form/AssemblySystem/AssemblyBOMEntryType.php new file mode 100644 index 000000000..851ab8157 --- /dev/null +++ b/src/Form/AssemblySystem/AssemblyBOMEntryType.php @@ -0,0 +1,92 @@ +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('referencedAssembly', AssemblySelectType::class, [ + 'label' => 'assembly.bom.referencedAssembly', + 'required' => false, + ]) + ->add('name', TextType::class, [ + 'label' => 'assembly.bom.name', + 'required' => false, + ]) + ->add('mountnames', TextType::class, [ + 'required' => false, + 'label' => 'assembly.bom.mountnames', + 'empty_data' => '', + 'attr' => [ + 'class' => 'tagsinput', + 'data-controller' => 'elements--tagsinput', + ] + ]) + ->add('comment', RichTextEditorType::class, [ + 'required' => false, + 'label' => 'assembly.bom.comment', + 'empty_data' => '', + 'mode' => 'markdown-single_line', + 'attr' => [ + 'rows' => 2, + ], + ]) + ->add('price', BigDecimalNumberType::class, [ + 'label' => false, + 'required' => false, + 'scale' => 5, + 'html5' => true, + 'attr' => [ + 'min' => 0, + 'step' => 'any', + ], + ]) + ->add('priceCurrency', CurrencyEntityType::class, [ + 'required' => false, + 'label' => false, + 'short' => true, + ] + ); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => AssemblyBOMEntry::class, + ]); + } +} diff --git a/src/Form/AssemblySystem/AssemblyBuildType.php b/src/Form/AssemblySystem/AssemblyBuildType.php new file mode 100644 index 000000000..e87acb868 --- /dev/null +++ b/src/Form/AssemblySystem/AssemblyBuildType.php @@ -0,0 +1,182 @@ +. + */ +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, define the fields after we know the data + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { + $form = $event->getForm(); + /** @var AssemblyBuildRequest $assemblyBuildRequest */ + $assemblyBuildRequest = $event->getData(); + + $form->add('addBuildsToBuildsPart', CheckboxType::class, [ + 'label' => 'assembly.build.add_builds_to_builds_part', + 'required' => false, + 'disabled' => !$assemblyBuildRequest->getAssembly()->getBuildPart() instanceof Part, + ]); + + if ($assemblyBuildRequest->getAssembly()->getBuildPart() instanceof Part) { + $form->add('buildsPartLot', PartLotSelectType::class, [ + 'label' => 'assembly.build.builds_part_lot', + 'required' => false, + 'part' => $assemblyBuildRequest->getAssembly()->getBuildPart(), + 'placeholder' => 'assembly.build.buildsPartLot.new_lot' + ]); + } + + foreach ($assemblyBuildRequest->getPartBomEntries() as $bomEntry) { + //Every part lot has a field to specify the number of parts to take from this lot + foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($bomEntry) as $lot) { + $form->add('lot_' . $lot->getID(), SIUnitType::class, [ + 'label' => false, + 'measurement_unit' => $bomEntry->getPart()->getPartUnit(), + 'max' => min($assemblyBuildRequest->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/AssemblyFilterType.php b/src/Form/Filters/AssemblyFilterType.php new file mode 100644 index 000000000..acfbb1a8e --- /dev/null +++ b/src/Form/Filters/AssemblyFilterType.php @@ -0,0 +1,114 @@ +. + */ +namespace App\Form\Filters; + +use App\DataTables\Filters\AssemblyFilter; +use App\Entity\Attachments\AttachmentType; +use App\Form\Filters\Constraints\DateTimeConstraintType; +use App\Form\Filters\Constraints\NumberConstraintType; +use App\Form\Filters\Constraints\StructuralEntityConstraintType; +use App\Form\Filters\Constraints\TextConstraintType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ResetType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class AssemblyFilterType extends AbstractType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => AssemblyFilter::class, + 'csrf_protection' => false, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + /* + * Common tab + */ + + $builder->add('name', TextConstraintType::class, [ + 'label' => 'assembly.filter.name', + ]); + + $builder->add('description', TextConstraintType::class, [ + 'label' => 'assembly.filter.description', + ]); + + $builder->add('comment', TextConstraintType::class, [ + 'label' => 'assembly.filter.comment' + ]); + + /* + * Advanced tab + */ + + $builder->add('dbId', NumberConstraintType::class, [ + 'label' => 'assembly.filter.dbId', + 'min' => 1, + 'step' => 1, + ]); + + $builder->add('ipn', TextConstraintType::class, [ + 'label' => 'assembly.filter.ipn', + ]); + + $builder->add('lastModified', DateTimeConstraintType::class, [ + 'label' => 'lastModified' + ]); + + $builder->add('addedDate', DateTimeConstraintType::class, [ + 'label' => 'createdAt' + ]); + + /** + * Attachments count + */ + $builder->add('attachmentsCount', NumberConstraintType::class, [ + 'label' => 'assembly.filter.attachments_count', + 'step' => 1, + 'min' => 0, + ]); + + $builder->add('attachmentType', StructuralEntityConstraintType::class, [ + 'label' => 'attachment.attachment_type', + 'entity_class' => AttachmentType::class + ]); + + $builder->add('attachmentName', TextConstraintType::class, [ + 'label' => 'assembly.filter.attachmentName', + ]); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'filter.submit', + ]); + + $builder->add('discard', ResetType::class, [ + 'label' => 'filter.discard', + ]); + } +} diff --git a/src/Form/Filters/AttachmentFilterType.php b/src/Form/Filters/AttachmentFilterType.php index ff80bd384..a44588955 100644 --- a/src/Form/Filters/AttachmentFilterType.php +++ b/src/Form/Filters/AttachmentFilterType.php @@ -23,6 +23,7 @@ namespace App\Form\Filters; use App\DataTables\Filters\AttachmentFilter; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Attachments\CategoryAttachment; @@ -80,6 +81,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'category.label' => CategoryAttachment::class, 'currency.label' => CurrencyAttachment::class, 'project.label' => ProjectAttachment::class, + 'assembly.label' => AssemblyAttachment::class, 'footprint.label' => FootprintAttachment::class, 'group.label' => GroupAttachment::class, 'label_profile.label' => LabelAttachment::class, diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php index dfe449d18..7d0c14aa1 100644 --- a/src/Form/Filters/PartFilterType.php +++ b/src/Form/Filters/PartFilterType.php @@ -29,6 +29,7 @@ use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\ProjectSystem\Project; @@ -130,6 +131,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'entity_class' => MeasurementUnit::class ]); + $builder->add('partCustomState', StructuralEntityConstraintType::class, [ + 'label' => 'part.edit.partCustomState', + 'entity_class' => PartCustomState::class + ]); + $builder->add('lastModified', DateTimeConstraintType::class, [ 'label' => 'lastModified' ]); diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index 0bd3d0e3f..1c904eaa1 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -30,6 +30,7 @@ use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; +use App\Entity\Parts\PartCustomState; use App\Entity\PriceInformations\Orderdetail; use App\Form\AttachmentFormType; use App\Form\ParameterType; @@ -86,6 +87,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'attr' => [ 'placeholder' => 'part.edit.description.placeholder', 'rows' => 2, + 'data-ipn-suggestion' => 'descriptionField', ], ]) ->add('minAmount', SIUnitType::class, [ @@ -104,6 +106,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'disable_not_selectable' => true, //Do not require category for new parts, so that the user must select the category by hand and cannot forget it (the requirement is handled by the constraint in the entity) 'required' => !$new_part, + 'attr' => [ + 'data-ipn-suggestion' => 'categoryField', + ] ]) ->add('footprint', StructuralEntityType::class, [ 'class' => Footprint::class, @@ -171,10 +176,21 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'disable_not_selectable' => true, 'label' => 'part.edit.partUnit', ]) + ->add('partCustomState', StructuralEntityType::class, [ + 'class' => PartCustomState::class, + 'required' => false, + 'disable_not_selectable' => true, + 'label' => 'part.edit.partCustomState', + ]) ->add('ipn', TextType::class, [ 'required' => false, 'empty_data' => null, 'label' => 'part.edit.ipn', + 'attr' => [ + 'class' => 'ipn-suggestion-field', + 'data-elements--ipn-suggestion-target' => 'input', + 'autocomplete' => 'off', + ] ]); //Comment section diff --git a/src/Form/ProjectSystem/ProjectBOMEntryType.php b/src/Form/ProjectSystem/ProjectBOMEntryType.php index cac362fbb..44850c304 100644 --- a/src/Form/ProjectSystem/ProjectBOMEntryType.php +++ b/src/Form/ProjectSystem/ProjectBOMEntryType.php @@ -22,8 +22,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 +34,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void }); $builder - ->add('part', PartSelectType::class, [ + 'label' => 'project.bom.part', 'required' => false, ]) - ->add('name', TextType::class, [ 'label' => 'project.bom.name', 'required' => false, @@ -77,10 +74,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'label' => false, 'short' => true, - ]) - - ; - + ]); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Form/ProjectSystem/ProjectBuildType.php b/src/Form/ProjectSystem/ProjectBuildType.php index 2b7b52e28..b13dd12f2 100644 --- a/src/Form/ProjectSystem/ProjectBuildType.php +++ b/src/Form/ProjectSystem/ProjectBuildType.php @@ -82,36 +82,35 @@ public function buildForm(FormBuilderInterface $builder, array $options): void //The form is initially empty, we have to define the fields after we know the data $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { $form = $event->getForm(); - /** @var ProjectBuildRequest $build_request */ - $build_request = $event->getData(); + /** @var ProjectBuildRequest $projectBuildRequest */ + $projectBuildRequest = $event->getData(); $form->add('addBuildsToBuildsPart', CheckboxType::class, [ 'label' => 'project.build.add_builds_to_builds_part', 'required' => false, - 'disabled' => !$build_request->getProject()->getBuildPart() instanceof Part, + 'disabled' => !$projectBuildRequest->getProject()->getBuildPart() instanceof Part, ]); - if ($build_request->getProject()->getBuildPart() instanceof Part) { + if ($projectBuildRequest->getProject()->getBuildPart() instanceof Part) { $form->add('buildsPartLot', PartLotSelectType::class, [ 'label' => 'project.build.builds_part_lot', 'required' => false, - 'part' => $build_request->getProject()->getBuildPart(), + 'part' => $projectBuildRequest->getProject()->getBuildPart(), 'placeholder' => 'project.build.buildsPartLot.new_lot' ]); } - foreach ($build_request->getPartBomEntries() as $bomEntry) { + foreach ($projectBuildRequest->getPartBomEntries() as $bomEntry) { //Every part lot has a field to specify the number of parts to take from this lot - foreach ($build_request->getPartLotsForBOMEntry($bomEntry) as $lot) { + foreach ($projectBuildRequest->getPartLotsForBOMEntry($bomEntry) as $lot) { $form->add('lot_' . $lot->getID(), SIUnitType::class, [ 'label' => false, 'measurement_unit' => $bomEntry->getPart()->getPartUnit(), - 'max' => min($build_request->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()), + 'max' => min($projectBuildRequest->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()), 'disabled' => !$this->security->isGranted('withdraw', $lot), ]); } } - }); } diff --git a/src/Form/Type/AssemblySelectType.php b/src/Form/Type/AssemblySelectType.php new file mode 100644 index 000000000..10e858f26 --- /dev/null +++ b/src/Form/Type/AssemblySelectType.php @@ -0,0 +1,124 @@ +addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { + $form = $event->getForm(); + $config = $form->getConfig()->getOptions(); + $data = $event->getData() ?? []; + + $config['compound'] = false; + $config['choices'] = is_iterable($data) ? $data : [$data]; + $config['error_bubbling'] = true; + + $form->add('autocomplete', EntityType::class, $config); + }); + + //After form submit, we have to add the selected element as choice, otherwise the form will not accept this element + $builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) { + $data = $event->getData(); + $form = $event->getForm(); + $options = $form->get('autocomplete')->getConfig()->getOptions(); + + + if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) { + $options['choices'] = []; + } else { + //Extract the ID from the submitted data + $id = $data['autocomplete']; + //Find the element in the database + $element = $this->em->find($options['class'], $id); + + //Add the element as choice + $options['choices'] = [$element]; + $options['error_bubbling'] = true; + $form->add('autocomplete', EntityType::class, $options); + } + }); + + $builder->setDataMapper($this); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'class' => Assembly::class, + 'choice_label' => 'name', + 'compound' => true, + 'error_bubbling' => false, + ]); + + error_log($this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__'])); + + $resolver->setDefaults([ + 'attr' => [ + 'data-controller' => 'elements--assembly-select', + 'data-autocomplete' => $this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']), + 'autocomplete' => 'off', + ], + ]); + + $resolver->setDefaults([ + //Prefill the selected choice with the needed data, so the user can see it without an additional Ajax request + 'choice_attr' => ChoiceList::attr($this, function (?Assembly $assembly) { + if($assembly instanceof Assembly) { + //Determine the picture to show: + $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($assembly); + if ($preview_attachment instanceof Attachment) { + $preview_url = $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, + 'thumbnail_sm'); + } else { + $preview_url = ''; + } + } + + return $assembly instanceof Assembly ? [ + 'data-description' => $assembly->getDescription() ? mb_strimwidth($assembly->getDescription(), 0, 127, '...') : '', + 'data-category' => '', + 'data-footprint' => '', + 'data-image' => $preview_url, + ] : []; + }) + ]); + } + + public function mapDataToForms($data, \Traversable $forms): void + { + $form = current(iterator_to_array($forms, false)); + $form->setData($data); + } + + public function mapFormsToData(\Traversable $forms, &$data): void + { + $form = current(iterator_to_array($forms, false)); + $data = $form->getData(); + } + +} diff --git a/src/Form/Type/PartSelectType.php b/src/Form/Type/PartSelectType.php index 34b8fc7c4..c41d6b8f9 100644 --- a/src/Form/Type/PartSelectType.php +++ b/src/Form/Type/PartSelectType.php @@ -50,7 +50,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $options = $form->get('autocomplete')->getConfig()->getOptions(); - if (!isset($data['autocomplete']) || '' === $data['autocomplete']) { + if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) { $options['choices'] = []; } else { //Extract the ID from the submitted data @@ -84,7 +84,6 @@ public function configureOptions(OptionsResolver $resolver): void 'data-autocomplete' => $this->urlGenerator->generate('typeahead_parts', ['query' => '__QUERY__']), //Disable browser autocomplete 'autocomplete' => 'off', - ], ]); @@ -103,7 +102,7 @@ public function configureOptions(OptionsResolver $resolver): void } return $part instanceof Part ? [ - 'data-description' => mb_strimwidth($part->getDescription(), 0, 127, '...'), + 'data-description' => $part->getDescription() ? mb_strimwidth($part->getDescription(), 0, 127, '...') : '', 'data-category' => $part->getCategory() instanceof Category ? $part->getCategory()->getName() : '', 'data-footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '', 'data-image' => $preview_url, diff --git a/src/Form/Type/ProjectSelectType.php b/src/Form/Type/ProjectSelectType.php new file mode 100644 index 000000000..18a10c08c --- /dev/null +++ b/src/Form/Type/ProjectSelectType.php @@ -0,0 +1,124 @@ +addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { + $form = $event->getForm(); + $config = $form->getConfig()->getOptions(); + $data = $event->getData() ?? []; + + $config['compound'] = false; + $config['choices'] = is_iterable($data) ? $data : [$data]; + $config['error_bubbling'] = true; + + $form->add('autocomplete', EntityType::class, $config); + }); + + //After form submit, we have to add the selected element as choice, otherwise the form will not accept this element + $builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) { + $data = $event->getData(); + $form = $event->getForm(); + $options = $form->get('autocomplete')->getConfig()->getOptions(); + + + if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) { + $options['choices'] = []; + } else { + //Extract the ID from the submitted data + $id = $data['autocomplete']; + //Find the element in the database + $element = $this->em->find($options['class'], $id); + + //Add the element as choice + $options['choices'] = [$element]; + $options['error_bubbling'] = true; + $form->add('autocomplete', EntityType::class, $options); + } + }); + + $builder->setDataMapper($this); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'class' => Project::class, + 'choice_label' => 'name', + 'compound' => true, + 'error_bubbling' => false, + ]); + + error_log($this->urlGenerator->generate('typeahead_projects', ['query' => '__QUERY__'])); + + $resolver->setDefaults([ + 'attr' => [ + 'data-controller' => 'elements--project-select', + 'data-autocomplete' => $this->urlGenerator->generate('typeahead_projects', ['query' => '__QUERY__']), + 'autocomplete' => 'off', + ], + ]); + + $resolver->setDefaults([ + //Prefill the selected choice with the needed data, so the user can see it without an additional Ajax request + 'choice_attr' => ChoiceList::attr($this, function (?Project $project) { + if($project instanceof Project) { + //Determine the picture to show: + $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($project); + if ($preview_attachment instanceof Attachment) { + $preview_url = $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, + 'thumbnail_sm'); + } else { + $preview_url = ''; + } + } + + return $project instanceof Project ? [ + 'data-description' => $project->getDescription() ? mb_strimwidth($project->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/Helpers/Assemblies/AssemblyBuildRequest.php b/src/Helpers/Assemblies/AssemblyBuildRequest.php new file mode 100644 index 000000000..c33a6f612 --- /dev/null +++ b/src/Helpers/Assemblies/AssemblyBuildRequest.php @@ -0,0 +1,306 @@ +. + */ +namespace App\Helpers\Assemblies; + +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Validator\Constraints\AssemblySystem\ValidAssemblyBuildRequest; + +/** + * @see \App\Tests\Helpers\Assemblies\AssemblyBuildRequestTest + */ +#[ValidAssemblyBuildRequest] +final class AssemblyBuildRequest +{ + private readonly int $number_of_builds; + + /** + * @var array + */ + private array $withdraw_amounts = []; + + private string $comment = ''; + + private ?PartLot $builds_lot = null; + + private bool $add_build_to_builds_part = false; + + private bool $dont_check_quantity = false; + + /** + * @param Assembly $assembly The assembly that should be build + * @param int $number_of_builds The number of builds that should be created + */ + public function __construct(private readonly Assembly $assembly, int $number_of_builds) + { + if ($number_of_builds < 1) { + throw new \InvalidArgumentException('Number of builds must be at least 1!'); + } + $this->number_of_builds = $number_of_builds; + + $this->initializeArray(); + + //By default, use the first available lot of builds part if there is one. + if($assembly->getBuildPart() instanceof Part) { + $this->add_build_to_builds_part = true; + foreach( $assembly->getBuildPart()->getPartLots() as $lot) { + if (!$lot->isInstockUnknown()) { + $this->builds_lot = $lot; + break; + } + } + } + } + + private function initializeArray(): void + { + //Completely reset the array + $this->withdraw_amounts = []; + + //Now create an array for each BOM entry + foreach ($this->getPartBomEntries() as $bom_entry) { + $remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry); + foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) { + //If the lot has instock use it for the build + $this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount()); + $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]); + } + } + } + + /** + * Ensure that the assemblyBOMEntry belongs to the assembly, otherwise throw an exception. + */ + private function ensureBOMEntryValid(AssemblyBOMEntry $entry): void + { + if ($entry->getAssembly() !== $this->assembly) { + throw new \InvalidArgumentException('The given BOM entry does not belong to the assembly!'); + } + } + + /** + * Returns the partlot where the builds should be added to, or null if it should not be added to any lot. + */ + public function getBuildsPartLot(): ?PartLot + { + return $this->builds_lot; + } + + /** + * Return if the builds should be added to the builds part of this assembly as new stock + */ + public function getAddBuildsToBuildsPart(): bool + { + return $this->add_build_to_builds_part; + } + + /** + * Set if the builds should be added to the builds part of this assembly as new stock + * @return $this + */ + public function setAddBuildsToBuildsPart(bool $new_value): self + { + $this->add_build_to_builds_part = $new_value; + + if ($new_value === false) { + $this->builds_lot = null; + } + + return $this; + } + + /** + * Set the partlot where the builds should be added to, or null if it should not be added to any lot. + * The part lot must belong to the assembly build part, or an exception is thrown! + * @return $this + */ + public function setBuildsPartLot(?PartLot $new_part_lot): self + { + //Ensure that this new_part_lot belongs to the assembly + if (($new_part_lot instanceof PartLot && $new_part_lot->getPart() !== $this->assembly->getBuildPart()) || !$this->assembly->getBuildPart() instanceof Part) { + throw new \InvalidArgumentException('The given part lot does not belong to the assemblies build part!'); + } + + if ($new_part_lot instanceof PartLot) { + $this->setAddBuildsToBuildsPart(true); + } + + $this->builds_lot = $new_part_lot; + + return $this; + } + + /** + * Returns the comment where the user can write additional information about the build. + */ + public function getComment(): string + { + return $this->comment; + } + + /** + * Sets the comment where the user can write additional information about the build. + */ + public function setComment(string $comment): void + { + $this->comment = $comment; + } + + /** + * Returns the amount of parts that should be withdrawn from the given lot for the corresponding BOM entry. + * @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdrawal amount should be got + */ + public function getLotWithdrawAmount(PartLot|int $lot): float + { + $lot_id = $lot instanceof PartLot ? $lot->getID() : $lot; + + if (! array_key_exists($lot_id, $this->withdraw_amounts)) { + throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!'); + } + + return $this->withdraw_amounts[$lot_id]; + } + + /** + * Sets the amount of parts that should be withdrawn from the given lot for the corresponding BOM entry. + * @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdrawal amount should be got + * @return $this + */ + public function setLotWithdrawAmount(PartLot|int $lot, float $amount): self + { + if ($lot instanceof PartLot) { + $lot_id = $lot->getID(); + } elseif (is_int($lot)) { + $lot_id = $lot; + } else { + throw new \InvalidArgumentException('The given lot must be an instance of PartLot or an ID of a PartLot!'); + } + + $this->withdraw_amounts[$lot_id] = $amount; + + return $this; + } + + /** + * Returns the sum of all withdraw amounts for the given BOM entry. + */ + public function getWithdrawAmountSum(AssemblyBOMEntry $entry): float + { + $this->ensureBOMEntryValid($entry); + + $sum = 0; + foreach ($this->getPartLotsForBOMEntry($entry) as $lot) { + $sum += $this->getLotWithdrawAmount($lot); + } + + if ($entry->getPart() && !$entry->getPart()->useFloatAmount()) { + $sum = round($sum); + } + + return $sum; + } + + /** + * Returns the number of available lots to take stock from for the given BOM entry. + * @return PartLot[]|null Returns null if the entry is a non-part BOM entry + */ + public function getPartLotsForBOMEntry(AssemblyBOMEntry $assemblyBOMEntry): ?array + { + $this->ensureBOMEntryValid($assemblyBOMEntry); + + if (!$assemblyBOMEntry->getPart() instanceof Part) { + return null; + } + + //Filter out all lots which have unknown instock + return $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); + } + + /** + * Returns the needed amount of parts for the given BOM entry. + */ + public function getNeededAmountForBOMEntry(AssemblyBOMEntry $entry): float + { + $this->ensureBOMEntryValid($entry); + + return $entry->getQuantity() * $this->number_of_builds; + } + + /** + * Returns the list of all bom entries. + * @return AssemblyBOMEntry[] + */ + public function getBomEntries(): array + { + return $this->assembly->getBomEntries()->toArray(); + } + + /** + * Returns all part bom entries. + * @return AssemblyBOMEntry[] + */ + public function getPartBomEntries(): array + { + return $this->assembly->getBomEntries()->filter(fn(AssemblyBOMEntry $entry) => $entry->isPartBomEntry())->toArray(); + } + + /** + * Returns which assembly should be build + */ + public function getAssembly(): Assembly + { + return $this->assembly; + } + + /** + * Returns the number of builds that should be created. + */ + public function getNumberOfBuilds(): int + { + return $this->number_of_builds; + } + + /** + * If Set to true, the given withdraw amounts are used without any checks for requirements. + * @return bool + */ + public function isDontCheckQuantity(): bool + { + return $this->dont_check_quantity; + } + + /** + * Set to true, the given withdraw amounts are used without any checks for requirements. + * @param bool $dont_check_quantity + * @return $this + */ + public function setDontCheckQuantity(bool $dont_check_quantity): AssemblyBuildRequest + { + $this->dont_check_quantity = $dont_check_quantity; + return $this; + } + + +} diff --git a/src/Helpers/Assemblies/AssemblyPartAggregator.php b/src/Helpers/Assemblies/AssemblyPartAggregator.php new file mode 100644 index 000000000..2346075a1 --- /dev/null +++ b/src/Helpers/Assemblies/AssemblyPartAggregator.php @@ -0,0 +1,267 @@ +. + */ +namespace App\Helpers\Assemblies; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Parts\Part; +use Dompdf\Dompdf; +use Dompdf\Options; +use Twig\Environment; + +class AssemblyPartAggregator +{ + public function __construct(private readonly Environment $twig) + { + } + + /** + * Aggregate the required parts and their total quantities for an assembly. + * + * @param Assembly $assembly The assembly to process. + * @param float $multiplier The quantity multiplier from the parent assembly. + * @return array Array of parts with their aggregated quantities, keyed by Part ID. + */ + public function getAggregatedParts(Assembly $assembly, float $multiplier): array + { + $aggregatedParts = []; + + // Start processing the assembly recursively + $this->processAssembly($assembly, $multiplier, $aggregatedParts); + + // Return the final aggregated list of parts + return $aggregatedParts; + } + + /** + * Recursive helper to process an assembly and all its BOM entries. + * + * @param Assembly $assembly The current assembly to process. + * @param float $multiplier The quantity multiplier from the parent assembly. + * @param array &$aggregatedParts The array to accumulate parts and their quantities. + */ + private function processAssembly(Assembly $assembly, float $multiplier, array &$aggregatedParts): void + { + foreach ($assembly->getBomEntries() as $bomEntry) { + // If the BOM entry refers to a part, add its quantity + if ($bomEntry->getPart() instanceof Part) { + $part = $bomEntry->getPart(); + + if (!isset($aggregatedParts[$part->getId()])) { + $aggregatedParts[$part->getId()] = [ + 'part' => $part, + 'assembly' => $assembly, + 'quantity' => $bomEntry->getQuantity(), + 'multiplier' => $multiplier, + ]; + } + } elseif ($bomEntry->getReferencedAssembly() instanceof Assembly) { + // If the BOM entry refers to another assembly, process it recursively + $this->processAssembly($bomEntry->getReferencedAssembly(), $bomEntry->getQuantity(), $aggregatedParts); + } else { + $aggregatedParts[] = [ + 'part' => null, + 'assembly' => $assembly, + 'quantity' => $bomEntry->getQuantity(), + 'multiplier' => $multiplier, + ]; + } + } + } + + /** + * Exports a hierarchical Bill of Materials (BOM) for assemblies and parts in a readable format, + * including the multiplier for each part and assembly. + * + * @param Assembly $assembly The root assembly to export. + * @param string $indentationSymbol The symbol used for indentation (e.g., ' '). + * @param int $initialDepth The starting depth for formatting (default: 0). + * @return string Human-readable hierarchical BOM list. + */ + public function exportReadableHierarchy(Assembly $assembly, string $indentationSymbol = ' ', int $initialDepth = 0): string + { + // Start building the hierarchy + $output = ''; + $this->processAssemblyHierarchy($assembly, $initialDepth, 1, $indentationSymbol, $output); + + return $output; + } + + public function exportReadableHierarchyForPdf(array $assemblyHierarchies): string + { + $html = $this->twig->render('assemblies/export_bom_pdf.html.twig', [ + 'assemblies' => $assemblyHierarchies, + ]); + + $options = new Options(); + $options->set('isHtml5ParserEnabled', true); + $options->set('isPhpEnabled', true); + + $dompdf = new Dompdf($options); + $dompdf->loadHtml($html); + $dompdf->setPaper('A4'); + $dompdf->render(); + + $canvas = $dompdf->getCanvas(); + $font = $dompdf->getFontMetrics()->getFont('Arial', 'normal'); + + return $dompdf->output(); + } + + /** + * Recursive method to process assemblies and their parts. + * + * @param Assembly $assembly The current assembly to process. + * @param int $depth The current depth in the hierarchy. + * @param float $parentMultiplier The multiplier inherited from the parent (default is 1 for root). + * @param string $indentationSymbol The symbol used for indentation. + * @param string &$output The cumulative output string. + */ + private function processAssemblyHierarchy(Assembly $assembly, int $depth, float $parentMultiplier, string $indentationSymbol, string &$output): void + { + // Add the current assembly to the output + if ($depth === 0) { + $output .= sprintf( + "%sAssembly: %s [IPN: %s]\n\n", + str_repeat($indentationSymbol, $depth), + $assembly->getName(), + $assembly->getIpn(), + ); + } else { + $output .= sprintf( + "%sAssembly: %s [IPN: %s, Multiplier: %.2f]\n\n", + str_repeat($indentationSymbol, $depth), + $assembly->getName(), + $assembly->getIpn(), + $parentMultiplier + ); + } + + // Gruppiere BOM-Einträge in Kategorien + $parts = []; + $referencedAssemblies = []; + $others = []; + + foreach ($assembly->getBomEntries() as $bomEntry) { + if ($bomEntry->getPart() instanceof Part) { + $parts[] = $bomEntry; + } elseif ($bomEntry->getReferencedAssembly() instanceof Assembly) { + $referencedAssemblies[] = $bomEntry; + } else { + $others[] = $bomEntry; + } + } + + if (!empty($parts)) { + // Process each BOM entry for the current assembly + foreach ($parts as $bomEntry) { + $effectiveQuantity = $bomEntry->getQuantity() * $parentMultiplier; + + $output .= sprintf( + "%sPart: %s [IPN: %s, MPNR: %s, Quantity: %.2f%s, EffectiveQuantity: %.2f]\n", + str_repeat($indentationSymbol, $depth + 1), + $bomEntry->getPart()?->getName(), + $bomEntry->getPart()?->getIpn() ?? '-', + $bomEntry->getPart()?->getManufacturerProductNumber() ?? '-', + $bomEntry->getQuantity(), + $parentMultiplier > 1 ? sprintf(", Multiplier: %.2f", $parentMultiplier) : '', + $effectiveQuantity, + ); + } + + $output .= "\n"; + } + + foreach ($referencedAssemblies as $bomEntry) { + // Add referenced assembly details + $referencedQuantity = $bomEntry->getQuantity() * $parentMultiplier; + + $output .= sprintf( + "%sReferenced Assembly: %s [IPN: %s, Quantity: %.2f%s, EffectiveQuantity: %.2f]\n", + str_repeat($indentationSymbol, $depth + 1), + $bomEntry->getReferencedAssembly()->getName(), + $bomEntry->getReferencedAssembly()->getIpn() ?? '-', + $bomEntry->getQuantity(), + $parentMultiplier > 1 ? sprintf(", Multiplier: %.2f", $parentMultiplier) : '', + $referencedQuantity, + ); + + // Recurse into the referenced assembly + $this->processAssemblyHierarchy( + $bomEntry->getReferencedAssembly(), + $depth + 2, // Increase depth for nested assemblies + $referencedQuantity, // Pass the calculated multiplier + $indentationSymbol, + $output + ); + } + + foreach ($others as $bomEntry) { + $output .= sprintf( + "%sOther: %s [Quantity: %.2f, Multiplier: %.2f]\n", + str_repeat($indentationSymbol, $depth + 1), + $bomEntry->getName(), + $bomEntry->getQuantity(), + $parentMultiplier, + ); + } + } + + public function processAssemblyHierarchyForPdf(Assembly $assembly, int $depth, float $quantity, float $parentMultiplier): array + { + $result = [ + 'name' => $assembly->getName(), + 'ipn' => $assembly->getIpn(), + 'quantity' => $quantity, + 'multiplier' => $depth === 0 ? null : $parentMultiplier, + 'parts' => [], + 'referencedAssemblies' => [], + 'others' => [], + ]; + + foreach ($assembly->getBomEntries() as $bomEntry) { + if ($bomEntry->getPart() instanceof Part) { + $result['parts'][] = [ + 'name' => $bomEntry->getPart()->getName(), + 'ipn' => $bomEntry->getPart()->getIpn(), + 'quantity' => $bomEntry->getQuantity(), + 'effectiveQuantity' => $bomEntry->getQuantity() * $parentMultiplier, + ]; + } elseif ($bomEntry->getReferencedAssembly() instanceof Assembly) { + $result['referencedAssemblies'][] = $this->processAssemblyHierarchyForPdf( + $bomEntry->getReferencedAssembly(), + $depth + 1, + $bomEntry->getQuantity(), + $parentMultiplier * $bomEntry->getQuantity() + ); + } else { + $result['others'][] = [ + 'name' => $bomEntry->getName(), + 'quantity' => $bomEntry->getQuantity(), + 'multiplier' => $parentMultiplier, + ]; + } + } + + return $result; + } +} diff --git a/src/Helpers/Projects/ProjectBuildRequest.php b/src/Helpers/Projects/ProjectBuildRequest.php index 430d37b56..24bb5eb78 100644 --- a/src/Helpers/Projects/ProjectBuildRequest.php +++ b/src/Helpers/Projects/ProjectBuildRequest.php @@ -301,6 +301,4 @@ public function setDontCheckQuantity(bool $dont_check_quantity): ProjectBuildReq $this->dont_check_quantity = $dont_check_quantity; return $this; } - - -} +} \ No newline at end of file diff --git a/src/Repository/AssemblyRepository.php b/src/Repository/AssemblyRepository.php new file mode 100644 index 000000000..031e6e82b --- /dev/null +++ b/src/Repository/AssemblyRepository.php @@ -0,0 +1,69 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace App\Repository; + +use App\Entity\AssemblySystem\Assembly; + +/** + * @template TEntityClass of Assembly + * @extends DBElementRepository + */ +class AssemblyRepository extends StructuralDBElementRepository +{ + /** + * @return Assembly[] + */ + public function autocompleteSearch(string $query, int $max_limits = 50): array + { + $qb = $this->createQueryBuilder('assembly'); + $qb->select('assembly') + ->where('ILIKE(assembly.name, :query) = TRUE') + ->orWhere('ILIKE(assembly.description, :query) = TRUE'); + + $qb->setParameter('query', '%'.$query.'%'); + + $qb->setMaxResults($max_limits); + $qb->orderBy('NATSORT(assembly.name)', 'ASC'); + + return $qb->getQuery()->getResult(); + } +} \ No newline at end of file diff --git a/src/Repository/DBElementRepository.php b/src/Repository/DBElementRepository.php index 2437e8488..23ad296aa 100644 --- a/src/Repository/DBElementRepository.php +++ b/src/Repository/DBElementRepository.php @@ -154,4 +154,14 @@ protected function setField(AbstractDBElement $element, string $field, int $new_ $property->setAccessible(true); $property->setValue($element, $new_value); } + + protected function save(AbstractDBElement $entity, bool $flush = true): void + { + $manager = $this->getEntityManager(); + $manager->persist($entity); + + if ($flush) { + $manager->flush(); + } + } } diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index edccd74ba..693615533 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -22,17 +22,31 @@ namespace App\Repository; +use App\Entity\Parts\Category; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; +use Symfony\Contracts\Translation\TranslatorInterface; +use Doctrine\ORM\EntityManagerInterface; /** * @extends NamedDBElementRepository */ class PartRepository extends NamedDBElementRepository { + private TranslatorInterface $translator; + + public function __construct( + EntityManagerInterface $em, + TranslatorInterface $translator + ) { + parent::__construct($em, $em->getClassMetadata(Part::class)); + + $this->translator = $translator; + } + /** * Gets the summed up instock of all parts (only parts without a measurement unit). * @@ -94,4 +108,237 @@ public function autocompleteSearch(string $query, int $max_limits = 50): array return $qb->getQuery()->getResult(); } + + /** + * Provides IPN (Internal Part Number) suggestions for a given part based on its category, description, + * and configured autocomplete digit length. + * + * This function generates suggestions for common prefixes and incremented prefixes based on + * the part's current category and its hierarchy. If the part is unsaved, a default "n.a." prefix is returned. + * + * @param Part $part The part for which autocomplete suggestions are generated. + * @param string $description Base64-encoded description to assist in generating suggestions. + * @param int $autocompletePartDigits The number of digits used in autocomplete increments. + * + * @return array An associative array containing the following keys: + * - 'commonPrefixes': List of common prefixes found for the part. + * - 'prefixesPartIncrement': Increments for the generated prefixes, including hierarchical prefixes. + */ + public function autoCompleteIpn(Part $part, string $description, int $autocompletePartDigits): array + { + $category = $part->getCategory(); + $ipnSuggestions = ['commonPrefixes' => [], 'prefixesPartIncrement' => []]; + $description = base64_decode($description); + + if (strlen($description) > 150) { + $description = substr($description, 0, 150); + } + + // Validate the category and ensure it's an instance of Category + if ($category instanceof Category) { + $currentPath = $category->getPartIpnPrefix(); + $directIpnPrefixEmpty = $category->getPartIpnPrefix() === ''; + $currentPath = $currentPath === '' ? 'n.a.' : $currentPath; + + $increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $autocompletePartDigits); + + $ipnSuggestions['commonPrefixes'][] = [ + 'title' => $currentPath . '-', + 'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category') + ]; + + $suggestionByDescription = $this->getIpnSuggestByDescription($description); + + if ($suggestionByDescription !== null && $suggestionByDescription !== $part->getIpn() && $part->getIpn() !== null && $part->getIpn() !== '') { + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $part->getIpn(), + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.description.current-increment') + ]; + } + + if ($suggestionByDescription !== null) { + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $suggestionByDescription, + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.description.increment') + ]; + } + + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $currentPath . '-' . $increment, + 'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category.increment') + ]; + + // Process parent categories + $parentCategory = $category->getParent(); + + while ($parentCategory instanceof Category) { + // Prepend the parent category's prefix to the current path + $currentPath = $parentCategory->getPartIpnPrefix() . '-' . $currentPath; + $currentPath = $parentCategory->getPartIpnPrefix() === '' ? 'n.a.-' . $currentPath : $currentPath; + + $ipnSuggestions['commonPrefixes'][] = [ + 'title' => $currentPath . '-', + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment') + ]; + + $increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $autocompletePartDigits); + + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $currentPath . '-' . $increment, + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.hierarchical.increment') + ]; + + // Move to the next parent category + $parentCategory = $parentCategory->getParent(); + } + } elseif ($part->getID() === null) { + $ipnSuggestions['commonPrefixes'][] = [ + 'title' => 'n.a.', + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.not_saved') + ]; + } + + return $ipnSuggestions; + } + + /** + * Suggests the next IPN (Internal Part Number) based on the provided part description. + * + * Searches for parts with similar descriptions and retrieves their existing IPNs to calculate the next suggestion. + * Returns null if the description is empty or no suggestion can be generated. + * + * @param string $description The part description to search for. + * + * @return string|null The suggested IPN, or null if no suggestion is possible. + * + * @throws NonUniqueResultException + */ + public function getIpnSuggestByDescription(string $description): ?string + { + if ($description === '') { + return null; + } + + $qb = $this->createQueryBuilder('part'); + + $qb->select('part') + ->where('part.description LIKE :descriptionPattern') + ->setParameter('descriptionPattern', $description.'%') + ->orderBy('part.id', 'ASC'); + + $partsBySameDescription = $qb->getQuery()->getResult(); + $givenIpnsWithSameDescription = []; + + foreach ($partsBySameDescription as $part) { + if ($part->getIpn() === null || $part->getIpn() === '') { + continue; + } + + $givenIpnsWithSameDescription[] = $part->getIpn(); + } + + return $this->getNextIpnSuggestion($givenIpnsWithSameDescription); + } + + /** + * Generates the next possible increment for a part within a given category, while ensuring uniqueness. + * + * This method calculates the next available increment for a part's identifier (`ipn`) based on the current path + * and the number of digits specified for the autocomplete feature. It ensures that the generated identifier + * aligns with the expected length and does not conflict with already existing identifiers in the same category. + * + * @param string $currentPath The base path or prefix for the part's identifier. + * @param Part $currentPart The part entity for which the increment is being generated. + * @param int $autocompletePartDigits The number of digits reserved for the increment. + * + * @return string|null The next possible increment as a zero-padded string, or null if it cannot be generated. + * + * @throws NonUniqueResultException If the query returns non-unique results. + * @throws NoResultException If the query fails to return a result. + */ + private function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $autocompletePartDigits): ?string + { + $qb = $this->createQueryBuilder('part'); + + $expectedLength = strlen($currentPath) + 1 + $autocompletePartDigits; // Path + '-' + $autocompletePartDigits digits + + // Fetch all parts in the given category, sorted by their ID in ascending order + $qb->select('part') + ->where('part.ipn LIKE :ipnPattern') + ->andWhere('LENGTH(part.ipn) = :expectedLength') + ->setParameter('ipnPattern', $currentPath . '%') + ->setParameter('expectedLength', $expectedLength) + ->orderBy('part.id', 'ASC'); + + $parts = $qb->getQuery()->getResult(); + + // Collect all used increments in the category + $usedIncrements = []; + foreach ($parts as $part) { + if ($part->getIpn() === null || $part->getIpn() === '') { + continue; + } + + if ($part->getId() === $currentPart->getId()) { + // Extract and return the current part's increment directly + $incrementPart = substr($part->getIpn(), -$autocompletePartDigits); + if (is_numeric($incrementPart)) { + return str_pad((string) $incrementPart, $autocompletePartDigits, '0', STR_PAD_LEFT); + } + } + + // Extract last $autocompletePartDigits digits for possible available part increment + $incrementPart = substr($part->getIpn(), -$autocompletePartDigits); + if (is_numeric($incrementPart)) { + $usedIncrements[] = (int) $incrementPart; + } + + } + + // Generate the next free $autocompletePartDigits-digit increment + $nextIncrement = 1; // Start at the beginning + + while (in_array($nextIncrement, $usedIncrements)) { + $nextIncrement++; + } + + return str_pad((string) $nextIncrement, $autocompletePartDigits, '0', STR_PAD_LEFT); + } + + /** + * Generates the next IPN suggestion based on the maximum numeric suffix found in the given IPNs. + * + * The new IPN is constructed using the base format of the first provided IPN, + * incremented by the next free numeric suffix. If no base IPNs are found, + * returns null. + * + * @param array $givenIpns List of IPNs to analyze. + * + * @return string|null The next suggested IPN, or null if no base IPNs can be derived. + */ + private function getNextIpnSuggestion(array $givenIpns): ?string { + $maxSuffix = 0; + + foreach ($givenIpns as $ipn) { + // Check whether the IPN contains a suffix "_ " + if (preg_match('/_(\d+)$/', $ipn, $matches)) { + $suffix = (int)$matches[1]; + if ($suffix > $maxSuffix) { + $maxSuffix = $suffix; // Höchste Nummer speichern + } + } + } + + // Find the basic format (the IPN without suffix) from the first IPN + $baseIpn = $givenIpns[0] ?? ''; + $baseIpn = preg_replace('/_\d+$/', '', $baseIpn); // Entferne vorhandene "_" + + if ($baseIpn === '') { + return null; + } + + // Generate next free possible IPN + return $baseIpn . '_' . ($maxSuffix + 1); + } + } diff --git a/src/Repository/Parts/DeviceRepository.php b/src/Repository/Parts/DeviceRepository.php index 442c91e58..c714523af 100644 --- a/src/Repository/Parts/DeviceRepository.php +++ b/src/Repository/Parts/DeviceRepository.php @@ -51,4 +51,22 @@ public function getPartsCount(object $element): int //Prevent user from deleting devices, to not accidentally remove filled devices from old versions return 1; } + + /** + * @return Project[] + */ + public function autocompleteSearch(string $query, int $max_limits = 50): array + { + $qb = $this->createQueryBuilder('project'); + $qb->select('project') + ->where('ILIKE(project.name, :query) = TRUE') + ->orWhere('ILIKE(project.description, :query) = TRUE'); + + $qb->setParameter('query', '%'.$query.'%'); + + $qb->setMaxResults($max_limits); + $qb->orderBy('NATSORT(project.name)', 'ASC'); + + return $qb->getQuery()->getResult(); + } } diff --git a/src/Repository/Parts/PartCustomStateRepository.php b/src/Repository/Parts/PartCustomStateRepository.php new file mode 100644 index 000000000..d66221a24 --- /dev/null +++ b/src/Repository/Parts/PartCustomStateRepository.php @@ -0,0 +1,48 @@ +. + */ +namespace App\Repository\Parts; + +use App\Entity\Parts\PartCustomState; +use App\Repository\AbstractPartsContainingRepository; +use InvalidArgumentException; + +class PartCustomStateRepository extends AbstractPartsContainingRepository +{ + public function getParts(object $element, string $nameOrderDirection = "ASC"): array + { + if (!$element instanceof PartCustomState) { + throw new InvalidArgumentException('$element must be an PartCustomState!'); + } + + return $this->getPartsByField($element, $nameOrderDirection, 'partUnit'); + } + + public function getPartsCount(object $element): int + { + if (!$element instanceof PartCustomState) { + throw new InvalidArgumentException('$element must be an PartCustomState!'); + } + + return $this->getPartsCountByField($element, 'partUnit'); + } +} diff --git a/src/Security/Voter/AttachmentVoter.php b/src/Security/Voter/AttachmentVoter.php index bd7ae4df8..c233a236f 100644 --- a/src/Security/Voter/AttachmentVoter.php +++ b/src/Security/Voter/AttachmentVoter.php @@ -22,6 +22,8 @@ namespace App\Security\Voter; +use App\Entity\Attachments\AssemblyAttachment; +use App\Entity\Attachments\PartCustomStateAttachment; use App\Services\UserSystem\VoterHelper; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Attachments\AttachmentContainingDBElement; @@ -89,6 +91,8 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $ $param = 'currencies'; } elseif (is_a($subject, ProjectAttachment::class, true)) { $param = 'projects'; + } elseif (is_a($subject, AssemblyAttachment::class, true)) { + $param = 'assemblies'; } elseif (is_a($subject, FootprintAttachment::class, true)) { $param = 'footprints'; } elseif (is_a($subject, GroupAttachment::class, true)) { @@ -99,6 +103,8 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $ $param = 'measurement_units'; } elseif (is_a($subject, PartAttachment::class, true)) { $param = 'parts'; + } elseif (is_a($subject, PartCustomStateAttachment::class, true)) { + $param = 'part_custom_states'; } elseif (is_a($subject, StorageLocationAttachment::class, true)) { $param = 'storelocations'; } elseif (is_a($subject, SupplierAttachment::class, true)) { diff --git a/src/Security/Voter/ParameterVoter.php b/src/Security/Voter/ParameterVoter.php index f59bdeaf5..5dc30ea25 100644 --- a/src/Security/Voter/ParameterVoter.php +++ b/src/Security/Voter/ParameterVoter.php @@ -22,6 +22,7 @@ */ namespace App\Security\Voter; +use App\Entity\Parameters\PartCustomStateParameter; use App\Services\UserSystem\VoterHelper; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractDBElement; @@ -97,6 +98,8 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $ $param = 'measurement_units'; } elseif (is_a($subject, PartParameter::class, true)) { $param = 'parts'; + } elseif (is_a($subject, PartCustomStateParameter::class, true)) { + $param = 'part_custom_states'; } elseif (is_a($subject, StorageLocationParameter::class, true)) { $param = 'storelocations'; } elseif (is_a($subject, SupplierParameter::class, true)) { diff --git a/src/Security/Voter/StructureVoter.php b/src/Security/Voter/StructureVoter.php index ad0299a79..cb05ffdd5 100644 --- a/src/Security/Voter/StructureVoter.php +++ b/src/Security/Voter/StructureVoter.php @@ -22,7 +22,9 @@ namespace App\Security\Voter; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\AttachmentType; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -47,12 +49,14 @@ 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', Supplier::class => 'suppliers', Currency::class => 'currencies', MeasurementUnit::class => 'measurement_units', + PartCustomState::class => 'part_custom_states', ]; public function __construct(private readonly VoterHelper $helper) diff --git a/src/Services/AssemblySystem/AssemblyBuildHelper.php b/src/Services/AssemblySystem/AssemblyBuildHelper.php new file mode 100644 index 000000000..9180f3e80 --- /dev/null +++ b/src/Services/AssemblySystem/AssemblyBuildHelper.php @@ -0,0 +1,166 @@ +. + */ +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; + /** @var AssemblyBOMEntry $bom_entry */ + foreach ($assembly->getBomEntries() as $bom_entry) { + //Skip BOM entries without a part (as we can not determine that) + if (!$bom_entry->isPartBomEntry() && !$bom_entry->isAssemblyBomEntry()) { + continue; + } + + //The maximum buildable count for the whole assembly is the minimum of all BOM entries + if ($bom_entry->getPart() !== null) { + $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry)); + } elseif ($bom_entry->getReferencedAssembly() !== null) { + $maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCount($bom_entry->getReferencedAssembly())); + } + } + + 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 referenced assembly BOM entries for which parts are missing in the stock for the given number of builds + * @param Assembly $assembly The assembly for which the BOM entries should be checked + * @param int $number_of_builds How often should the assembly be build? + * @return AssemblyBOMEntry[] + */ + 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!'); + } + + $nonBuildableEntries = []; + + /** @var AssemblyBOMEntry $bomEntry */ + foreach ($assembly->getBomEntries() as $bomEntry) { + $part = $bomEntry->getPart(); + + //Skip BOM entries without a part (as we can not determine that) + if (!$part instanceof Part && $bomEntry->getReferencedAssembly() === null) { + continue; + } + + if ($bomEntry->getPart() !== null) { + $amount_sum = $part->getAmountSum(); + + if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) { + $nonBuildableEntries[] = $bomEntry; + } + } elseif ($bomEntry->getReferencedAssembly() !== null) { + $nonBuildableAssemblyEntries = $this->getNonBuildableAssemblyBomEntries($bomEntry->getReferencedAssembly(), $number_of_builds); + $nonBuildableEntries = array_merge($nonBuildableEntries, $nonBuildableAssemblyEntries); + } + } + + return $nonBuildableEntries; + } + + /** + * Withdraw the parts from the stock using the given AssemblyBuildRequest and create the build parts entries, if needed. + * The AssemblyBuildRequest has to be validated before!! + * You have to flush changes to DB afterward + */ + public function doBuild(AssemblyBuildRequest $buildRequest): void + { + $message = $buildRequest->getComment(); + $message .= ' (Assembly build: '.$buildRequest->getAssembly()->getName().')'; + + foreach ($buildRequest->getPartBomEntries() as $bom_entry) { + foreach ($buildRequest->getPartLotsForBOMEntry($bom_entry) as $part_lot) { + $amount = $buildRequest->getLotWithdrawAmount($part_lot); + if ($amount > 0) { + $this->withdraw_add_helper->withdraw($part_lot, $amount, $message); + } + } + } + + if ($buildRequest->getAddBuildsToBuildsPart()) { + $this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message); + } + } +} diff --git a/src/Services/AssemblySystem/AssemblyBuildPartHelper.php b/src/Services/AssemblySystem/AssemblyBuildPartHelper.php new file mode 100644 index 000000000..9a5503505 --- /dev/null +++ b/src/Services/AssemblySystem/AssemblyBuildPartHelper.php @@ -0,0 +1,40 @@ +setBuiltAssembly($assembly); + + //Set the name of the part to the name of the assembly + $part->setName($assembly->getName()); + + //Set the description of the part to the description of the assembly + $part->setDescription($assembly->getDescription()); + + //Add a tag to the part that indicates that it is a build part + $part->setTags('assembly-build'); + + //Associate the part with the assembly + $assembly->setBuildPart($part); + + return $part; + } +} diff --git a/src/Services/Attachments/AssemblyPreviewGenerator.php b/src/Services/Attachments/AssemblyPreviewGenerator.php new file mode 100644 index 000000000..9ecbbd070 --- /dev/null +++ b/src/Services/Attachments/AssemblyPreviewGenerator.php @@ -0,0 +1,93 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\Attachments; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\Attachment; + +class AssemblyPreviewGenerator +{ + public function __construct(protected AttachmentManager $attachmentHelper) + { + } + + /** + * Returns a list of attachments that can be used for previewing the assembly ordered by priority. + * + * @param Assembly $assembly the assembly for which the attachments should be determined + * + * @return (Attachment|null)[] + * + * @psalm-return list + */ + public function getPreviewAttachments(Assembly $assembly): array + { + $list = []; + + //Master attachment has top priority + $attachment = $assembly->getMasterPictureAttachment(); + if ($this->isAttachmentValidPicture($attachment)) { + $list[] = $attachment; + } + + //Then comes the other images of the assembly + foreach ($assembly->getAttachments() as $attachment) { + //Dont show the master attachment twice + if ($this->isAttachmentValidPicture($attachment) && $attachment !== $assembly->getMasterPictureAttachment()) { + $list[] = $attachment; + } + } + + return $list; + } + + /** + * Determines what attachment should be used for previewing a assembly (especially in assembly table). + * The returned attachment is guaranteed to be existing and be a picture. + * + * @param Assembly $assembly The assembly for which the attachment should be determined + */ + public function getTablePreviewAttachment(Assembly $assembly): ?Attachment + { + $attachment = $assembly->getMasterPictureAttachment(); + if ($this->isAttachmentValidPicture($attachment)) { + return $attachment; + } + + return null; + } + + /** + * Checks if a attachment is exising and a valid picture. + * + * @param Attachment|null $attachment the attachment that should be checked + * + * @return bool true if the attachment is valid + */ + protected function isAttachmentValidPicture(?Attachment $attachment): bool + { + return $attachment instanceof Attachment + && $attachment->isPicture() + && $this->attachmentHelper->isFileExisting($attachment); + } +} diff --git a/src/Services/Attachments/AttachmentSubmitHandler.php b/src/Services/Attachments/AttachmentSubmitHandler.php index a30163ae1..81b998bec 100644 --- a/src/Services/Attachments/AttachmentSubmitHandler.php +++ b/src/Services/Attachments/AttachmentSubmitHandler.php @@ -22,6 +22,7 @@ namespace App\Services\Attachments; +use App\Entity\Attachments\AssemblyAttachment; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentType; @@ -81,6 +82,7 @@ public function __construct( CategoryAttachment::class => 'category', CurrencyAttachment::class => 'currency', ProjectAttachment::class => 'project', + AssemblyAttachment::class => 'assembly', FootprintAttachment::class => 'footprint', GroupAttachment::class => 'group', ManufacturerAttachment::class => 'manufacturer', diff --git a/src/Services/Attachments/PartPreviewGenerator.php b/src/Services/Attachments/PartPreviewGenerator.php index ba6e5db0d..ef13265f1 100644 --- a/src/Services/Attachments/PartPreviewGenerator.php +++ b/src/Services/Attachments/PartPreviewGenerator.php @@ -23,6 +23,7 @@ namespace App\Services\Attachments; use App\Entity\Parts\Footprint; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\StorageLocation; @@ -103,6 +104,13 @@ public function getPreviewAttachments(Part $part): array } } + if ($part->getPartCustomState() instanceof PartCustomState) { + $attachment = $part->getPartCustomState()->getMasterPictureAttachment(); + if ($this->isAttachmentValidPicture($attachment)) { + $list[] = $attachment; + } + } + if ($part->getManufacturer() instanceof Manufacturer) { $attachment = $part->getManufacturer()->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { diff --git a/src/Services/Attachments/ProjectPreviewGenerator.php b/src/Services/Attachments/ProjectPreviewGenerator.php new file mode 100644 index 000000000..9929dbd3c --- /dev/null +++ b/src/Services/Attachments/ProjectPreviewGenerator.php @@ -0,0 +1,93 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\Attachments; + +use App\Entity\Attachments\Attachment; +use App\Entity\ProjectSystem\Project; + +class ProjectPreviewGenerator +{ + public function __construct(protected AttachmentManager $attachmentHelper) + { + } + + /** + * Returns a list of attachments that can be used for previewing the project ordered by priority. + * + * @param Project $project the project for which the attachments should be determined + * + * @return (Attachment|null)[] + * + * @psalm-return list + */ + public function getPreviewAttachments(Project $project): array + { + $list = []; + + //Master attachment has top priority + $attachment = $project->getMasterPictureAttachment(); + if ($this->isAttachmentValidPicture($attachment)) { + $list[] = $attachment; + } + + //Then comes the other images of the project + foreach ($project->getAttachments() as $attachment) { + //Dont show the master attachment twice + if ($this->isAttachmentValidPicture($attachment) && $attachment !== $project->getMasterPictureAttachment()) { + $list[] = $attachment; + } + } + + return $list; + } + + /** + * Determines what attachment should be used for previewing a project (especially in project table). + * The returned attachment is guaranteed to be existing and be a picture. + * + * @param Project $project The project for which the attachment should be determined + */ + public function getTablePreviewAttachment(Project $project): ?Attachment + { + $attachment = $project->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/EDA/KiCadHelper.php b/src/Services/EDA/KiCadHelper.php index 75c2cc346..4b7c5e5a5 100644 --- a/src/Services/EDA/KiCadHelper.php +++ b/src/Services/EDA/KiCadHelper.php @@ -233,6 +233,10 @@ public function getKiCADPart(Part $part): array } $result["fields"]["Part-DB Unit"] = $this->createField($unit); } + if ($part->getPartCustomState() !== null) { + $customState = $part->getPartCustomState()->getName(); + $result["fields"]["Part-DB Custom state"] = $this->createField($customState); + } if ($part->getMass()) { $result["fields"]["Mass"] = $this->createField($part->getMass() . ' g'); } diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php index 142471457..b2d473f25 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -22,12 +22,15 @@ namespace App\Services; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\NamedElementInterface; use App\Entity\Parts\PartAssociation; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parameters\AbstractParameter; @@ -64,6 +67,8 @@ public function __construct(protected TranslatorInterface $translator, private r AttachmentType::class => $this->translator->trans('attachment_type.label'), Project::class => $this->translator->trans('project.label'), ProjectBOMEntry::class => $this->translator->trans('project_bom_entry.label'), + Assembly::class => $this->translator->trans('assembly.label'), + AssemblyBOMEntry::class => $this->translator->trans('assembly_bom_entry.label'), Footprint::class => $this->translator->trans('footprint.label'), Manufacturer::class => $this->translator->trans('manufacturer.label'), MeasurementUnit::class => $this->translator->trans('measurement_unit.label'), @@ -79,6 +84,7 @@ public function __construct(protected TranslatorInterface $translator, private r AbstractParameter::class => $this->translator->trans('parameter.label'), LabelProfile::class => $this->translator->trans('label_profile.label'), PartAssociation::class => $this->translator->trans('part_association.label'), + PartCustomState::class => $this->translator->trans('part_custom_state.label'), ]; } @@ -178,6 +184,8 @@ public function formatLabelHTMLForEntity(AbstractDBElement $entity, bool $includ $on = $entity->getOrderdetail()->getPart(); } elseif ($entity instanceof ProjectBOMEntry && $entity->getProject() instanceof Project) { $on = $entity->getProject(); + } elseif ($entity instanceof AssemblyBOMEntry && $entity->getAssembly() instanceof Assembly) { + $on = $entity->getAssembly(); } if (isset($on) && $on instanceof NamedElementInterface) { diff --git a/src/Services/EntityMergers/Mergers/PartMerger.php b/src/Services/EntityMergers/Mergers/PartMerger.php index 4ce779e88..f1c25a7b2 100644 --- a/src/Services/EntityMergers/Mergers/PartMerger.php +++ b/src/Services/EntityMergers/Mergers/PartMerger.php @@ -65,6 +65,7 @@ public function merge(object $target, object $other, array $context = []): Part $this->useOtherValueIfNotNull($target, $other, 'footprint'); $this->useOtherValueIfNotNull($target, $other, 'category'); $this->useOtherValueIfNotNull($target, $other, 'partUnit'); + $this->useOtherValueIfNotNull($target, $other, 'partCustomState'); //We assume that the higher value is the correct one for minimum instock $this->useLargerValue($target, $other, 'minamount'); diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php index 78db06f07..bdf26fa94 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -22,11 +22,13 @@ namespace App\Services; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\PartAttachment; use App\Entity\Base\AbstractDBElement; use App\Entity\Parameters\PartParameter; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; @@ -98,6 +100,7 @@ public function timeTravelURL(AbstractDBElement $entity, \DateTimeInterface $dat AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', Project::class => 'project_edit', + Assembly::class => 'assembly_edit', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', StorageLocation::class => 'store_location_edit', @@ -107,6 +110,7 @@ public function timeTravelURL(AbstractDBElement $entity, \DateTimeInterface $dat MeasurementUnit::class => 'measurement_unit_edit', Group::class => 'group_edit', LabelProfile::class => 'label_profile_edit', + PartCustomState::class => 'part_custom_state_edit', ]; try { @@ -204,6 +208,7 @@ public function infoURL(AbstractDBElement $entity): string AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', Project::class => 'project_info', + Assembly::class => 'assembly_info', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', StorageLocation::class => 'store_location_edit', @@ -213,6 +218,7 @@ public function infoURL(AbstractDBElement $entity): string MeasurementUnit::class => 'measurement_unit_edit', Group::class => 'group_edit', LabelProfile::class => 'label_profile_edit', + PartCustomState::class => 'part_custom_state_edit', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); @@ -234,6 +240,7 @@ public function editURL(AbstractDBElement $entity): string AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', Project::class => 'project_edit', + Assembly::class => 'assembly_edit', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', StorageLocation::class => 'store_location_edit', @@ -243,6 +250,7 @@ public function editURL(AbstractDBElement $entity): string MeasurementUnit::class => 'measurement_unit_edit', Group::class => 'group_edit', LabelProfile::class => 'label_profile_edit', + PartCustomState::class => 'part_custom_state_edit', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); @@ -265,6 +273,7 @@ public function createURL(AbstractDBElement|string $entity): string AttachmentType::class => 'attachment_type_new', Category::class => 'category_new', Project::class => 'project_new', + Assembly::class => 'assembly_new', Supplier::class => 'supplier_new', Manufacturer::class => 'manufacturer_new', StorageLocation::class => 'store_location_new', @@ -274,6 +283,7 @@ public function createURL(AbstractDBElement|string $entity): string MeasurementUnit::class => 'measurement_unit_new', Group::class => 'group_new', LabelProfile::class => 'label_profile_new', + PartCustomState::class => 'part_custom_state_new', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity)); @@ -296,6 +306,7 @@ public function cloneURL(AbstractDBElement $entity): string AttachmentType::class => 'attachment_type_clone', Category::class => 'category_clone', Project::class => 'device_clone', + Assembly::class => 'assembly_clone', Supplier::class => 'supplier_clone', Manufacturer::class => 'manufacturer_clone', StorageLocation::class => 'store_location_clone', @@ -305,6 +316,7 @@ public function cloneURL(AbstractDBElement $entity): string MeasurementUnit::class => 'measurement_unit_clone', Group::class => 'group_clone', LabelProfile::class => 'label_profile_clone', + PartCustomState::class => 'part_custom_state_clone', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); @@ -323,6 +335,7 @@ public function listPartsURL(AbstractDBElement $entity): string { $map = [ Project::class => 'project_info', + Assembly::class => 'assembly_info', Category::class => 'part_list_category', Footprint::class => 'part_list_footprint', @@ -341,6 +354,7 @@ public function deleteURL(AbstractDBElement $entity): string AttachmentType::class => 'attachment_type_delete', Category::class => 'category_delete', Project::class => 'project_delete', + Assembly::class => 'assembly_delete', Supplier::class => 'supplier_delete', Manufacturer::class => 'manufacturer_delete', StorageLocation::class => 'store_location_delete', @@ -350,6 +364,7 @@ public function deleteURL(AbstractDBElement $entity): string MeasurementUnit::class => 'measurement_unit_delete', Group::class => 'group_delete', LabelProfile::class => 'label_profile_delete', + PartCustomState::class => 'part_custom_state_delete', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 862fa463f..650a6294c 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -22,21 +22,37 @@ */ 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\HttpFoundation\File\UploadedFile; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; +use UnexpectedValueException; +use Symfony\Component\Validator\ConstraintViolation; /** * @see \App\Tests\Services\ImportExportSystem\BOMImporterTest */ class BOMImporter { + private const IMPORT_TYPE_JSON = 'json'; + private const IMPORT_TYPE_CSV = 'csv'; + private const IMPORT_TYPE_KICAD_PCB = 'kicad_pcbnew'; + private const IMPORT_TYPE_KICAD_SCHEMATIC = 'kicad_schematic'; private const MAP_KICAD_PCB_FIELDS = [ 0 => 'Id', @@ -47,17 +63,35 @@ class BOMImporter 5 => 'Supplier and ref', ]; + private string $jsonRoot = ''; + + private PartRepository $partRepository; + + private ManufacturerRepository $manufacturerRepository; + + private CategoryRepository $categoryRepository; + + private DBElementRepository $projectBomEntryRepository; + + private DBElementRepository $assemblyBomEntryRepository; + public function __construct( private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, - private readonly BOMValidationService $validationService + private readonly BOMValidationService $validationService, + private readonly TranslatorInterface $translator ) { + $this->partRepository = $this->entityManager->getRepository(Part::class); + $this->manufacturerRepository = $this->entityManager->getRepository(Manufacturer::class); + $this->categoryRepository = $this->entityManager->getRepository(Category::class); + $this->projectBomEntryRepository = $this->entityManager->getRepository(ProjectBOMEntry::class); + $this->assemblyBomEntryRepository = $this->entityManager->getRepository(AssemblyBOMEntry::class); } protected function configureOptions(OptionsResolver $resolver): OptionsResolver { $resolver->setRequired('type'); - $resolver->setAllowedValues('type', ['kicad_pcbnew', 'kicad_schematic']); + $resolver->setAllowedValues('type', [self::IMPORT_TYPE_KICAD_PCB, self::IMPORT_TYPE_KICAD_SCHEMATIC, self::IMPORT_TYPE_JSON, self::IMPORT_TYPE_CSV]); // For flexible schematic import with field mapping $resolver->setDefined(['field_mapping', 'field_priorities', 'delimiter']); @@ -73,27 +107,118 @@ protected function configureOptions(OptionsResolver $resolver): OptionsResolver /** * Converts the given file into an array of BOM entries using the given options and save them into the given project. * The changes are not saved into the database yet. - * @return ProjectBOMEntry[] */ - public function importFileIntoProject(File $file, Project $project, array $options): array + public function importFileIntoProject(UploadedFile $file, Project $project, array $options): ImporterResult { - $bom_entries = $this->fileToBOMEntries($file, $options); + $importerResult = $this->fileToImporterResult($project, $file, $options); - //Assign the bom_entries to the project - foreach ($bom_entries as $bom_entry) { - $project->addBomEntry($bom_entry); + if ($importerResult->getViolations()->count() === 0) { + //Assign the bom_entries to the project + foreach ($importerResult->getBomEntries() as $bomEntry) { + $project->addBomEntry($bomEntry); + } } - return $bom_entries; + return $importerResult; + } + + /** + * Imports a file into an Assembly object and processes its contents. + * + * This method converts the provided file into an ImporterResult object that contains BOM entries and potential + * validation violations. If no violations are found, the BOM entries extracted from the file are added to the + * provided Assembly object. + * + * @param UploadedFile $file The file to be imported and processed. + * @param Assembly $assembly The target Assembly object to which the BOM entries are added. + * @param array $options Options or configurations related to the import process. + * + * @return ImporterResult An object containing the result of the import process, including BOM entries and any violations. + */ + public function importFileIntoAssembly(UploadedFile $file, Assembly $assembly, array $options): ImporterResult + { + $importerResult = $this->fileToImporterResult($assembly, $file, $options); + + if ($importerResult->getViolations()->count() === 0) { + //Assign the bom_entries to the assembly + foreach ($importerResult->getBomEntries() as $bomEntry) { + $assembly->addBomEntry($bomEntry); + } + } + + return $importerResult; } /** - * Converts the given file into an array of BOM entries using the given options. - * @return ProjectBOMEntry[] + * Converts the content of a file into an array of BOM (Bill of Materials) entries. + * + * This method processes the content of the provided file and delegates the conversion + * to a helper method that generates BOM entries based on the provided import object and options. + * + * @param Project|Assembly $importObject The object determining the context of the BOM entries (either a Project or Assembly). + * @param File $file The file whose content will be converted into BOM entries. + * @param array $options Additional options or configurations to be applied during the conversion process. + * + * @return array An array of BOM entries created from the file content. */ - public function fileToBOMEntries(File $file, array $options): array + public function fileToBOMEntries(Project|Assembly $importObject, File $file, array $options): array { - return $this->stringToBOMEntries($file->getContent(), $options); + return $this->stringToBOMEntries($importObject, $file->getContent(), $options); + } + + + /** + * Handles the conversion of an uploaded file into an ImporterResult for a given project or assembly. + * + * This method processes the uploaded file by validating its file extension based on the provided import type + * options and then proceeds to convert the file content into an ImporterResult. If the file extension is + * invalid or unsupported, the result will contain a corresponding violation. + * + * @param Project|Assembly $importObject The context of the import operation (either a Project or Assembly). + * @param UploadedFile $file The uploaded file to be processed. + * @param array $options An array of options, expected to include an 'type' key to determine valid file types. + * + * @return ImporterResult An object containing the results of the import process, including any detected violations. + */ + public function fileToImporterResult(Project|Assembly $importObject, UploadedFile $file, array $options): ImporterResult + { + $result = new ImporterResult(); + + //Available file endings depending on the import type + $validExtensions = match ($options['type']) { + self::IMPORT_TYPE_KICAD_PCB => ['kicad_pcb'], + self::IMPORT_TYPE_JSON => ['json'], + self::IMPORT_TYPE_CSV => ['csv'], + default => [], + }; + + //Get the file extension of the uploaded file + $fileExtension = pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION); + + //Check whether the file extension is valid + if ($validExtensions === []) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.invalid_import_type', + 'import.type' + )); + + return $result; + } else if (!in_array(strtolower($fileExtension), $validExtensions, true)) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.invalid_file_extension', + 'file.extension', + $fileExtension, + [ + '%extension%' => $fileExtension, + '%importType%' => $this->translator->trans($importObject instanceof Project ? 'project.bom_import.type.'.$options['type'] : 'assembly.bom_import.type.'.$options['type']), + '%allowedExtensions%' => implode(', ', $validExtensions), + ] + )); + + return $result; + } + + return $this->stringToImporterResult($importObject, $file->getContent(), $options); } /** @@ -115,31 +240,77 @@ public function validateBOMData(string $data, array $options): array /** * Import string data into an array of BOM entries, which are not yet assigned to a project. - * @param string $data The data to import - * @param array $options An array of options - * @return ProjectBOMEntry[] An array of imported entries + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $data The data to import + * @param array $options An array of options + * + * @return ProjectBOMEntry[]|AssemblyBOMEntry[] An array of imported entries */ - public function stringToBOMEntries(string $data, array $options): array + public function stringToBOMEntries(Project|Assembly $importObject, string $data, array $options): array { $resolver = new OptionsResolver(); $resolver = $this->configureOptions($resolver); $options = $resolver->resolve($options); return match ($options['type']) { - 'kicad_pcbnew' => $this->parseKiCADPCB($data), - 'kicad_schematic' => $this->parseKiCADSchematic($data, $options), - default => throw new InvalidArgumentException('Invalid import type!'), + self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $importObject)->getBomEntries(), + self::IMPORT_TYPE_KICAD_SCHEMATIC => $this->parseKiCADSchematic($data, $options), + default => throw new InvalidArgumentException($this->translator->trans('validator.bom_importer.invalid_import_type', [], 'validators')), + }; + } + + /** + * Import string data into an array of BOM entries, which are not yet assigned to a project. + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $data The data to import + * @param array $options An array of options + * + * @return ImporterResult An result of imported entries or a violation list + */ + public function stringToImporterResult(Project|Assembly $importObject, string $data, array $options): ImporterResult + { + $resolver = new OptionsResolver(); + $resolver = $this->configureOptions($resolver); + $options = $resolver->resolve($options); + + $defaultImporterResult = new ImporterResult(); + $defaultImporterResult->addViolation($this->buildJsonViolation( + 'validator.bom_importer.invalid_import_type', + 'import.type' + )); + + return match ($options['type']) { + self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $importObject), + self::IMPORT_TYPE_JSON => $this->parseJson($importObject, $data), + self::IMPORT_TYPE_CSV => $this->parseCsv($importObject, $data), + default => $defaultImporterResult, }; } - private function parseKiCADPCB(string $data): array + /** + * Parses a KiCAD PCB file and imports its BOM (Bill of Materials) entries into the given Project or Assembly context. + * + * This method processes a semicolon-delimited CSV data string, normalizes column names, + * validates the required fields, and creates BOM entries for each record in the data. + * The BOM entries are added to the provided Project or Assembly, depending on the context. + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $data The semicolon- or comma-delimited CSV data to be parsed + * + * @return ImporterResult The result of the import process, containing the created BOM entries. + * + * @throws UnexpectedValueException If required fields are missing in the provided data. + */ + private function parseKiCADPCB(string $data, Project|Assembly $importObject): ImporterResult { + $result = new ImporterResult(); + $csv = Reader::createFromString($data); $csv->setDelimiter(';'); $csv->setHeaderOffset(0); - $bom_entries = []; - foreach ($csv->getRecords() as $offset => $entry) { //Translate the german field names to english $entry = $this->normalizeColumnNames($entry); @@ -158,16 +329,21 @@ private function parseKiCADPCB(string $data): array throw new \UnexpectedValueException('Quantity missing at line ' . ($offset + 1) . '!'); } - $bom_entry = new ProjectBOMEntry(); - $bom_entry->setName($entry['Designation'] . ' (' . $entry['Package'] . ')'); - $bom_entry->setMountnames($entry['Designator'] ?? ''); + $bom_entry = $importObject instanceof Project ? new ProjectBOMEntry() : new AssemblyBOMEntry(); + if ($bom_entry instanceof ProjectBOMEntry) { + $bom_entry->setName($entry['Designation'] . ' (' . $entry['Package'] . ')'); + } else { + $bom_entry->setName($entry['Designation']); + } + + $bom_entry->setMountnames($entry['Designator']); $bom_entry->setComment($entry['Supplier and ref'] ?? ''); $bom_entry->setQuantity((float) ($entry['Quantity'] ?? 1)); - $bom_entries[] = $bom_entry; + $result->addBomEntry($bom_entry); } - return $bom_entries; + return $result; } /** @@ -227,6 +403,537 @@ private function validateKiCADSchematicData(string $data, array $options): array return $this->validationService->validateBOMEntries($mapped_entries, $options); } + /** + * Parses the given JSON data into an ImporterResult while validating and transforming entries according to the + * specified options and object type. If violations are encountered during parsing, they are added to the result. + * + * The structure of each entry in the JSON data is validated to ensure that required fields (e.g., quantity, and name) + * are present, and optional composite fields, like `part` and its sub-properties, meet specific criteria. Various + * conditions are checked, including whether the provided values are the correct types, and if relationships (like + * matching parts or manufacturers) are resolved successfully. + * + * Violations are added for: + * - Missing or invalid `quantity` values. + * - Non-string `name` values. + * - Invalid structure or missing sub-properties in `part`. + * - Incorrect or unresolved references to parts and their information, such as `id`, `name`, `manufacturer_product_number` + * (mpnr), `internal_part_number` (ipn), or `description`. + * - Inconsistent or absent manufacturer information. + * + * If a match for a part or manufacturer cannot be resolved, a violation is added alongside an indication of the + * imported value and any partially matched information. Warnings for no exact matches are also added for parts + * using specific identifying properties like name, manufacturer product number, or internal part numbers. + * + * Additional validations include: + * - Checking for empty or invalid descriptions. + * - Ensuring manufacturers, if specified, have valid `name` or `id` values. + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $data JSON encoded string containing BOM entries data. + * + * @return ImporterResult The result containing parsed data and any violations encountered during the parsing process. + */ + private function parseJson(Project|Assembly $importObject, string $data): ImporterResult + { + $result = new ImporterResult(); + $this->jsonRoot = 'JSON Import for '.($importObject instanceof Project ? 'Project' : 'Assembly'); + + $data = json_decode($data, true); + + foreach ($data as $key => $entry) { + if (!isset($entry['quantity'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.quantity.required', + "entry[$key].quantity" + )); + } + + if (isset($entry['quantity']) && (!is_float($entry['quantity']) || $entry['quantity'] <= 0)) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.quantity.float', + "entry[$key].quantity", + $entry['quantity'] + )); + } + + if (isset($entry['name']) && !is_string($entry['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.string.notEmpty', + "entry[$key].name", + $entry['name'] + )); + } + + if (isset($entry['part'])) { + $this->processPart($importObject, $entry, $result, $key, self::IMPORT_TYPE_JSON); + } else { + $bomEntry = $this->getOrCreateBomEntry($importObject, $entry['name'] ?? null); + $bomEntry->setQuantity((float) $entry['quantity']); + + $result->addBomEntry($bomEntry); + } + } + + return $result; + } + + + /** + * Parses a CSV string and processes its rows into hierarchical data structures, + * performing validations and converting data based on the provided headers. + * Handles potential violations and manages the creation of BOM entries based on the given type. + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string $csvData The raw CSV data to parse, with rows separated by newlines. + * + * @return ImporterResult Returns an ImporterResult instance containing BOM entries and any validation violations encountered. + */ + function parseCsv(Project|Assembly $importObject, string $csvData): ImporterResult + { + $result = new ImporterResult(); + $rows = explode("\r\n", trim($csvData)); + $headers = str_getcsv(array_shift($rows)); + + if (count($headers) === 1 && isset($headers[0])) { + //If only one column was recognized, try fallback with semicolon as a separator + $headers = str_getcsv($headers[0], ';'); + } + + foreach ($rows as $key => $row) { + $entry = []; + $values = str_getcsv($row); + + if (count($values) === 1 || count($values) !== count($headers)) { + //If only one column was recognized, try fallback with semicolon as a separator + $values = str_getcsv($row, ';'); + } + + foreach ($headers as $index => $column) { + //Change the column names in small letters + $column = strtolower($column); + + //Convert column name into hierarchy + $path = explode('_', $column); + $temp = &$entry; + + foreach ($path as $step) { + if (!isset($temp[$step])) { + $temp[$step] = []; + } + + $temp = &$temp[$step]; + } + + //If there is no value, skip + if (isset($values[$index]) && $values[$index] !== '') { + //Check whether the value is numerical + if (is_numeric($values[$index]) && !in_array($column, ['name','description','manufacturer','designator'])) { + //Convert to integer or float + $temp = (str_contains($values[$index], '.')) + ? floatval($values[$index]) + : intval($values[$index]); + } else { + //Leave other data types untouched + $temp = $values[$index]; + } + } + } + + $entry = $this->removeEmptyProperties($entry); + + if (!isset($entry['quantity'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.csv.quantity.required', + "row[$key].quantity" + )); + } + + if (isset($entry['quantity']) && (!is_numeric($entry['quantity']) || $entry['quantity'] <= 0)) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.csv.quantity.float', + "row[$key].quantity", + $entry['quantity'] + )); + } + + if (isset($entry['name']) && !is_string($entry['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.string.notEmpty', + "row[$key].name", + $entry['name'] + )); + } + + if (isset($entry['id']) && is_numeric($entry['id'])) { + //Use id column as a fallback for the expected part_id column + $entry['part']['id'] = (int) $entry['id']; + } + + if (isset($entry['part'])) { + $this->processPart($importObject, $entry, $result, $key, self::IMPORT_TYPE_CSV); + } else { + $bomEntry = $this->getOrCreateBomEntry($importObject, $entry['name'] ?? null); + + if (isset($entry['designator'])) { + $bomEntry->setMountnames(trim($entry['designator']) === '' ? '' : trim($entry['designator'])); + } + + $bomEntry->setQuantity((float) $entry['quantity'] ?? 0); + + $result->addBomEntry($bomEntry); + } + } + + return $result; + } + + /** + * Processes an individual part entry in the import data. + * + * This method validates the structure and content of the provided part entry and uses the findings + * to identify corresponding objects in the database. The result is recorded, and violations are + * logged if issues or discrepancies exist in the validation or database matching process. + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param array $entry The array representation of the part entry. + * @param ImporterResult $result The result object used for recording validation violations. + * @param int $key The index of the entry in the data array. + * @param string $importType The type of import being performed. + * + * @return void + */ + private function processPart(Project|Assembly $importObject, array $entry, ImporterResult $result, int $key, string $importType): void + { + $prefix = $importType === self::IMPORT_TYPE_JSON ? 'entry' : 'row'; + + if (!is_array($entry['part'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.array', + $prefix."[$key].part", + $entry['part'] + )); + } + + $partIdValid = isset($entry['part']['id']) && is_int($entry['part']['id']) && $entry['part']['id'] > 0; + $partMpnrValid = isset($entry['part']['mpnr']) && is_string($entry['part']['mpnr']) && trim($entry['part']['mpnr']) !== ''; + $partIpnValid = isset($entry['part']['ipn']) && is_string($entry['part']['ipn']) && trim($entry['part']['ipn']) !== ''; + $partNameValid = isset($entry['part']['name']) && is_string($entry['part']['name']) && trim($entry['part']['name']) !== ''; + + if (!$partIdValid && !$partNameValid && !$partMpnrValid && !$partIpnValid) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.subproperties', + $prefix."[$key].part", + $entry['part'], + ['%propertyString%' => '"id", "name", "mpnr", or "ipn"'] + )); + } + + $part = $partIdValid ? $this->partRepository->findOneBy(['id' => $entry['part']['id']]) : null; + $part = $part ?? ($partMpnrValid ? $this->partRepository->findOneBy(['manufacturer_product_number' => trim($entry['part']['mpnr'])]) : null); + $part = $part ?? ($partIpnValid ? $this->partRepository->findOneBy(['ipn' => trim($entry['part']['ipn'])]) : null); + $part = $part ?? ($partNameValid ? $this->partRepository->findOneBy(['name' => trim($entry['part']['name'])]) : null); + + if ($part === null) { + $value = sprintf('part.id: %s, part.mpnr: %s, part.ipn: %s, part.name: %s', + isset($entry['part']['id']) ? '' . $entry['part']['id'] . '' : '-', + isset($entry['part']['mpnr']) ? '' . $entry['part']['mpnr'] . '' : '-', + isset($entry['part']['ipn']) ? '' . $entry['part']['ipn'] . '' : '-', + isset($entry['part']['name']) ? '' . $entry['part']['name'] . '' : '-', + ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.notFoundFor', + $prefix."[$key].part", + $entry['part'], + ['%value%' => $value] + )); + } + + if ($partNameValid && $part !== null && isset($entry['part']['name']) && $part->getName() !== trim($entry['part']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.name", + $entry['part']['name'], + [ + '%importValue%' => '' . $entry['part']['name'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getName() . '' + ] + )); + } + + if ($partMpnrValid && $part !== null && isset($entry['part']['mpnr']) && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.mpnr", + $entry['part']['mpnr'], + [ + '%importValue%' => '' . $entry['part']['mpnr'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getManufacturerProductNumber() . '' + ] + )); + } + + if ($partIpnValid && $part !== null && isset($entry['part']['ipn']) && $part->getIpn() !== trim($entry['part']['ipn'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.ipn", + $entry['part']['ipn'], + [ + '%importValue%' => '' . $entry['part']['ipn'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getIpn() . '' + ] + )); + } + + if (isset($entry['part']['description'])) { + if (!is_string($entry['part']['description']) || trim($entry['part']['description']) === '') { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.string.notEmpty', + 'entry[$key].part.description', + $entry['part']['description'] + )); + } + } + + $partDescription = $entry['part']['description'] ?? ''; + + $manufacturerIdValid = false; + $manufacturerNameValid = false; + if (array_key_exists('manufacturer', $entry['part'])) { + if (!is_array($entry['part']['manufacturer'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.array', + 'entry[$key].part.manufacturer', + $entry['part']['manufacturer']) ?? null + ); + } + + $manufacturerIdValid = isset($entry['part']['manufacturer']['id']) && is_int($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] > 0; + $manufacturerNameValid = isset($entry['part']['manufacturer']['name']) && is_string($entry['part']['manufacturer']['name']) && trim($entry['part']['manufacturer']['name']) !== ''; + + if (!$manufacturerIdValid && !$manufacturerNameValid) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties', + $prefix."[$key].part.manufacturer", + $entry['part']['manufacturer'], + )); + } + } + + $manufacturer = $manufacturerIdValid ? $this->manufacturerRepository->findOneBy(['id' => $entry['part']['manufacturer']['id']]) : null; + $manufacturer = $manufacturer ?? ($manufacturerNameValid ? $this->manufacturerRepository->findOneBy(['name' => trim($entry['part']['manufacturer']['name'])]) : null); + + if (($manufacturerIdValid || $manufacturerNameValid) && $manufacturer === null) { + $value = sprintf( + 'manufacturer.id: %s, manufacturer.name: %s', + isset($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] !== null ? '' . $entry['part']['manufacturer']['id'] . '' : '-', + isset($entry['part']['manufacturer']['name']) && $entry['part']['manufacturer']['name'] !== null ? '' . $entry['part']['manufacturer']['name'] . '' : '-' + ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.notFoundFor', + $prefix."[$key].part.manufacturer", + $entry['part']['manufacturer'], + ['%value%' => $value] + )); + } + + if ($manufacturerNameValid && $manufacturer !== null && isset($entry['part']['manufacturer']['name']) && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.manufacturer.name", + $entry['part']['manufacturer']['name'], + [ + '%importValue%' => '' . $entry['part']['manufacturer']['name'] . '', + '%foundId%' => $manufacturer->getID(), + '%foundValue%' => '' . $manufacturer->getName() . '' + ] + )); + } + + $categoryIdValid = false; + $categoryNameValid = false; + if (array_key_exists('category', $entry['part'])) { + if (!is_array($entry['part']['category'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.array', + 'entry[$key].part.category', + $entry['part']['category']) ?? null + ); + } + + $categoryIdValid = isset($entry['part']['category']['id']) && is_int($entry['part']['category']['id']) && $entry['part']['category']['id'] > 0; + $categoryNameValid = isset($entry['part']['category']['name']) && is_string($entry['part']['category']['name']) && trim($entry['part']['category']['name']) !== ''; + + if (!$categoryIdValid && !$categoryNameValid) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties', + $prefix."[$key].part.category", + $entry['part']['category'] + )); + } + } + + $category = $categoryIdValid ? $this->categoryRepository->findOneBy(['id' => $entry['part']['category']['id']]) : null; + $category = $category ?? ($categoryNameValid ? $this->categoryRepository->findOneBy(['name' => trim($entry['part']['category']['name'])]) : null); + + if (($categoryIdValid || $categoryNameValid)) { + $value = sprintf( + 'category.id: %s, category.name: %s', + isset($entry['part']['category']['id']) && $entry['part']['category']['id'] !== null ? '' . $entry['part']['category']['id'] . '' : '-', + isset($entry['part']['category']['name']) && $entry['part']['category']['name'] !== null ? '' . $entry['part']['category']['name'] . '' : '-' + ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.notFoundFor', + $prefix."[$key].part.category", + $entry['part']['category'], + ['%value%' => $value] + )); + } + + if ($categoryNameValid && $category !== null && isset($entry['part']['category']['name']) && $category->getName() !== trim($entry['part']['category']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json_csv.parameter.noExactMatch', + $prefix."[$key].part.category.name", + $entry['part']['category']['name'], + [ + '%importValue%' => '' . $entry['part']['category']['name'] . '', + '%foundId%' => $category->getID(), + '%foundValue%' => '' . $category->getName() . '' + ] + )); + } + + if ($result->getViolations()->count() > 0) { + return; + } + + if ($partDescription !== '') { + //When updating the associated parts to a assembly, take over the description of the part. + $part->setDescription($partDescription); + } + + if ($manufacturer !== null && $manufacturer->getID() !== $part->getManufacturer()->getID()) { + //When updating the associated parts, take over to a assembly of the manufacturer of the part. + $part->setManufacturer($manufacturer); + } + + if ($category !== null && $category->getID() !== $part->getCategory()->getID()) { + //When updating the associated parts to a assembly, take over the category of the part. + $part->setCategory($category); + } + + if ($importObject instanceof Assembly) { + $bomEntry = $this->assemblyBomEntryRepository->findOneBy(['assembly' => $importObject, 'part' => $part]); + + if ($bomEntry === null) { + if (isset($entry['name']) && $entry['name'] !== '') { + $bomEntry = $this->assemblyBomEntryRepository->findOneBy(['assembly' => $importObject, 'name' => $entry['name']]); + } + + if ($bomEntry === null) { + $bomEntry = new AssemblyBOMEntry(); + } + } + } else { + $bomEntry = $this->projectBomEntryRepository->findOneBy(['project' => $importObject, 'part' => $part]); + + if ($bomEntry === null) { + if (isset($entry['name']) && $entry['name'] !== '') { + $bomEntry = $this->projectBomEntryRepository->findOneBy(['project' => $importObject, 'name' => $entry['name']]); + } + + if ($bomEntry === null) { + $bomEntry = new ProjectBOMEntry(); + } + } + } + + $bomEntry->setQuantity((float) $entry['quantity']); + + if (isset($entry['name'])) { + $givenName = trim($entry['name']) === '' ? null : trim ($entry['name']); + + if ($givenName !== null && $bomEntry->getPart() !== null && $bomEntry->getPart()->getName() !== $givenName) { + //Apply different names for parts list entry + $bomEntry->setName(trim($entry['name']) === '' ? null : trim ($entry['name'])); + } + } else { + $bomEntry->setName(null); + } + + if (isset($entry['designator'])) { + $bomEntry->setMountnames(trim($entry['designator']) === '' ? '' : trim($entry['designator'])); + } + + $bomEntry->setPart($part); + + $result->addBomEntry($bomEntry); + } + + private function removeEmptyProperties(array $data): array + { + foreach ($data as $key => &$value) { + //Recursive check when the value is an array + if (is_array($value)) { + $value = $this->removeEmptyProperties($value); + + //Remove the array when it is empty after cleaning + if (empty($value)) { + unset($data[$key]); + } + } elseif ($value === null || $value === '') { + //Remove values that are explicitly zero or empty + unset($data[$key]); + } + } + + return $data; + } + + /** + * Retrieves an existing BOM (Bill of Materials) entry by name or creates a new one if not found. + * + * Depending on whether the provided import object is a Project or Assembly, this method attempts to locate + * a corresponding BOM entry in the appropriate repository. If no entry is located, a new BOM entry object + * is instantiated according to the type of the import object. + * + * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly). + * @param string|null $name The name of the BOM entry to search for or assign to a new entry. + * + * @return ProjectBOMEntry|AssemblyBOMEntry An existing or newly created BOM entry. + */ + private function getOrCreateBomEntry(Project|Assembly $importObject, ?string $name): ProjectBOMEntry|AssemblyBOMEntry + { + $bomEntry = null; + + //Check whether there is a name + if (!empty($name)) { + if ($importObject instanceof Project) { + $bomEntry = $this->projectBomEntryRepository->findOneBy(['name' => $name]); + } else { + $bomEntry = $this->assemblyBomEntryRepository->findOneBy(['name' => $name]); + } + } + + //If no bom entry was found, a new object create + if ($bomEntry === null) { + if ($importObject instanceof Project) { + $bomEntry = new ProjectBOMEntry(); + } else { + $bomEntry = new AssemblyBOMEntry(); + } + } + + $bomEntry->setName($name); + + return $bomEntry; + } + /** * This function uses the order of the fields in the CSV files to make them locale independent. * @param array $entry @@ -243,13 +950,28 @@ private function normalizeColumnNames(array $entry): array } //@phpstan-ignore-next-line We want to keep this check just to be safe when something changes - $new_index = self::MAP_KICAD_PCB_FIELDS[$index] ?? throw new \UnexpectedValueException('Invalid field index!'); + $new_index = self::MAP_KICAD_PCB_FIELDS[$index] ?? throw new UnexpectedValueException('Invalid field index!'); $out[$new_index] = $field; } return $out; } + + /** + * Builds a JSON-based constraint violation. + * + * This method creates a `ConstraintViolation` object that represents a validation error. + * The violation includes a message, property path, invalid value, and other contextual information. + * Translations for the violation message can be applied through the translator service. + * + * @param string $message The translation key for the validation message. + * @param string $propertyPath The property path where the violation occurred. + * @param mixed|null $invalidValue The value that caused the violation (optional). + * @param array $parameters Additional parameters for message placeholders (default is an empty array). + * + * @return ConstraintViolation The created constraint violation object. + */ /** * Parse KiCad schematic BOM with flexible field mapping */ @@ -727,4 +1449,30 @@ public function detectFields(string $data, ?string $delimiter = null): array return array_values($headers); } + + /** + * Builds a JSON-based constraint violation. + * + * This method creates a `ConstraintViolation` object that represents a validation error. + * The violation includes a message, property path, invalid value, and other contextual information. + * Translations for the violation message can be applied through the translator service. + * + * @param string $message The translation key for the validation message. + * @param string $propertyPath The property path where the violation occurred. + * @param mixed|null $invalidValue The value that caused the violation (optional). + * @param array $parameters Additional parameters for message placeholders (default is an empty array). + * + * @return ConstraintViolation The created constraint violation object. + */ + private function buildJsonViolation(string $message, string $propertyPath, mixed $invalidValue = null, array $parameters = []): ConstraintViolation + { + return new ConstraintViolation( + message: $this->translator->trans($message, $parameters, 'validators'), + messageTemplate: $message, + parameters: $parameters, + root: $this->jsonRoot, + propertyPath: $propertyPath, + invalidValue: $invalidValue + ); + } } diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 271642dad..50364da1d 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -22,8 +22,21 @@ namespace App\Services\ImportExportSystem; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\LabelSystem\LabelProfile; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\StorageLocation; +use App\Entity\Parts\Supplier; +use App\Entity\PriceInformations\Currency; +use App\Entity\ProjectSystem\Project; +use App\Helpers\Assemblies\AssemblyPartAggregator; use App\Helpers\FilenameSanatizer; use App\Serializer\APIPlatform\SkippableItemNormalizer; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -45,8 +58,10 @@ */ class EntityExporter { - public function __construct(protected SerializerInterface $serializer) - { + public function __construct( + protected SerializerInterface $serializer, + protected AssemblyPartAggregator $partAggregator, private readonly AssemblyPartAggregator $assemblyPartAggregator, + ) { } protected function configureOptions(OptionsResolver $resolver): void @@ -62,6 +77,10 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setDefault('include_children', false); $resolver->setAllowedTypes('include_children', 'bool'); + + $resolver->setDefault('readableSelect', null); + $resolver->setAllowedValues('readableSelect', [null, 'readable', 'readable_bom']); + } /** @@ -144,15 +163,67 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities, $entities = [$entities]; } - //Do the serialization with the given options - $serialized_data = $this->exportEntities($entities, $options); + if ($request->get('readableSelect', false) === 'readable') { + // Map entity classes to export functions + $entityExportMap = [ + AttachmentType::class => fn($entities) => $this->exportReadable($entities, AttachmentType::class), + Category::class => fn($entities) => $this->exportReadable($entities, Category::class), + Project::class => fn($entities) => $this->exportReadable($entities, Project::class), + Assembly::class => fn($entities) => $this->exportReadable($entities, Assembly::class), + Supplier::class => fn($entities) => $this->exportReadable($entities, Supplier::class), + Manufacturer::class => fn($entities) => $this->exportReadable($entities, Manufacturer::class), + StorageLocation::class => fn($entities) => $this->exportReadable($entities, StorageLocation::class), + Footprint::class => fn($entities) => $this->exportReadable($entities, Footprint::class), + Currency::class => fn($entities) => $this->exportReadable($entities, Currency::class), + MeasurementUnit::class => fn($entities) => $this->exportReadable($entities, MeasurementUnit::class), + LabelProfile::class => fn($entities) => $this->exportReadable($entities, LabelProfile::class, false), + ]; + + // Determine the type of the entity + $type = null; + foreach ($entities as $entity) { + $entityClass = get_class($entity); + if (isset($entityExportMap[$entityClass])) { + $type = $entityClass; + break; + } + } + + // Generate the response + $response = isset($entityExportMap[$type]) + ? new Response($entityExportMap[$type]($entities)) + : new Response(''); + + $options['format'] = 'csv'; + $options['level'] = 'readable'; + } elseif ($request->get('readableSelect', false) === 'readable_bom') { + $hierarchies = []; + + foreach ($entities as $entity) { + if (!$entity instanceof Assembly) { + throw new InvalidArgumentException('Only assemblies can be exported in readable BOM format'); + } + + $hierarchies[] = $this->assemblyPartAggregator->processAssemblyHierarchyForPdf($entity, 0, 1, 1); + } + + $pdfContent = $this->assemblyPartAggregator->exportReadableHierarchyForPdf($hierarchies); + + $response = new Response($pdfContent); - $response = new Response($serialized_data); + $options['format'] = 'pdf'; + $options['level'] = 'readable_bom'; + } else { + //Do the serialization with the given options + $serialized_data = $this->exportEntities($entities, $options); - //Resolve the format - $optionsResolver = new OptionsResolver(); - $this->configureOptions($optionsResolver); - $options = $optionsResolver->resolve($options); + $response = new Response($serialized_data); + + //Resolve the format + $optionsResolver = new OptionsResolver(); + $this->configureOptions($optionsResolver); + $options = $optionsResolver->resolve($options); + } //Determine the content type for the response @@ -168,6 +239,9 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities, case 'json': $content_type = 'application/json'; break; + case 'pdf': + $content_type = 'application/pdf'; + break; } $response->headers->set('Content-Type', $content_type); @@ -203,4 +277,293 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities, return $response; } + + /** + * Exports data for multiple entity types in a readable CSV format. + * + * @param array $entities The entities to export. + * @param string $type The type of entities ('category', 'project', 'assembly', 'attachmentType', 'supplier'). + * @return string The generated CSV content as a string. + */ + public function exportReadable(array $entities, string $type, bool $isHierarchical = true): string + { + //Define headers and entity-specific processing logic + $defaultProcessEntity = fn($entity, $depth) => [ + 'Id' => $entity->getId(), + 'ParentId' => $entity->getParent()?->getId() ?? '', + 'NameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(), + 'Name' => $entity->getName(), + 'FullName' => $this->getFullName($entity), + ]; + + $config = [ + AttachmentType::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Category::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Project::class => [ + 'header' => [ + 'Id', 'ParentId', 'Type', 'ProjectNameHierarchical', 'ProjectName', 'ProjectFullName', 'BomQuantity', + 'BomPartId', 'BomPartIpn', 'BomPartMpnr', 'BomPartName', 'BomDesignator', 'BomPartDescription', + 'BomMountNames' + ], + 'processEntity' => fn($entity, $depth) => [ + 'ProjectId' => $entity->getId(), + 'ParentProjectId' => $entity->getParent()?->getId() ?? '', + 'Type' => 'project', + 'ProjectNameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(), + 'ProjectName' => $entity->getName(), + 'ProjectFullName' => $this->getFullName($entity), + 'BomQuantity' => '-', + 'BomPartId' => '-', + 'BomPartIpn' => '-', + 'BomPartMpnr' => '-', + 'BomPartName' => '-', + 'BomDesignator' => '-', + 'BomPartDescription' => '-', + 'BomMountNames' => '-', + ], + 'processBomEntries' => fn($entity, $depth) => array_map(fn(AssemblyBOMEntry $bomEntry) => [ + 'Id' => $entity->getId(), + 'ParentId' => '', + 'Type' => 'project_bom_entry', + 'ProjectNameHierarchical' => str_repeat('--', $depth) . '> ' . $entity->getName(), + 'ProjectName' => $entity->getName(), + 'ProjectFullName' => $this->getFullName($entity), + 'BomQuantity' => $bomEntry->getQuantity() ?? '', + 'BomPartId' => $bomEntry->getPart()?->getId() ?? '', + 'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '', + 'BomPartMpnr' => $bomEntry->getPart()?->getManufacturerProductNumber() ?? '', + 'BomPartName' => $bomEntry->getPart()?->getName() ?? '', + 'BomDesignator' => $bomEntry->getName() ?? '', + 'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '', + 'BomMountNames' => $bomEntry->getMountNames(), + ], $entity->getBomEntries()->toArray()), + ], + Assembly::class => [ + 'header' => [ + 'Id', 'ParentId', 'Type', 'AssemblyIpn', 'AssemblyNameHierarchical', 'AssemblyName', + 'AssemblyFullName', 'BomQuantity', 'BomMultiplier', 'BomPartId', 'BomPartIpn', 'BomPartMpnr', + 'BomPartName', 'BomDesignator', 'BomPartDescription', 'BomMountNames', 'BomReferencedAssemblyId', + 'BomReferencedAssemblyIpn', 'BomReferencedAssemblyFullName' + ], + 'processEntity' => fn($entity, $depth) => [ + 'Id' => $entity->getId(), + 'ParentId' => $entity->getParent()?->getId() ?? '', + 'Type' => 'assembly', + 'AssemblyIpn' => $entity->getIpn(), + 'AssemblyNameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(), + 'AssemblyName' => $entity->getName(), + 'AssemblyFullName' => $this->getFullName($entity), + 'BomQuantity' => '-', + 'BomMultiplier' => '-', + 'BomPartId' => '-', + 'BomPartIpn' => '-', + 'BomPartMpnr' => '-', + 'BomPartName' => '-', + 'BomDesignator' => '-', + 'BomPartDescription' => '-', + 'BomMountNames' => '-', + 'BomReferencedAssemblyId' => '-', + 'BomReferencedAssemblyIpn' => '-', + 'BomReferencedAssemblyFullName' => '-', + ], + 'processBomEntries' => fn($entity, $depth) => $this->processBomEntriesWithAggregatedParts($entity, $depth), + ], + Supplier::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Manufacturer::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + StorageLocation::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Footprint::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Currency::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + MeasurementUnit::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + LabelProfile::class => [ + 'header' => ['Id', 'SupportedElement', 'Name'], + 'processEntity' => fn(LabelProfile $entity, $depth) => [ + 'Id' => $entity->getId(), + 'SupportedElement' => $entity->getOptions()->getSupportedElement()->name, + 'Name' => $entity->getName(), + ], + ], + ]; + + //Get configuration for the entity type + $entityConfig = $config[$type] ?? null; + + if (!$entityConfig) { + return ''; + } + + //Initialize CSV data with the header + $csvData = []; + $csvData[] = $entityConfig['header']; + + $relevantEntities = $entities; + + if ($isHierarchical) { + //Filter root entities (those without parents) + $relevantEntities = array_filter($entities, fn($entity) => $entity->getParent() === null); + + if (count($relevantEntities) === 0 && count($entities) > 0) { + //If no root entities are found, then we need to add all entities + + $relevantEntities = $entities; + } + } + + //Sort root entities alphabetically by `name` + usort($relevantEntities, fn($a, $b) => strnatcasecmp($a->getName(), $b->getName())); + + //Recursive function to process an entity and its children + $processEntity = function ($entity, &$csvData, $depth = 0) use (&$processEntity, $entityConfig, $isHierarchical) { + //Add main entity data to CSV + $csvData[] = $entityConfig['processEntity']($entity, $depth); + + //Process BOM entries if applicable + if (isset($entityConfig['processBomEntries'])) { + $bomRows = $entityConfig['processBomEntries']($entity, $depth); + foreach ($bomRows as $bomRow) { + $csvData[] = $bomRow; + } + } + + if ($isHierarchical) { + //Retrieve children, sort alphabetically, then process them + $children = $entity->getChildren()->toArray(); + usort($children, fn($a, $b) => strnatcasecmp($a->getName(), $b->getName())); + foreach ($children as $childEntity) { + $processEntity($childEntity, $csvData, $depth + 1); + } + } + }; + + //Start processing with root entities + foreach ($relevantEntities as $rootEntity) { + $processEntity($rootEntity, $csvData); + } + + //Generate CSV string + $output = ''; + foreach ($csvData as $line) { + $output .= implode(';', $line) . "\n"; // Use a semicolon as the delimiter + } + + return $output; + } + + /** + * Process BOM entries and include aggregated parts as "complete_part_list". + * + * @param Assembly $assembly The assembly being processed. + * @param int $depth The current depth in the hierarchy. + * @return array Processed BOM entries and aggregated parts rows. + */ + private function processBomEntriesWithAggregatedParts(Assembly $assembly, int $depth): array + { + $rows = []; + + foreach ($assembly->getBomEntries() as $bomEntry) { + // Add the BOM entry itself + $rows[] = [ + 'Id' => $assembly->getId(), + 'ParentId' => '', + 'Type' => 'assembly_bom_entry', + 'AssemblyIpn' => $assembly->getIpn(), + 'AssemblyNameHierarchical' => str_repeat('--', $depth) . '> ' . $assembly->getName(), + 'AssemblyName' => $assembly->getName(), + 'AssemblyFullName' => $this->getFullName($assembly), + 'BomQuantity' => $bomEntry->getQuantity() ?? '', + 'BomMultiplier' => '', + 'BomPartId' => $bomEntry->getPart()?->getId() ?? '-', + 'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '-', + 'BomPartMpnr' => $bomEntry->getPart()?->getManufacturerProductNumber() ?? '-', + 'BomPartName' => $bomEntry->getPart()?->getName() ?? '-', + 'BomDesignator' => $bomEntry->getName() ?? '-', + 'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '-', + 'BomMountNames' => $bomEntry->getMountNames(), + 'BomReferencedAssemblyId' => $bomEntry->getReferencedAssembly()?->getId() ?? '-', + 'BomReferencedAssemblyIpn' => $bomEntry->getReferencedAssembly()?->getIpn() ?? '-', + 'BomReferencedAssemblyFullName' => $this->getFullName($bomEntry->getReferencedAssembly() ?? null), + ]; + + // If a referenced assembly exists, add aggregated parts + if ($bomEntry->getReferencedAssembly() instanceof Assembly) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + // Get aggregated parts for the referenced assembly + $aggregatedParts = $this->assemblyPartAggregator->getAggregatedParts($referencedAssembly, $bomEntry->getQuantity());; + + foreach ($aggregatedParts as $partData) { + $partAssembly = $partData['assembly'] ?? null; + + $rows[] = [ + 'Id' => $assembly->getId(), + 'ParentId' => '', + 'Type' => 'subassembly_part_list', + 'AssemblyIpn' => $partAssembly ? $partAssembly->getIpn() : '', + 'AssemblyNameHierarchical' => '', + 'AssemblyName' => $partAssembly ? $partAssembly->getName() : '', + 'AssemblyFullName' => $this->getFullName($partAssembly), + 'BomQuantity' => $partData['quantity'], + 'BomMultiplier' => $partData['multiplier'], + 'BomPartId' => $partData['part']?->getId(), + 'BomPartIpn' => $partData['part']?->getIpn(), + 'BomPartMpnr' => $partData['part']?->getManufacturerProductNumber(), + 'BomPartName' => $partData['part']?->getName(), + 'BomDesignator' => $partData['part']?->getName(), + 'BomPartDescription' => $partData['part']?->getDescription(), + 'BomMountNames' => '-', + 'BomReferencedAssemblyId' => '-', + 'BomReferencedAssemblyIpn' => '-', + 'BomReferencedAssemblyFullName' => '-', + ]; + } + } + } + + return $rows; + } + + /** + * Constructs the full hierarchical name of an object by traversing + * through its parent objects and concatenating their names using + * a specified separator. + * + * @param AttachmentType|Category|Project|Assembly|Supplier|Manufacturer|StorageLocation|Footprint|Currency|MeasurementUnit|LabelProfile|null $object The object whose full name is to be constructed. If null, the result will be an empty string. + * @param string $separator The string used to separate the names of the objects in the full hierarchy. + * + * @return string The full hierarchical name constructed by concatenating the names of the object and its parents. + */ + private function getFullName(AttachmentType|Category|Project|Assembly|Supplier|Manufacturer|StorageLocation|Footprint|Currency|MeasurementUnit|LabelProfile|null $object, string $separator = '->'): string + { + $fullNameParts = []; + + while ($object !== null) { + array_unshift($fullNameParts, $object->getName()); + $object = $object->getParent(); + } + + return implode($separator, $fullNameParts); + } } diff --git a/src/Services/ImportExportSystem/ImporterResult.php b/src/Services/ImportExportSystem/ImporterResult.php new file mode 100644 index 000000000..4e289d133 --- /dev/null +++ b/src/Services/ImportExportSystem/ImporterResult.php @@ -0,0 +1,60 @@ +bomEntries = $bomEntries; + $this->violations = new ConstraintViolationList(); + } + + /** + * Fügt einen neuen BOM-Eintrag hinzu. + */ + public function addBomEntry(object $bomEntry): void + { + $this->bomEntries[] = $bomEntry; + } + + /** + * Gibt alle BOM-Einträge zurück. + */ + public function getBomEntries(): array + { + return $this->bomEntries; + } + + /** + * Gibt die Liste der Violation zurück. + */ + public function getViolations(): ConstraintViolationList + { + return $this->violations; + } + + /** + * Fügt eine neue `ConstraintViolation` zur Liste hinzu. + */ + public function addViolation(ConstraintViolation $violation): void + { + $this->violations->add($violation); + } + + /** + * Prüft, ob die Liste der Violationen leer ist. + */ + public function hasViolations(): bool + { + return count($this->violations) > 0; + } +} \ No newline at end of file diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php index 1f842c23d..9e674f05c 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php @@ -29,6 +29,7 @@ use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use Doctrine\ORM\EntityManagerInterface; @@ -148,6 +149,26 @@ public function importPartUnits(array $data): int return is_countable($partunit_data) ? count($partunit_data) : 0; } + public function importPartCustomStates(array $data): int + { + if (!isset($data['partcustomstate'])) { + throw new \RuntimeException('$data must contain a "partcustomstate" key!'); + } + + $partCustomStateData = $data['partcustomstate']; + foreach ($partCustomStateData as $partCustomState) { + $customState = new PartCustomState(); + $customState->setName($partCustomState['name']); + + $this->setIDOfEntity($customState, $partCustomState['id']); + $this->em->persist($customState); + } + + $this->em->flush(); + + return is_countable($partCustomStateData) ? count($partCustomStateData) : 0; + } + public function importCategories(array $data): int { if (!isset($data['partcategory'])) { diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php index 80c2dbf77..ab06a1346 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php @@ -91,6 +91,8 @@ public function importParts(array $data): int $this->setAssociationField($entity, 'partUnit', MeasurementUnit::class, $part['partUnit_id']); } + $this->setAssociationField($entity, 'partCustomState', MeasurementUnit::class, $part['partCustomState_id']); + //Create a part lot to store the stock level and location $lot = new PartLot(); $lot->setAmount((float) ($part['stockLevel'] ?? 0)); diff --git a/src/Services/LabelSystem/SandboxedTwigFactory.php b/src/Services/LabelSystem/SandboxedTwigFactory.php index d6ea69685..d5e09fa54 100644 --- a/src/Services/LabelSystem/SandboxedTwigFactory.php +++ b/src/Services/LabelSystem/SandboxedTwigFactory.php @@ -133,7 +133,7 @@ final class SandboxedTwigFactory Supplier::class => ['getShippingCosts', 'getDefaultCurrency'], Part::class => ['isNeedsReview', 'getTags', 'getMass', 'getIpn', 'getProviderReference', 'getDescription', 'getComment', 'isFavorite', 'getCategory', 'getFootprint', - 'getPartLots', 'getPartUnit', 'useFloatAmount', 'getMinAmount', 'getAmountSum', 'isNotEnoughInstock', 'isAmountUnknown', 'getExpiredAmountSum', + 'getPartLots', 'getPartUnit', 'getPartCustomState', 'useFloatAmount', 'getMinAmount', 'getAmountSum', 'isNotEnoughInstock', 'isAmountUnknown', 'getExpiredAmountSum', 'getManufacturerProductUrl', 'getCustomProductURL', 'getManufacturingStatus', 'getManufacturer', 'getManufacturerProductNumber', 'getOrderdetails', 'isObsolete', 'getParameters', 'getGroupedParameters', diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index f7a9d1c40..26c3295af 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -22,6 +22,7 @@ namespace App\Services\Trees; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\AttachmentType; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; @@ -29,6 +30,7 @@ use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; @@ -49,8 +51,15 @@ */ class ToolsTreeBuilder { - public function __construct(protected TranslatorInterface $translator, protected UrlGeneratorInterface $urlGenerator, protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator, protected Security $security) - { + public function __construct( + protected TranslatorInterface $translator, + protected UrlGeneratorInterface $urlGenerator, + protected TagAwareCacheInterface $cache, + protected UserCacheKeyGenerator $keyGenerator, + protected Security $security, + protected ?array $dataSourceSynonyms = [], + ) { + $this->dataSourceSynonyms = $dataSourceSynonyms ?? []; } /** @@ -160,37 +169,43 @@ protected function getEditNodes(): array } if ($this->security->isGranted('read', new Category())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.categories'), + $this->getTranslatedDataSourceOrSynonym('category', 'tree.tools.edit.categories', $this->translator->getLocale()), $this->urlGenerator->generate('category_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-tags'); } if ($this->security->isGranted('read', new Project())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.projects'), + $this->getTranslatedDataSourceOrSynonym('project', 'tree.tools.edit.projects', $this->translator->getLocale()), $this->urlGenerator->generate('project_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-archive'); } + if ($this->security->isGranted('read', new Assembly())) { + $nodes[] = (new TreeViewNode( + $this->getTranslatedDataSourceOrSynonym('assembly', 'tree.tools.edit.assemblies', $this->translator->getLocale()), + $this->urlGenerator->generate('assembly_new') + ))->setIcon('fa-fw fa-treeview fa-solid fa-list'); + } if ($this->security->isGranted('read', new Supplier())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.suppliers'), + $this->getTranslatedDataSourceOrSynonym('supplier', 'tree.tools.edit.suppliers', $this->translator->getLocale()), $this->urlGenerator->generate('supplier_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-truck'); } if ($this->security->isGranted('read', new Manufacturer())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.manufacturer'), + $this->getTranslatedDataSourceOrSynonym('manufacturer', 'tree.tools.edit.manufacturer', $this->translator->getLocale()), $this->urlGenerator->generate('manufacturer_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-industry'); } if ($this->security->isGranted('read', new StorageLocation())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.storelocation'), + $this->getTranslatedDataSourceOrSynonym('storagelocation', 'tree.tools.edit.storelocation', $this->translator->getLocale()), $this->urlGenerator->generate('store_location_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-cube'); } if ($this->security->isGranted('read', new Footprint())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.footprint'), + $this->getTranslatedDataSourceOrSynonym('footprint', 'tree.tools.edit.footprint', $this->translator->getLocale()), $this->urlGenerator->generate('footprint_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-microchip'); } @@ -212,6 +227,12 @@ protected function getEditNodes(): array $this->urlGenerator->generate('label_profile_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-qrcode'); } + if ($this->security->isGranted('read', new PartCustomState())) { + $nodes[] = (new TreeViewNode( + $this->translator->trans('tree.tools.edit.part_custom_state'), + $this->urlGenerator->generate('part_custom_state_new') + ))->setIcon('fa-fw fa-treeview fa-solid fa-tools'); + } if ($this->security->isGranted('create', new Part())) { $nodes[] = (new TreeViewNode( $this->translator->trans('tree.tools.edit.part'), @@ -298,4 +319,22 @@ protected function getSystemNodes(): array return $nodes; } + + protected function getTranslatedDataSourceOrSynonym(string $dataSource, string $translationKey, string $locale): string + { + $currentTranslation = $this->translator->trans($translationKey); + + // Call alternatives from DataSourcesynonyms (if available) + if (!empty($this->dataSourceSynonyms[$dataSource][$locale])) { + $alternativeTranslation = $this->dataSourceSynonyms[$dataSource][$locale]; + + // Use alternative translation when it deviates from the standard translation + if ($alternativeTranslation !== $currentTranslation) { + return $alternativeTranslation; + } + } + + // Otherwise return the standard translation + return $currentTranslation; + } } diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index 73ffa5baf..d5358bfa2 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -22,6 +22,7 @@ namespace App\Services\Trees; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; @@ -67,9 +68,11 @@ public function __construct( protected TranslatorInterface $translator, private readonly UrlGeneratorInterface $router, private readonly SidebarSettings $sidebarSettings, + protected ?array $dataSourceSynonyms = [], ) { $this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled; $this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded; + $this->dataSourceSynonyms = $dataSourceSynonyms ?? []; } /** @@ -154,6 +157,10 @@ private function getTreeViewUncached( $href_type = 'list_parts'; } + if ($mode === 'assemblies') { + $href_type = 'list_parts'; + } + $generic = $this->getGenericTree($class, $parent); $treeIterator = new TreeViewNodeIterator($generic); $recursiveIterator = new RecursiveIteratorIterator($treeIterator, RecursiveIteratorIterator::SELF_FIRST); @@ -183,6 +190,15 @@ private function getTreeViewUncached( $root_node->setExpanded($this->rootNodeExpandedByDefault); $root_node->setIcon($this->entityClassToRootNodeIcon($class)); + $generic = [$root_node]; + } elseif ($mode === 'assemblies' && $this->rootNodeEnabled) { + //We show the root node as a link to the list of all assemblies + $show_all_parts_url = $this->router->generate('assemblies_list'); + + $root_node = new TreeViewNode($this->entityClassToRootNodeString($class), $show_all_parts_url, $generic); + $root_node->setExpanded($this->rootNodeExpandedByDefault); + $root_node->setIcon($this->entityClassToRootNodeIcon($class)); + $generic = [$root_node]; } @@ -212,13 +228,16 @@ protected function entityClassToRootNodeHref(string $class): ?string protected function entityClassToRootNodeString(string $class): string { + $locale = $this->translator->getLocale(); + return match ($class) { - Category::class => $this->translator->trans('category.labelp'), - StorageLocation::class => $this->translator->trans('storelocation.labelp'), - Footprint::class => $this->translator->trans('footprint.labelp'), - Manufacturer::class => $this->translator->trans('manufacturer.labelp'), - Supplier::class => $this->translator->trans('supplier.labelp'), - Project::class => $this->translator->trans('project.labelp'), + Category::class => $this->getTranslatedOrSynonym('category', $locale), + StorageLocation::class => $this->getTranslatedOrSynonym('storelocation', $locale), + Footprint::class => $this->getTranslatedOrSynonym('footprint', $locale), + Manufacturer::class => $this->getTranslatedOrSynonym('manufacturer', $locale), + Supplier::class => $this->getTranslatedOrSynonym('supplier', $locale), + Project::class => $this->getTranslatedOrSynonym('project', $locale), + Assembly::class => $this->getTranslatedOrSynonym('assembly', $locale), default => $this->translator->trans('tree.root_node.text'), }; } @@ -233,6 +252,7 @@ protected function entityClassToRootNodeIcon(string $class): ?string Manufacturer::class => $icon.'fa-industry', Supplier::class => $icon.'fa-truck', Project::class => $icon.'fa-archive', + Assembly::class => $icon.'fa-list', default => null, }; } @@ -274,4 +294,22 @@ public function getGenericTree(string $class, ?AbstractStructuralDBElement $pare return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line }); } + + protected function getTranslatedOrSynonym(string $key, string $locale): string + { + $currentTranslation = $this->translator->trans($key . '.labelp'); + + // Call alternatives from DataSourcesynonyms (if available) + if (!empty($this->dataSourceSynonyms[$key][$locale])) { + $alternativeTranslation = $this->dataSourceSynonyms[$key][$locale]; + + // Use alternative translation when it deviates from the standard translation + if ($alternativeTranslation !== $currentTranslation) { + return $alternativeTranslation; + } + } + + // Otherwise return the standard translation + return $currentTranslation; + } } diff --git a/src/Services/UserSystem/PermissionPresetsHelper.php b/src/Services/UserSystem/PermissionPresetsHelper.php index 554da8b32..a63013886 100644 --- a/src/Services/UserSystem/PermissionPresetsHelper.php +++ b/src/Services/UserSystem/PermissionPresetsHelper.php @@ -102,6 +102,7 @@ private function admin(HasPermissionsInterface $perm_holder): void $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'attachment_types', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'currencies', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'measurement_units', PermissionData::ALLOW); + $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'part_custom_states', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'suppliers', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'projects', PermissionData::ALLOW); diff --git a/src/Settings/BehaviorSettings/AssemblyBomTableColumns.php b/src/Settings/BehaviorSettings/AssemblyBomTableColumns.php new file mode 100644 index 000000000..da8557c2d --- /dev/null +++ b/src/Settings/BehaviorSettings/AssemblyBomTableColumns.php @@ -0,0 +1,46 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\BehaviorSettings; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +enum AssemblyBomTableColumns : string implements TranslatableInterface +{ + + case NAME = "name"; + case ID = "id"; + case QUANTITY = "quantity"; + case IPN = "ipn"; + case DESCRIPTION = "description"; + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + $key = match($this) { + default => 'assembly.bom.table.' . $this->value, + }; + + return $translator->trans($key, locale: $locale); + } +} diff --git a/src/Settings/BehaviorSettings/AssemblyTableColumns.php b/src/Settings/BehaviorSettings/AssemblyTableColumns.php new file mode 100644 index 000000000..02c315b49 --- /dev/null +++ b/src/Settings/BehaviorSettings/AssemblyTableColumns.php @@ -0,0 +1,49 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\BehaviorSettings; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +enum AssemblyTableColumns : string implements TranslatableInterface +{ + + case NAME = "name"; + case ID = "id"; + case IPN = "ipn"; + case DESCRIPTION = "description"; + case REFERENCED_ASSEMBLIES = "referencedAssemblies"; + case ADDED_DATE = "addedDate"; + case LAST_MODIFIED = "lastModified"; + case EDIT = "edit"; + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + $key = match($this) { + default => 'assembly.table.' . $this->value, + }; + + return $translator->trans($key, locale: $locale); + } +} diff --git a/src/Settings/BehaviorSettings/TableSettings.php b/src/Settings/BehaviorSettings/TableSettings.php index b69648769..5c7455e25 100644 --- a/src/Settings/BehaviorSettings/TableSettings.php +++ b/src/Settings/BehaviorSettings/TableSettings.php @@ -53,7 +53,6 @@ class TableSettings )] public int $fullDefaultPageSize = 50; - /** @var PartTableColumns[] */ #[SettingsParameter(ArrayType::class, label: new TM("settings.behavior.table.parts_default_columns"), @@ -70,6 +69,37 @@ class TableSettings PartTableColumns::CATEGORY, PartTableColumns::FOOTPRINT, PartTableColumns::MANUFACTURER, PartTableColumns::LOCATION, PartTableColumns::AMOUNT]; + /** @var AssemblyTableColumns[] */ + #[SettingsParameter(ArrayType::class, + label: new TM("settings.behavior.table.assemblies_default_columns"), + description: new TM("settings.behavior.table.assemblies_default_columns.help"), + options: ['type' => EnumType::class, 'options' => ['class' => AssemblyTableColumns::class]], + formType: \Symfony\Component\Form\Extension\Core\Type\EnumType::class, + formOptions: ['class' => AssemblyTableColumns::class, 'multiple' => true, 'ordered' => true], + envVar: "TABLE_ASSEMBLIES_DEFAULT_COLUMNS", envVarMode: EnvVarMode::OVERWRITE, envVarMapper: [self::class, 'mapAssembliesDefaultColumnsEnv'] + )] + #[Assert\NotBlank()] + #[Assert\Unique()] + #[Assert\All([new Assert\Type(AssemblyTableColumns::class)])] + public array $assembliesDefaultColumns = [AssemblyTableColumns::ID, AssemblyTableColumns::IPN, AssemblyTableColumns::NAME, + AssemblyTableColumns::DESCRIPTION, AssemblyTableColumns::REFERENCED_ASSEMBLIES, AssemblyTableColumns::EDIT]; + + /** @var AssemblyBomTableColumns[] */ + #[SettingsParameter(ArrayType::class, + label: new TM("settings.behavior.table.assemblies_bom_default_columns"), + description: new TM("settings.behavior.table.assemblies_bom_default_columns.help"), + options: ['type' => EnumType::class, 'options' => ['class' => AssemblyBomTableColumns::class]], + formType: \Symfony\Component\Form\Extension\Core\Type\EnumType::class, + formOptions: ['class' => AssemblyBomTableColumns::class, 'multiple' => true, 'ordered' => true], + envVar: "TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS", envVarMode: EnvVarMode::OVERWRITE, envVarMapper: [self::class, 'mapAssemblyBomsDefaultColumnsEnv'] + )] + #[Assert\NotBlank()] + #[Assert\Unique()] + #[Assert\All([new Assert\Type(AssemblyBomTableColumns::class)])] + + public array $assembliesBomDefaultColumns = [AssemblyBomTableColumns::QUANTITY, AssemblyTableColumns::ID, AssemblyTableColumns::IPN, + AssemblyTableColumns::NAME, AssemblyTableColumns::DESCRIPTION]; + #[SettingsParameter(label: new TM("settings.behavior.table.preview_image_min_width"), formOptions: ['attr' => ['min' => 1, 'max' => 100]], envVar: "int:TABLE_IMAGE_PREVIEW_MIN_SIZE", envVarMode: EnvVarMode::OVERWRITE @@ -101,4 +131,36 @@ public static function mapPartsDefaultColumnsEnv(string $columns): array return $ret; } + public static function mapAssembliesDefaultColumnsEnv(string $columns): array + { + $exploded = explode(',', $columns); + $ret = []; + foreach ($exploded as $column) { + $enum = AssemblyTableColumns::tryFrom($column); + if (!$enum) { + throw new \InvalidArgumentException("Invalid column '$column' in TABLE_ASSEMBLIES_DEFAULT_COLUMNS"); + } + + $ret[] = $enum; + } + + return $ret; + } + + public static function mapAssemblyBomsDefaultColumnsEnv(string $columns): array + { + $exploded = explode(',', $columns); + $ret = []; + foreach ($exploded as $column) { + $enum = AssemblyBomTableColumns::tryFrom($column); + if (!$enum) { + throw new \InvalidArgumentException("Invalid column '$column' in TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS"); + } + + $ret[] = $enum; + } + + return $ret; + } + } diff --git a/src/Settings/MiscSettings/AssemblySettings.php b/src/Settings/MiscSettings/AssemblySettings.php new file mode 100644 index 000000000..82fb26b66 --- /dev/null +++ b/src/Settings/MiscSettings/AssemblySettings.php @@ -0,0 +1,45 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\MiscSettings; + +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Translation\TranslatableMessage as TM; + +#[Settings(label: new TM("settings.misc.assembly"))] +#[SettingsIcon("fa-list")] +class AssemblySettings +{ + use SettingsTrait; + + #[SettingsParameter( + label: new TM("settings.misc.assembly.useIpnPlaceholderInName"), + envVar: "bool:CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME", envVarMode: EnvVarMode::OVERWRITE, + )] + public bool $useIpnPlaceholderInName = true; +} diff --git a/src/Settings/MiscSettings/MiscSettings.php b/src/Settings/MiscSettings/MiscSettings.php index b8a3a73f6..68d0cf6d1 100644 --- a/src/Settings/MiscSettings/MiscSettings.php +++ b/src/Settings/MiscSettings/MiscSettings.php @@ -34,4 +34,7 @@ class MiscSettings #[EmbeddedSettings] public ?ExchangeRateSettings $exchangeRate = null; -} \ No newline at end of file + + #[EmbeddedSettings] + public ?AssemblySettings $assembly = null; +} diff --git a/src/Twig/DataSourceNameExtension.php b/src/Twig/DataSourceNameExtension.php new file mode 100644 index 000000000..1c02243f7 --- /dev/null +++ b/src/Twig/DataSourceNameExtension.php @@ -0,0 +1,42 @@ +translator = $translator; + $this->dataSourceSynonyms = $dataSourceSynonyms ?? []; + } + + public function getFunctions(): array + { + return [ + new TwigFunction('get_data_source_name', [$this, 'getDataSourceName']), + ]; + } + + /** + * Based on the locale and data source names, gives the right synonym value back or the default translator value. + */ + public function getDataSourceName(string $dataSourceName, string $defaultKey): string + { + $locale = $this->translator->getLocale(); + + // Use alternative dataSource synonym (if available) + if (isset($this->dataSourceSynonyms[$dataSourceName][$locale])) { + return $this->dataSourceSynonyms[$dataSourceName][$locale]; + } + + // Otherwise return the standard translation + return $this->translator->trans($defaultKey); + } +} \ No newline at end of file diff --git a/src/Twig/EntityExtension.php b/src/Twig/EntityExtension.php index 762ebb094..b757d75e8 100644 --- a/src/Twig/EntityExtension.php +++ b/src/Twig/EntityExtension.php @@ -22,8 +22,10 @@ */ namespace App\Twig; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Base\AbstractDBElement; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; @@ -108,6 +110,7 @@ public function getEntityType(object $entity): ?string Manufacturer::class => 'manufacturer', Category::class => 'category', Project::class => 'device', + Assembly::class => 'assembly', Attachment::class => 'attachment', Supplier::class => 'supplier', User::class => 'user', @@ -115,6 +118,7 @@ public function getEntityType(object $entity): ?string Currency::class => 'currency', MeasurementUnit::class => 'measurement_unit', LabelProfile::class => 'label_profile', + PartCustomState::class => 'part_custom_state', ]; foreach ($map as $class => $type) { diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyCycle.php b/src/Validator/Constraints/AssemblySystem/AssemblyCycle.php new file mode 100644 index 000000000..9d79b879c --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/AssemblyCycle.php @@ -0,0 +1,39 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use Symfony\Component\Validator\Constraint; + +/** + * This constraint checks that there is no cycle in bom configuration of the assembly + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class AssemblyCycle extends Constraint +{ + public string $message = 'assembly.bom_entry.assembly_cycle'; + + public function validatedBy(): string + { + return AssemblyCycleValidator::class; + } +} \ No newline at end of file diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php new file mode 100644 index 000000000..f12f19a73 --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php @@ -0,0 +1,169 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use Symfony\Component\Form\Form; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; +use ReflectionClass; + +/** + * Validator class to check for cycles in assemblies based on BOM entries. + * + * This validator ensures that the structure of assemblies does not contain circular dependencies + * by validating each entry in the Bill of Materials (BOM) of the given assembly. Additionally, + * it can handle form-submitted BOM entries to include these in the validation process. + */ +class AssemblyCycleValidator extends ConstraintValidator +{ + public function validate($value, Constraint $constraint): void + { + if (!$constraint instanceof AssemblyCycle) { + throw new UnexpectedTypeException($constraint, AssemblyCycle::class); + } + + if (!$value instanceof Assembly) { + return; + } + + $availableViolations = $this->context->getViolations(); + if (count($availableViolations) > 0) { + //already violations given, currently no more needed to check + + return; + } + + $bomEntries = []; + + if ($this->context->getRoot() instanceof Form && $this->context->getRoot()->has('bom_entries')) { + $bomEntries = $this->context->getRoot()->get('bom_entries')->getData(); + $bomEntries = is_array($bomEntries) ? $bomEntries : iterator_to_array($bomEntries); + } elseif ($this->context->getRoot() instanceof Assembly) { + $bomEntries = $value->getBomEntries()->toArray(); + } + + $relevantEntries = []; + + foreach ($bomEntries as $bomEntry) { + if ($bomEntry->getReferencedAssembly() !== null) { + $relevantEntries[$bomEntry->getId()] = $bomEntry; + } + } + + $visitedAssemblies = []; + foreach ($relevantEntries as $bomEntry) { + if ($this->hasCycle($bomEntry->getReferencedAssembly(), $value, $visitedAssemblies)) { + $this->addViolation($value, $constraint); + } + } + } + + /** + * Determines if there is a cyclic dependency in the assembly hierarchy. + * + * This method checks if a cycle exists in the hierarchy of referenced assemblies starting + * from a given assembly. It traverses through the Bill of Materials (BOM) entries of each + * assembly recursively and keeps track of visited assemblies to detect cycles. + * + * @param Assembly|null $currentAssembly The current assembly being checked for cycles. + * @param Assembly $originalAssembly The original assembly from where the cycle detection started. + * @param Assembly[] $visitedAssemblies A list of assemblies that have been visited during the current traversal. + * + * @return bool True if a cycle is detected, false otherwise. + */ + private function hasCycle(?Assembly $currentAssembly, Assembly $originalAssembly, array $visitedAssemblies = []): bool + { + //No referenced assembly → no cycle + if ($currentAssembly === null) { + return false; + } + + //If the assembly has already been visited, there is a cycle + if (in_array($currentAssembly->getId(), array_map(fn($a) => $a->getId(), $visitedAssemblies), true)) { + return true; + } + + //Add the current assembly to the visited + $visitedAssemblies[] = $currentAssembly; + + //Go through the bom entries of the current assembly + foreach ($currentAssembly->getBomEntries() as $bomEntry) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + if ($referencedAssembly !== null && $this->hasCycle($referencedAssembly, $originalAssembly, $visitedAssemblies)) { + return true; + } + } + + //Remove the current assembly from the list of visit (recursion completed) + array_pop($visitedAssemblies); + + return false; + } + + /** + * Adds a violation to the current context if it hasn’t already been added. + * + * This method checks whether a violation with the same property path as the current violation + * already exists in the context. If such a violation is found, the current violation is not added again. + * The process involves reflection to access private or protected properties of violation objects. + * + * @param mixed $value The value that triggered the violation. + * @param Constraint $constraint The constraint containing the validation details. + * + */ + private function addViolation(mixed $value, Constraint $constraint): void + { + /** @var ConstraintViolationBuilder $buildViolation */ + $buildViolation = $this->context->buildViolation($constraint->message) + ->setParameter('%name%', $value->getName()); + + $alreadyAdded = false; + + try { + $reflectionClass = new ReflectionClass($buildViolation); + $property = $reflectionClass->getProperty('propertyPath'); + $propertyPath = $property->getValue($buildViolation); + + $availableViolations = $this->context->getViolations(); + + foreach ($availableViolations as $tmpViolation) { + $tmpReflectionClass = new ReflectionClass($tmpViolation); + $tmpProperty = $tmpReflectionClass->getProperty('propertyPath'); + $tmpPropertyPath = $tmpProperty->getValue($tmpViolation); + + if ($tmpPropertyPath === $propertyPath) { + $alreadyAdded = true; + } + } + } catch (\ReflectionException) { + } + + if (!$alreadyAdded) { + $buildViolation->addViolation(); + } + } +} \ No newline at end of file diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php b/src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php new file mode 100644 index 000000000..73234c86e --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php @@ -0,0 +1,21 @@ +context->getViolations(); + if (count($availableViolations) > 0) { + //already violations given, currently no more needed to check + + return; + } + + $bomEntries = []; + + if ($this->context->getRoot() instanceof Form && $this->context->getRoot()->has('bom_entries')) { + $bomEntries = $this->context->getRoot()->get('bom_entries')->getData(); + $bomEntries = is_array($bomEntries) ? $bomEntries : iterator_to_array($bomEntries); + } elseif ($this->context->getRoot() instanceof Assembly) { + $bomEntries = $value->getBomEntries()->toArray(); + } + + $relevantEntries = []; + + foreach ($bomEntries as $bomEntry) { + if ($bomEntry->getReferencedAssembly() !== null) { + $relevantEntries[$bomEntry->getId()] = $bomEntry; + } + } + + foreach ($relevantEntries as $bomEntry) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + if ($bomEntry->getAssembly()->getParent()?->getId() === $referencedAssembly->getParent()?->getId()) { + //Save on the same assembly level + continue; + } elseif ($this->isInvalidBomEntry($referencedAssembly, $bomEntry->getAssembly())) { + $this->addViolation($value, $constraint); + } + } + } + + /** + * Determines whether a Bill of Materials (BOM) entry is invalid based on the relationship + * between the current assembly and the parent assembly. + * + * @param Assembly|null $currentAssembly The current assembly being analyzed. Null indicates no assembly is referenced. + * @param Assembly $parentAssembly The parent assembly to check against the current assembly. + * + * @return bool Returns + */ + private function isInvalidBomEntry(?Assembly $currentAssembly, Assembly $parentAssembly): bool + { + //No assembly referenced -> no problems + if ($currentAssembly === null) { + return false; + } + + //Check: is the current assembly a descendant of the parent assembly? + if ($currentAssembly->isChildOf($parentAssembly)) { + return true; + } + + //Recursive check: Analyze the current assembly list + foreach ($currentAssembly->getBomEntries() as $bomEntry) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + if ($this->isInvalidBomEntry($referencedAssembly, $parentAssembly)) { + return true; + } + } + + return false; + + } + + private function isOnSameLevel(Assembly $assembly1, Assembly $assembly2): bool + { + $parent1 = $assembly1->getParent(); + $parent2 = $assembly2->getParent(); + + if ($parent1 === null || $parent2 === null) { + return false; + } + + // Beide Assemblies teilen denselben Parent + return $parent1 !== null && $parent2 !== null && $parent1->getId() === $parent2->getId(); + } + + /** + * Adds a violation to the current context if it hasn’t already been added. + * + * This method checks whether a violation with the same property path as the current violation + * already exists in the context. If such a violation is found, the current violation is not added again. + * The process involves reflection to access private or protected properties of violation objects. + * + * @param mixed $value The value that triggered the violation. + * @param Constraint $constraint The constraint containing the validation details. + * + */ + private function addViolation($value, Constraint $constraint): void + { + /** @var ConstraintViolationBuilder $buildViolation */ + $buildViolation = $this->context->buildViolation($constraint->message) + ->setParameter('%name%', $value->getName()); + + $alreadyAdded = false; + + try { + $reflectionClass = new ReflectionClass($buildViolation); + $property = $reflectionClass->getProperty('propertyPath'); + $propertyPath = $property->getValue($buildViolation); + + $availableViolations = $this->context->getViolations(); + + foreach ($availableViolations as $tmpViolation) { + $tmpReflectionClass = new ReflectionClass($tmpViolation); + $tmpProperty = $tmpReflectionClass->getProperty('propertyPath'); + $tmpPropertyPath = $tmpProperty->getValue($tmpViolation); + + if ($tmpPropertyPath === $propertyPath) { + $alreadyAdded = true; + } + } + } catch (\ReflectionException) { + } + + if (!$alreadyAdded) { + $buildViolation->addViolation(); + } + } +} \ No newline at end of file diff --git a/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssembly.php b/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssembly.php new file mode 100644 index 000000000..55a31440a --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssembly.php @@ -0,0 +1,34 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use Symfony\Component\Validator\Constraint; + +/** + * This constraint checks that the given UniqueReferencedAssembly is valid. + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class UniqueReferencedAssembly extends Constraint +{ + public string $message = 'assembly.bom_entry.assembly_already_in_bom'; +} \ No newline at end of file diff --git a/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssemblyValidator.php b/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssemblyValidator.php new file mode 100644 index 000000000..0e58c0663 --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/UniqueReferencedAssemblyValidator.php @@ -0,0 +1,48 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; + +class UniqueReferencedAssemblyValidator extends ConstraintValidator +{ + public function validate($value, Constraint $constraint) + { + $assemblies = []; + foreach ($value as $entry) { + $referencedAssemblyId = $entry->getReferencedAssembly()?->getId(); + if ($referencedAssemblyId === null) { + continue; + } + + if (isset($assemblies[$referencedAssemblyId])) { + $this->context->buildViolation($constraint->message) + ->atPath('referencedAssembly') + ->addViolation(); + return; + } + $assemblies[$referencedAssemblyId] = true; + } + } +} \ No newline at end of file diff --git a/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequest.php b/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequest.php new file mode 100644 index 000000000..dd3bc19ee --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequest.php @@ -0,0 +1,37 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use Symfony\Component\Validator\Constraint; + +/** + * This constraint checks that the given ValidAssemblyBuildRequest is valid. + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class ValidAssemblyBuildRequest extends Constraint +{ + public function getTargets(): string + { + return self::CLASS_CONSTRAINT; + } +} diff --git a/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequestValidator.php b/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequestValidator.php new file mode 100644 index 000000000..9d8c2e56a --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/ValidAssemblyBuildRequestValidator.php @@ -0,0 +1,84 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use App\Entity\Parts\PartLot; +use App\Helpers\Assemblies\AssemblyBuildRequest; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; + +class ValidAssemblyBuildRequestValidator extends ConstraintValidator +{ + private function buildViolationForLot(PartLot $partLot, string $message): ConstraintViolationBuilderInterface + { + return $this->context->buildViolation($message) + ->atPath('lot_' . $partLot->getID()) + ->setParameter('{{ lot }}', $partLot->getName()); + } + + public function validate($value, Constraint $constraint): void + { + if (!$constraint instanceof ValidAssemblyBuildRequest) { + throw new UnexpectedTypeException($constraint, ValidAssemblyBuildRequest::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!$value instanceof AssemblyBuildRequest) { + throw new UnexpectedTypeException($value, AssemblyBuildRequest::class); + } + + foreach ($value->getPartBomEntries() as $bom_entry) { + $withdraw_sum = $value->getWithdrawAmountSum($bom_entry); + $needed_amount = $value->getNeededAmountForBOMEntry($bom_entry); + + foreach ($value->getPartLotsForBOMEntry($bom_entry) as $lot) { + $withdraw_amount = $value->getLotWithdrawAmount($lot); + + if ($withdraw_amount < 0) { + $this->buildViolationForLot($lot, 'validator.assembly_build.lot_must_not_smaller_0') + ->addViolation(); + } + + if ($withdraw_amount > $lot->getAmount()) { + $this->buildViolationForLot($lot, 'validator.assembly_build.lot_must_not_bigger_than_stock') + ->addViolation(); + } + + if ($withdraw_sum > $needed_amount && $value->isDontCheckQuantity() === false) { + $this->buildViolationForLot($lot, 'validator.assembly_build.lot_bigger_than_needed') + ->addViolation(); + } + + if ($withdraw_sum < $needed_amount && $value->isDontCheckQuantity() === false) { + $this->buildViolationForLot($lot, 'validator.assembly_build.lot_smaller_than_needed') + ->addViolation(); + } + } + } + } +} diff --git a/src/Validator/Constraints/UniquePartIpnConstraint.php b/src/Validator/Constraints/UniquePartIpnConstraint.php new file mode 100644 index 000000000..13fd0330f --- /dev/null +++ b/src/Validator/Constraints/UniquePartIpnConstraint.php @@ -0,0 +1,20 @@ +entityManager = $entityManager; + $this->enforceUniqueIpn = $enforceUniqueIpn; + } + + public function validate($value, Constraint $constraint) + { + if (null === $value || '' === $value) { + return; + } + + if (!$this->enforceUniqueIpn) { + return; + } + + /** @var Part $currentPart */ + $currentPart = $this->context->getObject(); + + if (!$currentPart instanceof Part) { + return; + } + + $repository = $this->entityManager->getRepository(Part::class); + $existingParts = $repository->findBy(['ipn' => $value]); + + foreach ($existingParts as $existingPart) { + if ($currentPart->getId() !== $existingPart->getId()) { + if ($this->enforceUniqueIpn) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $value) + ->addViolation(); + } + } + } + } +} \ No newline at end of file diff --git a/templates/admin/_export_form.html.twig b/templates/admin/_export_form.html.twig index 07b00d43c..b02d4a8e8 100644 --- a/templates/admin/_export_form.html.twig +++ b/templates/admin/_export_form.html.twig @@ -1,6 +1,6 @@ -
    + -
    +
    @@ -23,7 +23,7 @@
    -
    +
    @@ -34,9 +34,32 @@
    + {% if path is defined and 'assembly' in path %} +
    + +
    + +
    +
    + {% else %} +
    + +
    + + +
    +
    + {% endif %} +
    - \ No newline at end of file + diff --git a/templates/admin/assembly_admin.html.twig b/templates/admin/assembly_admin.html.twig new file mode 100644 index 000000000..2e68a3da9 --- /dev/null +++ b/templates/admin/assembly_admin.html.twig @@ -0,0 +1,50 @@ +{% extends "admin/base_admin.html.twig" %} + +{# @var entity App\Entity\AssemblySystem\Assembly #} + +{% block card_title %} + {% set dataSourceName = get_data_source_name('assembly', 'assembly.caption') %} + {% set translatedSource = 'assembly.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} +{% endblock %} + +{% block edit_title %} + {% 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) }} + {{ form_row(form.ipn) }} +{% 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 %} diff --git a/templates/admin/base_admin.html.twig b/templates/admin/base_admin.html.twig index 51790c3c1..e9fc0fb99 100644 --- a/templates/admin/base_admin.html.twig +++ b/templates/admin/base_admin.html.twig @@ -86,7 +86,7 @@ - {% if entity.parameters is defined %} + {% if entity.parameters is defined and showParameters == true %} diff --git a/templates/admin/category_admin.html.twig b/templates/admin/category_admin.html.twig index 5811640b9..82089a283 100644 --- a/templates/admin/category_admin.html.twig +++ b/templates/admin/category_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}category.labelp{% endtrans %} + {% set dataSourceName = get_data_source_name('category', 'category.labelp') %} + {% set translatedSource = 'category.labelp'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block additional_pills %} @@ -31,6 +33,7 @@
    {{ form_row(form.partname_regex) }} {{ form_row(form.partname_hint) }} + {{ form_row(form.part_ipn_prefix) }}
    {{ form_row(form.default_description) }} {{ form_row(form.default_comment) }} diff --git a/templates/admin/footprint_admin.html.twig b/templates/admin/footprint_admin.html.twig index a2c3e4afd..a6acbe84e 100644 --- a/templates/admin/footprint_admin.html.twig +++ b/templates/admin/footprint_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}footprint.labelp{% endtrans %} + {% set dataSourceName = get_data_source_name('footprint', 'footprint.labelp') %} + {% set translatedSource = 'footprint.labelp'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block master_picture_block %} diff --git a/templates/admin/manufacturer_admin.html.twig b/templates/admin/manufacturer_admin.html.twig index 5db892c04..3ce9a124c 100644 --- a/templates/admin/manufacturer_admin.html.twig +++ b/templates/admin/manufacturer_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - {% trans %}manufacturer.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('manufacturer', 'manufacturer.caption') %} + {% set translatedSource = 'manufacturer.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block edit_title %} diff --git a/templates/admin/part_custom_state_admin.html.twig b/templates/admin/part_custom_state_admin.html.twig new file mode 100644 index 000000000..004ceb657 --- /dev/null +++ b/templates/admin/part_custom_state_admin.html.twig @@ -0,0 +1,14 @@ +{% extends "admin/base_admin.html.twig" %} + +{% block card_title %} + {% trans %}part_custom_state.caption{% endtrans %} +{% endblock %} + +{% block edit_title %} + {% trans %}part_custom_state.edit{% endtrans %}: {{ entity.name }} +{% endblock %} + +{% block new_title %} + {% trans %}part_custom_state.new{% endtrans %} +{% endblock %} + diff --git a/templates/admin/project_admin.html.twig b/templates/admin/project_admin.html.twig index 1a9950691..401be7cf7 100644 --- a/templates/admin/project_admin.html.twig +++ b/templates/admin/project_admin.html.twig @@ -3,7 +3,9 @@ {# @var entity App\Entity\ProjectSystem\Project #} {% block card_title %} - {% trans %}project.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('project', 'project.caption') %} + {% set translatedSource = 'project.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block edit_title %} @@ -36,7 +38,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/admin/storelocation_admin.html.twig b/templates/admin/storelocation_admin.html.twig index c93339dc1..1e60eeea2 100644 --- a/templates/admin/storelocation_admin.html.twig +++ b/templates/admin/storelocation_admin.html.twig @@ -2,7 +2,9 @@ {% import "label_system/dropdown_macro.html.twig" as dropdown %} {% block card_title %} - {% trans %}storelocation.labelp{% endtrans %} + {% set dataSourceName = get_data_source_name('storagelocation', 'storelocation.labelp') %} + {% set translatedSource = 'storelocation.labelp'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block additional_controls %} diff --git a/templates/admin/supplier_admin.html.twig b/templates/admin/supplier_admin.html.twig index ce38a5ca4..b5cf7b236 100644 --- a/templates/admin/supplier_admin.html.twig +++ b/templates/admin/supplier_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - {% trans %}supplier.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('supplier', 'supplier.caption') %} + {% set translatedSource = 'supplier.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block additional_panes %} diff --git a/templates/assemblies/add_parts.html.twig b/templates/assemblies/add_parts.html.twig new file mode 100644 index 000000000..d8d8e657f --- /dev/null +++ b/templates/assemblies/add_parts.html.twig @@ -0,0 +1,22 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{% trans %}assembly.add_parts_to_assembly{% endtrans %}{% endblock %} + +{% block card_title %} + + {% trans %}assembly.add_parts_to_assembly{% endtrans %}{% if assembly %}: {{ assembly.name }}{% endif %} +{% endblock %} + +{% block card_content %} + + {{ form_start(form) }} + + {{ form_row(form.assembly) }} + {% form_theme form.bom_entries with ['form/collection_types_layout_assembly.html.twig'] %} + {{ form_widget(form.bom_entries) }} + + {{ form_row(form.submit) }} + + {{ form_end(form) }} + +{% endblock %} \ No newline at end of file diff --git a/templates/assemblies/build/_form.html.twig b/templates/assemblies/build/_form.html.twig new file mode 100644 index 000000000..97cace564 --- /dev/null +++ b/templates/assemblies/build/_form.html.twig @@ -0,0 +1,90 @@ +{% 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 %} + {% elseif bom_entry.referencedAssembly %} + {{ 'assembly.build.form.referencedAssembly'|trans({'%name%': bom_entry.referencedAssembly.name}) }} {% if bom_entry.name %}({{ bom_entry.name }}){% endif %} + {% else %} + {{ bom_entry.name }} + {% endif %} + + {% for tag in bom_entry.mountnames|split(',') %} + {{ tag | trim }} + {% endfor %} + + {{ build_request.neededAmountForBOMEntry(bom_entry) | format_amount(bom_entry.part.partUnit ?? null) }} {% trans %}assembly.builds.needed{% endtrans %} + (= {{ number_of_builds }} x {{ bom_entry.quantity | format_amount(bom_entry.part.partUnit ?? null) }}) +
    + {% set lots = build_request.partLotsForBOMEntry(bom_entry) %} + {% if lots is not null %} + {% for lot in lots %} + {# @var lot \App\Entity\Parts\PartLot #} +
    + +
    + {{ form_errors(form["lot_"~lot.id]) }} + {{ form_widget(form["lot_"~lot.id]) }} +
    +
    + / {{ lot.amount | format_amount(lot.part.partUnit) }} {% trans %}assembly.builds.stocked{% endtrans %} +
    +
    + {% endfor %} + {% endif %} +
    + +{{ form_row(form.comment) }} +
    +{{ form_row(form.dontCheckQuantity) }} +
    + +{{ form_row(form.addBuildsToBuildsPart) }} +{% if form.buildsPartLot is defined %} + {{ form_row(form.buildsPartLot) }} +{% endif %} + +{{ form_row(form.submit) }} + +{{ form_end(form) }} \ No newline at end of file diff --git a/templates/assemblies/build/build.html.twig b/templates/assemblies/build/build.html.twig new file mode 100644 index 000000000..8f01607cb --- /dev/null +++ b/templates/assemblies/build/build.html.twig @@ -0,0 +1,40 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{% trans %}assembly.info.builds.label{% endtrans %}: {{ number_of_builds }}x {{ assembly.name }}{% endblock %} + +{% block card_title %} + + {% trans %}assembly.info.builds.label{% endtrans %}: {{ number_of_builds }}x {{ assembly.name }} +{% endblock %} + +{% block card_content %} + {% set can_build = buildHelper.assemblyBuildable(assembly, number_of_builds) %} + {% import "components/assemblies.macro.html.twig" as assembly_macros %} + + {% if assembly.status is not empty and assembly.status != "in_production" %} + + {% endif %} + + + +

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

    + + {% include 'assemblies/build/_form.html.twig' %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/assemblies/export_bom_pdf.html.twig b/templates/assemblies/export_bom_pdf.html.twig new file mode 100644 index 000000000..15bf5d883 --- /dev/null +++ b/templates/assemblies/export_bom_pdf.html.twig @@ -0,0 +1,103 @@ + + + + Assembly Hierarchy + + + + + +

    Table of Contents

    + + + + + + + + + + + {% for assembly in assemblies %} + + + + + + + {% endfor %} + +
    #Assembly NameIPNSection
    {{ loop.index }}Assembly: {{ assembly.name }}{% if assembly.ipn != '' %}{{ assembly.ipn }}{% else %}-{% endif %}{{ loop.index + 1 }}
    +
    + + +{% for assembly in assemblies %} +
    Assembly: {{ assembly.name }}
    + + + + + + + + + + + + {% for part in assembly.parts %} + + + + + + + + {% endfor %} + {% for other in assembly.others %} + + + + + + + + {% endfor %} + {% for referencedAssembly in assembly.referencedAssemblies %} + + + + + + + + {% endfor %} + +
    NameIPNQuantityMultiplierEffective Quantity
    {{ part.name }}{{ part.ipn }}{{ part.quantity }}{% if assembly.multiplier %}{{ assembly.multiplier }}{% else %}-{% endif %}{{ part.effectiveQuantity }}
    {{ other.name }}{{ other.ipn }}{{ other.quantity }}{{ other.multiplier }}{{ other.effectiveQuantity }}
    {{ referencedAssembly.name }}{{ referencedAssembly.ipn }}{{ referencedAssembly.quantity }}{{ referencedAssembly.quantity }}
    + + {% for refAssembly in assembly.referencedAssemblies %} + {% include 'assemblies/export_bom_referenced_assembly_pdf.html.twig' with {'assembly': refAssembly} only %} + {% endfor %} + + {% if not loop.last %} +
    + {% endif %} + + +{% endfor %} + + diff --git a/templates/assemblies/export_bom_referenced_assembly_pdf.html.twig b/templates/assemblies/export_bom_referenced_assembly_pdf.html.twig new file mode 100644 index 000000000..b5a1324d9 --- /dev/null +++ b/templates/assemblies/export_bom_referenced_assembly_pdf.html.twig @@ -0,0 +1,55 @@ +
    +
    Referenced Assembly: {{ assembly.name }} [IPN: {% if assembly.ipn != '' %}{{ assembly.ipn }}{% else %}-{% endif %}, quantity: {{ assembly.quantity }}]
    + + + + + + + + + + + + + + {% for part in assembly.parts %} + + + + + + + + + {% endfor %} + + {% for other in assembly.others %} + + + + + + + + + {% endfor %} + + {% for referencedAssembly in assembly.referencedAssemblies %} + + + + + + + + + {% endfor %} + +
    TypeNameIPNQuantityMultiplierEffective Quantity
    Part{{ part.name }}{{ part.ipn }}{{ part.quantity }}{% if assembly.multiplier %}{{ assembly.multiplier }}{% else %}-{% endif %}{{ part.effectiveQuantity }}
    Other{{ other.name }}-{{ other.quantity }}{{ other.multiplier }}-
    Referenced assembly{{ referencedAssembly.name }}-{{ referencedAssembly.quantity }}{{ referencedAssembly.multiplier }}
    + + + {% for refAssembly in assembly.referencedAssemblies %} + {% include 'assemblies/export_bom_referenced_assembly_pdf.html.twig' with {'assembly': refAssembly} only %} + {% endfor %} +
    diff --git a/templates/assemblies/import_bom.html.twig b/templates/assemblies/import_bom.html.twig new file mode 100644 index 000000000..89f504c2f --- /dev/null +++ b/templates/assemblies/import_bom.html.twig @@ -0,0 +1,112 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{% trans %}assembly.import_bom{% endtrans %}{% endblock %} + +{% block before_card %} + {% if validationErrors or importerErrors %} +
    +

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

    +
      + {% if validationErrors %} + {% for violation in validationErrors %} +
    • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators') }} +
    • + {% endfor %} + {% endif %} + + {% if importerErrors %} + {% for violation in importerErrors %} +
    • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators')|raw }} +
    • + {% endfor %} + {% endif %} +
    +
    + {% endif %} +{% endblock %} + +{% block card_title %} + + {% trans %}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.csv{% endtrans %} +
    +
    + {{ 'assembly.bom_import.template.csv.exptected_columns'|trans }} + +
    quantity;name;part_id;part_mpnr;part_ipn;part_name;part_description;part_manufacturer_id;part_manufacturer_name;part_category_id;part_category_name
    + +
      +
    • quantity
    • +
    • name
    • +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    • part_description
    • +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    • part_category_id
    • +
    • part_category_name
    • +
    + + {{ 'assembly.bom_import.template.csv.table'|trans|raw }} +
    +
    +
    +
    +
    +
    + {% 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
    + +
      +
    • Id
    • +
    • Designator
    • +
    • Package
    • +
    • Quantity
    • +
    • Designation
    • +
    • Supplier and ref
    • +
    • Note
    • +
    • Footprint
    • +
    • Value
    • +
    • Footprint
    • +
    + + {{ 'assembly.bom_import.template.kicad_pcbnew.exptected_columns.note'|trans|raw }} + + {{ 'assembly.bom_import.template.kicad_pcbnew.table'|trans|raw }} +
    +
    +
    +
    +{% endblock %} diff --git a/templates/assemblies/info/_attachments_info.html.twig b/templates/assemblies/info/_attachments_info.html.twig new file mode 100644 index 000000000..747426c3a --- /dev/null +++ b/templates/assemblies/info/_attachments_info.html.twig @@ -0,0 +1,91 @@ +{% import "helper.twig" as helper %} + + + + + + + + + + + + + + + + + {% for attachment in assembly.attachments %} + + + + + + + + + + {% endfor %} + + + +
    {% trans %}attachment.name{% endtrans %}{% trans %}attachment.attachment_type{% endtrans %}{% trans %}attachment.file_name{% endtrans %}{% trans %}attachment.file_size{% endtrans %}
    + {% import "components/attachments.macro.html.twig" as attachments %} + {{ attachments.attachment_icon(attachment, attachment_manager) }} + {{ attachment.name }}{{ attachment.attachmentType.fullPath }} + {% if attachment.hasInternal() %} + {{ attachment.filename }} + {% endif %} + + {% if not attachment.hasInternal() %} + + {% trans %}attachment.external_only{% endtrans %} + + {% elseif attachment_manager.internalFileExisting(attachment) %} + + {{ attachment_manager.humanFileSize(attachment) }} + + {% else %} + + {% trans %}attachment.file_not_found{% endtrans %} + + {% endif %} + {% if attachment.secure %} +
    + {% trans %}attachment.secure{% endtrans %} + + {% endif %} + {% if attachment == assembly.masterPictureAttachment %} +
    + + {% trans %}attachment.preview{% endtrans %} + + {% endif %} +
    + + + + + + + + + + +
    + + +
    +
    \ No newline at end of file diff --git a/templates/assemblies/info/_builds.html.twig b/templates/assemblies/info/_builds.html.twig new file mode 100644 index 000000000..780c8c609 --- /dev/null +++ b/templates/assemblies/info/_builds.html.twig @@ -0,0 +1,40 @@ +{% set can_build = buildHelper.assemblyBuildable(assembly) %} + +{% import "components/assemblies.macro.html.twig" as assembly_macros %} + +{% if assembly.status is not empty and assembly.status != "in_production" %} + +{% endif %} + + + +
    +
    +
    +
    + + + +
    +
    +
    +
    + +{% if assembly.buildPart %} +

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

    +{% endif %} \ No newline at end of file diff --git a/templates/assemblies/info/_info.html.twig b/templates/assemblies/info/_info.html.twig new file mode 100644 index 000000000..97da3f708 --- /dev/null +++ b/templates/assemblies/info/_info.html.twig @@ -0,0 +1,72 @@ +{% 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) }}
    +
    +
    +
    + + +
    {# 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 %} +
    diff --git a/templates/assemblies/info/_info_card.html.twig b/templates/assemblies/info/_info_card.html.twig new file mode 100644 index 000000000..2d0c535b2 --- /dev/null +++ b/templates/assemblies/info/_info_card.html.twig @@ -0,0 +1,118 @@ +{% 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.comment is not empty %} +
    +
    + {{ assembly.comment|format_markdown }} +
    +
    + {% endif %} +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/assemblies/info/_part.html.twig b/templates/assemblies/info/_part.html.twig new file mode 100644 index 000000000..1fa8b90ed --- /dev/null +++ b/templates/assemblies/info/_part.html.twig @@ -0,0 +1,5 @@ +{% import "components/datatables.macro.html.twig" as datatables %} + +
    + +{{ datatables.datatable(datatable, 'elements/datatables/datatables', 'assemblies') }} \ No newline at end of file diff --git a/templates/assemblies/info/info.html.twig b/templates/assemblies/info/info.html.twig new file mode 100644 index 000000000..667da909f --- /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 before_card %} + +{% 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" %} +
    +
    + {% include "assemblies/info/_part.html.twig" %} +
    +
    + {% include "assemblies/info/_attachments_info.html.twig" with {"assembly": assembly} %} +
    +
    + +{% endblock %} diff --git a/templates/assemblies/lists/_action_bar.html.twig b/templates/assemblies/lists/_action_bar.html.twig new file mode 100644 index 000000000..37289812a --- /dev/null +++ b/templates/assemblies/lists/_action_bar.html.twig @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/templates/assemblies/lists/_filter.html.twig b/templates/assemblies/lists/_filter.html.twig new file mode 100644 index 000000000..11be7bc24 --- /dev/null +++ b/templates/assemblies/lists/_filter.html.twig @@ -0,0 +1,62 @@ +
    +
    + +
    +
    +
    + + + {{ form_start(filterForm, {"attr": {"data-controller": "helpers--form-cleanup", "data-action": "helpers--form-cleanup#submit"}}) }} + +
    +
    + {{ form_row(filterForm.name) }} + {{ form_row(filterForm.description) }} + {{ form_row(filterForm.comment) }} +
    + +
    + {{ form_row(filterForm.dbId) }} + {{ form_row(filterForm.ipn) }} + {{ form_row(filterForm.lastModified) }} + {{ form_row(filterForm.addedDate) }} +
    + +
    + {{ form_row(filterForm.attachmentsCount) }} + {{ form_row(filterForm.attachmentType) }} + {{ form_row(filterForm.attachmentName) }} +
    +
    + + {{ form_row(filterForm.submit) }} + {{ form_row(filterForm.discard) }} + +
    +
    + +
    +
    + + {# Retain the query parameters of the search form if it is existing #} + {% if searchFilter is defined %} + {% for property, value in searchFilter|to_array %} + + {% endfor %} + + {% endif %} + + {{ form_end(filterForm) }} +
    +
    +
    \ No newline at end of file diff --git a/templates/assemblies/lists/all_list.html.twig b/templates/assemblies/lists/all_list.html.twig new file mode 100644 index 000000000..70d75ad40 --- /dev/null +++ b/templates/assemblies/lists/all_list.html.twig @@ -0,0 +1,30 @@ +{% extends "base.html.twig" %} + +{% block title %} + {% trans %}assembly_list.all.title{% endtrans %} +{% endblock %} + +{% block content %} + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + + {% include "assemblies/lists/_filter.html.twig" %} +
    + + {% include "assemblies/lists/_action_bar.html.twig" with {'url_options': {}} %} + {% include "assemblies/lists/data.html.twig" %} + +{% endblock %} diff --git a/templates/assemblies/lists/data.html.twig b/templates/assemblies/lists/data.html.twig new file mode 100644 index 000000000..69e13e4f5 --- /dev/null +++ b/templates/assemblies/lists/data.html.twig @@ -0,0 +1,3 @@ +{% import "components/datatables.macro.html.twig" as datatables %} + +{{ datatables.partsDatatableWithForm(datatable) }} diff --git a/templates/components/assemblies.macro.html.twig b/templates/components/assemblies.macro.html.twig new file mode 100644 index 000000000..d59005e05 --- /dev/null +++ b/templates/components/assemblies.macro.html.twig @@ -0,0 +1,8 @@ +{% macro assembly_bom_entry_with_missing_instock(assembly_bom_entry, number_of_builds = 1) %} + {# @var \App\Entity\AssemblySystem\AssemblyBOMEntry assembly_bom_entry #} + {{ assembly_bom_entry.part.name }} + {% if assembly_bom_entry.name %} ({{ assembly_bom_entry.name }}){% endif %}: + {{ assembly_bom_entry.part.amountSum | format_amount(assembly_bom_entry.part.partUnit) }} {% trans %}assembly.builds.stocked{% endtrans %} + / + {{ (assembly_bom_entry.quantity * number_of_builds) | format_amount(assembly_bom_entry.part.partUnit) }} {% trans %}assembly.builds.needed{% endtrans %} +{% endmacro %} \ No newline at end of file diff --git a/templates/components/tree_macros.html.twig b/templates/components/tree_macros.html.twig index 366d42fe8..210a00633 100644 --- a/templates/components/tree_macros.html.twig +++ b/templates/components/tree_macros.html.twig @@ -1,13 +1,16 @@ {% macro sidebar_dropdown() %} + {% set currentLocale = app.request.locale %} + {# Format is [mode, route, label, show_condition] #} {% set data_sources = [ - ['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read')], - ['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read')], - ['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read')], - ['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')], - ['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')], - ['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')], - ['tools', path('tree_tools'), 'tools.label', true], + ['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read'), 'category'], + ['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read'), 'storagelocation'], + ['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read'), 'footprint'], + ['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read'), 'manufacturer'], + ['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read'), 'supplier'], + ['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read'), 'project'], + ['assembly', path('tree_assembly_root'), 'assembly.labelp', is_granted('@assemblies.read'), 'assembly'], + ['tools', path('tree_tools'), 'tools.label', true, 'tool'], ] %} @@ -18,9 +21,9 @@ {% for source in data_sources %} {% if source[3] %} {# show_condition #} -
  • + >{{ get_data_source_name(source[4], source[2]) }} {% endif %} {% endfor %} {% endmacro %} @@ -61,4 +64,4 @@
    -{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/templates/form/collection_types_layout_assembly.html.twig b/templates/form/collection_types_layout_assembly.html.twig new file mode 100644 index 000000000..6dc6d49a0 --- /dev/null +++ b/templates/form/collection_types_layout_assembly.html.twig @@ -0,0 +1,83 @@ +{% 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.partOrAssembly{% 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_row(form.part) }} + {{ form_errors(form.part) }} +
    + {{ form_widget(form.referencedAssembly) }} + {{ form_errors(form.referencedAssembly) }} + + + {{ 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/form/permission_layout.html.twig b/templates/form/permission_layout.html.twig index 166147b4c..dcceae335 100644 --- a/templates/form/permission_layout.html.twig +++ b/templates/form/permission_layout.html.twig @@ -6,12 +6,36 @@
    {% else %} - {{ form.vars.label | trans }} + def{{ form.vars.label | trans }} {% endif %} diff --git a/templates/helper.twig b/templates/helper.twig index bd1d2aa7a..3ddb4f7fa 100644 --- a/templates/helper.twig +++ b/templates/helper.twig @@ -76,6 +76,21 @@ {% endif %} {% endmacro %} +{% macro assemblies_status_to_badge(status, class="badge") %} + {% if status is not empty %} + {% set color = " bg-secondary" %} + + {% if status == "in_production" %} + {% set color = " bg-success" %} + {% endif %} + + + + {{ ("assembly.status." ~ status) | trans }} + + {% endif %} +{% endmacro %} + {% macro structural_entity_link(entity, link_type = "list_parts") %} {# @var entity \App\Entity\Base\StructuralDBElement #} {% if entity %} @@ -101,6 +116,7 @@ "category": ["fa-solid fa-tags", "category.label"], "currency": ["fa-solid fa-coins", "currency.label"], "device": ["fa-solid fa-archive", "project.label"], + "assembly": ["fa-solid fa-list", "assembly.label"], "footprint": ["fa-solid fa-microchip", "footprint.label"], "group": ["fa-solid fa-users", "group.label"], "label_profile": ["fa-solid fa-qrcode", "label_profile.label"], diff --git a/templates/parts/edit/_advanced.html.twig b/templates/parts/edit/_advanced.html.twig index 12b546abc..991a36ebb 100644 --- a/templates/parts/edit/_advanced.html.twig +++ b/templates/parts/edit/_advanced.html.twig @@ -1,5 +1,16 @@ {{ form_row(form.needsReview) }} {{ form_row(form.favorite) }} {{ form_row(form.mass) }} -{{ form_row(form.ipn) }} -{{ form_row(form.partUnit) }} \ No newline at end of file +
    + {{ form_row(form.ipn) }} +
    +{{ form_row(form.partUnit) }} +{{ form_row(form.partCustomState) }} diff --git a/templates/parts/info/_assemblies.html.twig b/templates/parts/info/_assemblies.html.twig new file mode 100644 index 000000000..d4996c592 --- /dev/null +++ b/templates/parts/info/_assemblies.html.twig @@ -0,0 +1,31 @@ +{% import "components/attachments.macro.html.twig" as attachments %} +{% import "helper.twig" as helper %} + + + + + + + + + + + + + {% for bom_entry in part.assemblyBomEntries %} + {# @var bom_entry App\Entity\Assembly\AssemblyBOMEntry #} + + + {# Name #} + {# Description #} + + + {% endfor %} + +
    {% trans %}entity.info.name{% endtrans %}{% trans %}description.label{% endtrans %}{% trans %}assembly.bom.quantity{% endtrans %}
    {% if bom_entry.assembly.masterPictureAttachment is not null %}{{ attachments.attachment_icon(bom_entry.assembly.masterPictureAttachment, attachment_manager) }}{% endif %}{{ bom_entry.assembly.name }}{{ bom_entry.assembly.description|format_markdown }}{{ bom_entry.quantity | format_amount(part.partUnit) }}
    + + + + {% trans %}part.info.add_part_to_assembly{% endtrans %} + \ No newline at end of file diff --git a/templates/parts/info/show_part_info.html.twig b/templates/parts/info/show_part_info.html.twig index 96b5e2091..cd7b4ce7a 100644 --- a/templates/parts/info/show_part_info.html.twig +++ b/templates/parts/info/show_part_info.html.twig @@ -109,15 +109,20 @@ {% trans %}vendor.partinfo.history{% endtrans %} - {% if part.projectBomEntries is not empty %} - - {% endif %} + +
    +
    + {% include "parts/info/_assemblies.html.twig" %} +
    +
    {% include "parts/info/_history.html.twig" %}
    diff --git a/templates/parts/lists/_filter.html.twig b/templates/parts/lists/_filter.html.twig index c29e8ecdc..72cfc5bed 100644 --- a/templates/parts/lists/_filter.html.twig +++ b/templates/parts/lists/_filter.html.twig @@ -56,6 +56,7 @@ {{ form_row(filterForm.favorite) }} {{ form_row(filterForm.needsReview) }} {{ form_row(filterForm.measurementUnit) }} + {{ form_row(filterForm.partCustomState) }} {{ form_row(filterForm.mass) }} {{ form_row(filterForm.dbId) }} {{ form_row(filterForm.ipn) }} diff --git a/templates/projects/build/_form.html.twig b/templates/projects/build/_form.html.twig index a8f772e97..b25ca81eb 100644 --- a/templates/projects/build/_form.html.twig +++ b/templates/projects/build/_form.html.twig @@ -75,7 +75,7 @@ {{ form_row(form.comment) }}
    -{{ form_row(form.dontCheckQuantity) }} + {{ form_row(form.dontCheckQuantity) }}
    {{ form_row(form.addBuildsToBuildsPart) }} diff --git a/templates/projects/import_bom.html.twig b/templates/projects/import_bom.html.twig index 0e7f17875..3f9059120 100644 --- a/templates/projects/import_bom.html.twig +++ b/templates/projects/import_bom.html.twig @@ -3,29 +3,107 @@ {% block title %}{% trans %}project.import_bom{% endtrans %}{% endblock %} {% block before_card %} - {% if errors %} + {% if validationErrors or importerErrors %}

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

      - {% for violation in errors %} -
    • - {{ violation.propertyPath }}: - {{ violation.message|trans(violation.parameters, 'validators') }} -
    • - {% endfor %} + {% if validationErrors %} + {% for violation in validationErrors %} +
    • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators') }} +
    • + {% endfor %} + {% endif %} + + {% if importerErrors %} + {% for violation in importerErrors %} +
    • + {{ violation.propertyPath }}: + {{ violation.message|trans(violation.parameters, 'validators')|raw }} +
    • + {% endfor %} + {% endif %}
    {% endif %} {% endblock %} - {% block card_title %} {% trans %}project.import_bom{% endtrans %}{% if project %}: {{ project.name }}{% endif %} {% endblock %} {% block card_content %} - {{ form(form) }} +{% endblock %} + +{% block additional_content %} +
    +
    +
    +
    + {% trans %}project.import_bom.template.header.json{% endtrans %} +
    +
    +
    {{ jsonTemplate|json_encode(constant('JSON_PRETTY_PRINT') b-or constant('JSON_UNESCAPED_UNICODE')) }}
    + + {{ 'project.bom_import.template.json.table'|trans|raw }} +
    +
    +
    +
    +
    +
    + {% trans %}project.import_bom.template.header.csv{% endtrans %} +
    +
    + {{ 'project.bom_import.template.csv.exptected_columns'|trans }} +
    quantity;name;part_id;part_mpnr;part_ipn;part_name;part_manufacturer_id;part_manufacturer_name
    + +
      +
    • quantity
    • +
    • name
    • +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + {{ 'project.bom_import.template.csv.table'|trans|raw }} +
    +
    +
    +
    +
    +
    + {% trans %}project.import_bom.template.header.kicad_pcbnew{% endtrans %} +
    +
    + {{ 'project.bom_import.template.kicad_pcbnew.exptected_columns'|trans }} +
    Id;Designator;Package;Quantity;Designation;Supplier and ref
    + +
      +
    • Id
    • +
    • Designator
    • +
    • Package
    • +
    • Quantity
    • +
    • Designation
    • +
    • Supplier and ref
    • +
    • Note
    • +
    • Footprint
    • +
    • Value
    • +
    • Footprint
    • +
    + + {{ 'project.bom_import.template.kicad_pcbnew.exptected_columns.note'|trans|raw }} + + {{ 'project.bom_import.template.kicad_pcbnew.table'|trans|raw }} +
    +
    +
    +
    {% endblock %} \ No newline at end of file diff --git a/tests/Controller/AdminPages/PartCustomStateControllerTest.php b/tests/Controller/AdminPages/PartCustomStateControllerTest.php new file mode 100644 index 000000000..b92308893 --- /dev/null +++ b/tests/Controller/AdminPages/PartCustomStateControllerTest.php @@ -0,0 +1,35 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Controller\AdminPages; + +use App\Entity\Parts\PartCustomState; + +/** + * @group slow + * @group DB + */ +class PartCustomStateControllerTest extends AbstractAdminControllerTest +{ + protected static string $base_path = '/en/part_custom_state'; + protected static string $entity_class = PartCustomState::class; +} diff --git a/tests/Entity/Attachments/AttachmentTest.php b/tests/Entity/Attachments/AttachmentTest.php index 00a68d7d4..21a0e89fa 100644 --- a/tests/Entity/Attachments/AttachmentTest.php +++ b/tests/Entity/Attachments/AttachmentTest.php @@ -24,11 +24,14 @@ 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; use App\Entity\Attachments\CategoryAttachment; use App\Entity\Attachments\CurrencyAttachment; +use App\Entity\Attachments\PartCustomStateAttachment; use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\FootprintAttachment; use App\Entity\Attachments\GroupAttachment; @@ -38,6 +41,7 @@ use App\Entity\Attachments\StorageLocationAttachment; use App\Entity\Attachments\SupplierAttachment; use App\Entity\Attachments\UserAttachment; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -81,11 +85,13 @@ public static function subClassesDataProvider(): \Iterator yield [CategoryAttachment::class, Category::class]; yield [CurrencyAttachment::class, Currency::class]; yield [ProjectAttachment::class, Project::class]; + yield [AssemblyAttachment::class, Assembly::class]; yield [FootprintAttachment::class, Footprint::class]; yield [GroupAttachment::class, Group::class]; yield [ManufacturerAttachment::class, Manufacturer::class]; yield [MeasurementUnitAttachment::class, MeasurementUnit::class]; yield [PartAttachment::class, Part::class]; + yield [PartCustomStateAttachment::class, PartCustomState::class]; yield [StorageLocationAttachment::class, StorageLocation::class]; yield [SupplierAttachment::class, Supplier::class]; yield [UserAttachment::class, User::class]; diff --git a/tests/Helpers/Assemblies/AssemblyBuildRequestTest.php b/tests/Helpers/Assemblies/AssemblyBuildRequestTest.php new file mode 100644 index 000000000..210e33018 --- /dev/null +++ b/tests/Helpers/Assemblies/AssemblyBuildRequestTest.php @@ -0,0 +1,177 @@ +. + */ +namespace App\Tests\Helpers\Assemblies; + +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Helpers\Assemblies\AssemblyBuildRequest; +use PHPUnit\Framework\TestCase; + +class AssemblyBuildRequestTest extends TestCase +{ + + /** @var MeasurementUnit $float_unit */ + private MeasurementUnit $float_unit; + + /** @var Assembly */ + private Assembly $assembly1; + /** @var AssemblyBOMEntry */ + private AssemblyBOMEntry $bom_entry1a; + /** @var AssemblyBOMEntry */ + private AssemblyBOMEntry $bom_entry1b; + /** @var AssemblyBOMEntry */ + private AssemblyBOMEntry $bom_entry1c; + + private PartLot $lot1a; + private PartLot $lot1b; + private PartLot $lot2; + + /** @var Part */ + private Part $part1; + /** @var Part */ + private Part $part2; + + + public function setUp(): void + { + $this->float_unit = new MeasurementUnit(); + $this->float_unit->setName('float'); + $this->float_unit->setUnit('f'); + $this->float_unit->setIsInteger(false); + $this->float_unit->setUseSIPrefix(true); + + //Setup some example parts and part lots + $this->part1 = new Part(); + $this->part1->setName('Part 1'); + $this->lot1a = new class extends PartLot { + public function getID(): ?int + { + return 1; + } + }; + $this->part1->addPartLot($this->lot1a); + $this->lot1a->setAmount(10); + $this->lot1a->setDescription('Lot 1a'); + + $this->lot1b = new class extends PartLot { + public function getID(): ?int + { + return 2; + } + }; + $this->part1->addPartLot($this->lot1b); + $this->lot1b->setAmount(20); + $this->lot1b->setDescription('Lot 1b'); + + $this->part2 = new Part(); + + $this->part2->setName('Part 2'); + $this->part2->setPartUnit($this->float_unit); + $this->lot2 = new PartLot(); + $this->part2->addPartLot($this->lot2); + $this->lot2->setAmount(2.5); + $this->lot2->setDescription('Lot 2'); + + $this->bom_entry1a = new AssemblyBOMEntry(); + $this->bom_entry1a->setPart($this->part1); + $this->bom_entry1a->setQuantity(2); + + $this->bom_entry1b = new AssemblyBOMEntry(); + $this->bom_entry1b->setPart($this->part2); + $this->bom_entry1b->setQuantity(1.5); + + $this->bom_entry1c = new AssemblyBOMEntry(); + $this->bom_entry1c->setName('Non-part BOM entry'); + $this->bom_entry1c->setQuantity(4); + + + $this->assembly1 = new Assembly(); + $this->assembly1->setName('Assembly 1'); + $this->assembly1->addBomEntry($this->bom_entry1a); + $this->assembly1->addBomEntry($this->bom_entry1b); + $this->assembly1->addBomEntry($this->bom_entry1c); + } + + public function testInitialization(): void + { + //The values should be already prefilled correctly + $request = new AssemblyBuildRequest($this->assembly1, 10); + //We need totally 20: Take 10 from the first (maximum 10) and 10 from the second (maximum 20) + $this->assertEqualsWithDelta(10.0, $request->getLotWithdrawAmount($this->lot1a), PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(10.0, $request->getLotWithdrawAmount($this->lot1b), PHP_FLOAT_EPSILON); + + //If the needed amount is higher than the maximum, we should get the maximum + $this->assertEqualsWithDelta(2.5, $request->getLotWithdrawAmount($this->lot2), PHP_FLOAT_EPSILON); + } + + public function testGetNumberOfBuilds(): void + { + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + $this->assertSame(5, $build_request->getNumberOfBuilds()); + } + + public function testGetAssembly(): void + { + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + $this->assertEquals($this->assembly1, $build_request->getAssembly()); + } + + public function testGetNeededAmountForBOMEntry(): void + { + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + $this->assertEqualsWithDelta(10.0, $build_request->getNeededAmountForBOMEntry($this->bom_entry1a), PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(7.5, $build_request->getNeededAmountForBOMEntry($this->bom_entry1b), PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(20.0, $build_request->getNeededAmountForBOMEntry($this->bom_entry1c), PHP_FLOAT_EPSILON); + } + + public function testGetSetLotWithdrawAmount(): void + { + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + + //We can set the amount for a lot either via the lot object or via the ID + $build_request->setLotWithdrawAmount($this->lot1a, 2); + $build_request->setLotWithdrawAmount($this->lot1b->getID(), 3); + + //And it should be possible to get the amount via the lot object or via the ID + $this->assertEqualsWithDelta(2.0, $build_request->getLotWithdrawAmount($this->lot1a->getID()), PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(3.0, $build_request->getLotWithdrawAmount($this->lot1b), PHP_FLOAT_EPSILON); + } + + public function testGetWithdrawAmountSum(): void + { + //The sum of all withdraw amounts for an BOM entry (over all lots of the associated part) should be correct + $build_request = new AssemblyBuildRequest($this->assembly1, 5); + + $build_request->setLotWithdrawAmount($this->lot1a, 2); + $build_request->setLotWithdrawAmount($this->lot1b, 3); + + $this->assertEqualsWithDelta(5.0, $build_request->getWithdrawAmountSum($this->bom_entry1a), PHP_FLOAT_EPSILON); + $build_request->setLotWithdrawAmount($this->lot2, 1.5); + $this->assertEqualsWithDelta(1.5, $build_request->getWithdrawAmountSum($this->bom_entry1b), PHP_FLOAT_EPSILON); + } + + +} diff --git a/tests/Services/AssemblySystem/AssemblyBuildHelperTest.php b/tests/Services/AssemblySystem/AssemblyBuildHelperTest.php new file mode 100644 index 000000000..c513ed8de --- /dev/null +++ b/tests/Services/AssemblySystem/AssemblyBuildHelperTest.php @@ -0,0 +1,117 @@ +. + */ +namespace App\Tests\Services\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use App\Entity\AssemblySystem\AssemblyBOMEntry; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Services\AssemblySystem\AssemblyBuildHelper; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class AssemblyBuildHelperTest extends WebTestCase +{ + /** @var AssemblyBuildHelper */ + protected $service; + + protected function setUp(): void + { + self::bootKernel(); + $this->service = self::getContainer()->get(AssemblyBuildHelper::class); + } + + public function testGetMaximumBuildableCountForBOMEntryNonPartBomEntry(): void + { + $bom_entry = new AssemblyBOMEntry(); + $bom_entry->setPart(null); + $bom_entry->setQuantity(10); + $bom_entry->setName('Test'); + + $this->expectException(\InvalidArgumentException::class); + $this->service->getMaximumBuildableCountForBOMEntry($bom_entry); + } + + public function testGetMaximumBuildableCountForBOMEntry(): void + { + $assembly_bom_entry = new AssemblyBOMEntry(); + $assembly_bom_entry->setQuantity(10); + + $part = new Part(); + $lot1 = new PartLot(); + $lot1->setAmount(120); + $lot2 = new PartLot(); + $lot2->setAmount(5); + $part->addPartLot($lot1); + $part->addPartLot($lot2); + + $assembly_bom_entry->setPart($part); + + //We have 125 parts in stock, so we can build 12 times the assembly (125 / 10 = 12.5) + $this->assertSame(12, $this->service->getMaximumBuildableCountForBOMEntry($assembly_bom_entry)); + + + $lot1->setAmount(0); + //We have 5 parts in stock, so we can build 0 times the assembly (5 / 10 = 0.5) + $this->assertSame(0, $this->service->getMaximumBuildableCountForBOMEntry($assembly_bom_entry)); + } + + public function testGetMaximumBuildableCount(): void + { + $assembly = new Assembly(); + + $assembly_bom_entry1 = new AssemblyBOMEntry(); + $assembly_bom_entry1->setQuantity(10); + $part = new Part(); + $lot1 = new PartLot(); + $lot1->setAmount(120); + $lot2 = new PartLot(); + $lot2->setAmount(5); + $part->addPartLot($lot1); + $part->addPartLot($lot2); + $assembly_bom_entry1->setPart($part); + $assembly->addBomEntry($assembly_bom_entry1); + + $assembly_bom_entry2 = new AssemblyBOMEntry(); + $assembly_bom_entry2->setQuantity(5); + $part2 = new Part(); + $lot3 = new PartLot(); + $lot3->setAmount(10); + $part2->addPartLot($lot3); + $assembly_bom_entry2->setPart($part2); + $assembly->addBomEntry($assembly_bom_entry2); + + $assembly->addBomEntry((new AssemblyBOMEntry())->setName('Non part entry')->setQuantity(1)); + + //Restricted by the few parts in stock of part2 + $this->assertSame(2, $this->service->getMaximumBuildableCount($assembly)); + + $lot3->setAmount(1000); + //Now the build count is restricted by the few parts in stock of part1 + $this->assertSame(12, $this->service->getMaximumBuildableCount($assembly)); + + $lot3->setAmount(0); + //Now the build count must be 0, as we have no parts in stock + $this->assertSame(0, $this->service->getMaximumBuildableCount($assembly)); + + } +} diff --git a/tests/Services/AssemblySystem/AssemblyBuildPartHelperTest.php b/tests/Services/AssemblySystem/AssemblyBuildPartHelperTest.php new file mode 100644 index 000000000..b8aa0ddc3 --- /dev/null +++ b/tests/Services/AssemblySystem/AssemblyBuildPartHelperTest.php @@ -0,0 +1,52 @@ +. + */ +namespace App\Tests\Services\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use App\Services\AssemblySystem\AssemblyBuildPartHelper; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class AssemblyBuildPartHelperTest extends WebTestCase +{ + /** @var AssemblyBuildPartHelper */ + protected $service; + + protected function setUp(): void + { + self::bootKernel(); + $this->service = self::getContainer()->get(AssemblyBuildPartHelper::class); + } + + public function testGetPartInitialization(): void + { + $assembly = new Assembly(); + $assembly->setName('Assembly 1'); + $assembly->setDescription('Description 1'); + + $part = $this->service->getPartInitialization($assembly); + $this->assertSame('Assembly 1', $part->getName()); + $this->assertSame('Description 1', $part->getDescription()); + $this->assertSame($assembly, $part->getBuiltAssembly()); + $this->assertSame($part, $assembly->getBuildPart()); + } +} diff --git a/tests/Services/EntityMergers/Mergers/PartMergerTest.php b/tests/Services/EntityMergers/Mergers/PartMergerTest.php index 56c7712e0..7db4ddd66 100644 --- a/tests/Services/EntityMergers/Mergers/PartMergerTest.php +++ b/tests/Services/EntityMergers/Mergers/PartMergerTest.php @@ -29,6 +29,7 @@ use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; use App\Entity\Parts\PartAssociation; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\PartLot; use App\Entity\PriceInformations\Orderdetail; use App\Services\EntityMergers\Mergers\PartMerger; @@ -54,6 +55,7 @@ public function testMergeOfEntityRelations(): void $manufacturer1 = new Manufacturer(); $manufacturer2 = new Manufacturer(); $unit = new MeasurementUnit(); + $customState = new PartCustomState(); $part1 = (new Part()) ->setCategory($category) @@ -62,7 +64,8 @@ public function testMergeOfEntityRelations(): void $part2 = (new Part()) ->setFootprint($footprint) ->setManufacturer($manufacturer2) - ->setPartUnit($unit); + ->setPartUnit($unit) + ->setPartCustomState($customState); $merged = $this->merger->merge($part1, $part2); $this->assertSame($merged, $part1); @@ -70,6 +73,7 @@ public function testMergeOfEntityRelations(): void $this->assertSame($footprint, $merged->getFootprint()); $this->assertSame($manufacturer1, $merged->getManufacturer()); $this->assertSame($unit, $merged->getPartUnit()); + $this->assertSame($customState, $merged->getPartCustomState()); } public function testMergeOfTags(): void diff --git a/tests/assets/partkeepr_import_test.xml b/tests/assets/partkeepr_import_test.xml index 4fa497e2e..28e6f0991 100644 --- a/tests/assets/partkeepr_import_test.xml +++ b/tests/assets/partkeepr_import_test.xml @@ -7437,11 +7437,13 @@ + + diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 1f234450e..514f144b8 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -351,6 +351,24 @@ Exportovat všechny prvky + + + export.readable.label + Čitelný export + + + + + export.readable + CSV + + + + + export.readable_bom + PDF + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -548,6 +566,12 @@ Měrné jednotky + + + part_custom_state.caption + Vlastní stav komponenty + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -1842,6 +1866,66 @@ Související prvky budou přesunuty nahoru. Pokročilé + + + part.edit.tab.advanced.ipn.commonSectionHeader + Návrhy bez přírůstku části + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Návrhy s číselnými přírůstky částí + + + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktuální specifikace IPN pro součást + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Další možná specifikace IPN na základě identického popisu součásti + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + IPN předpona přímé kategorie je prázdná, zadejte ji v kategorii „%name%“ + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN prefix přímé kategorie + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN prefix přímé kategorie a specifického přírůstku pro část + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN prefixy s hierarchickým pořadím kategorií rodičovských prefixů + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN prefixy s hierarchickým pořadím kategorií rodičovských prefixů a specifickým přírůstkem pro část + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Nejprve vytvořte součást a přiřaďte ji do kategorie: s dostupnými kategoriemi a jejich vlastními IPN prefixy lze automaticky navrhnout IPN označení pro danou součást + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -4741,7 +4825,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Název - + Part-DB1\src\DataTables\PartsDataTable.php:178 Part-DB1\src\DataTables\PartsDataTable.php:126 @@ -4831,6 +4915,12 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měrné jednotky + + + part.table.partCustomState + Vlastní stav součásti + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5695,6 +5785,12 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měrná jednotka + + + part.edit.partCustomState + Vlastní stav součásti + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -5982,6 +6078,12 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měrná jednotka + + + part_custom_state.label + Vlastní stav součásti + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6225,6 +6327,12 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Měrné jednotky + + + tree.tools.edit.part_custom_state + Vlastní stav součásti + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -6959,6 +7067,12 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Filtr názvů + + + category.edit.part_ipn_prefix + Předpona součásti IPN + + obsolete @@ -8495,6 +8609,12 @@ Element 3 Měrná jednotka + + + perm.part_custom_states + Vlastní stav součásti + + obsolete @@ -9864,6 +9984,48 @@ Element 3 Archivováno + + + assembly.edit.status + Stav sestavy + + + + + assembly.edit.ipn + Interní číslo dílu (IPN) + + + + + 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 @@ -10254,12 +10416,24 @@ Element 3 např. "/Kondenzátor \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + např. "B12A" + + category.edit.partname_regex.help Regulární výraz kompatibilní s PCRE, kterému musí název dílu odpovídat. + + + category.edit.part_ipn_prefix.help + Předpona navrhovaná při zadávání IPN části. + + entity.select.add_hint @@ -10806,6 +10980,12 @@ Element 3 Měrná jednotka + + + log.element_edited.changed_fields.partCustomState + Vlastní stav součásti + + log.element_edited.changed_fields.expiration_date @@ -11010,6 +11190,18 @@ Element 3 Typ + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11028,6 +11220,319 @@ Element 3 Výběrem této možnosti odstraníte všechny existující položky BOM v projektu a přepíšete je importovaným souborem BOM! + + + project.import_bom.template.header.json + Šablona importu JSON + + + + + project.import_bom.template.header.csv + Šablona importu CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Šablona importu CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Název komponenty v projektu + + + + + project.bom_import.template.entry.part.mpnr + Jedinečné číslo produktu u výrobce + + + + + project.bom_import.template.entry.part.ipn + Jedinečné IPN součásti + + + + + project.bom_import.template.entry.part.name + Jedinečný název součásti + + + + + project.bom_import.template.entry.part.manufacturer.name + Jedinečný název výrobce + + + + + project.bom_import.template.json.table + + + + + Pole + Podmínka + Datový typ + Popis + + + + + quantity + Povinné + Desetinné číslo (Float) + Musí být zadáno a obsahovat desetinnou hodnotu (Float), která je větší než 0.0. + + + name + Volitelné + Řetězec (String) + Pokud je přítomen, musí být neprázdný řetězec. Název položky v kusovníku. + + + part + Volitelné + Objekt/Array + + Pokud je potřeba přiřadit součástku, musí to být objekt/pole a musí být vyplněno alespoň jedno z následujících polí: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID součástky v Part-DB. + + + part.mpnr + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno part.id, part.ipn nebo part.name. + + + part.ipn + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno part.id, part.mpnr nebo part.name. + + + part.name + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno part.id, part.mpnr nebo part.ipn. + + + part.manufacturer + Volitelné + Objekt/Array + + Pokud má být upraven výrobce součástky nebo pokud má být součástka nalezena jednoznačně na základě part.mpnr, musí to být objekt/pole a musí být vyplněno alespoň jedno z následujících polí: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID výrobce. + + + manufacturer.name + Volitelné + Řetězec (String) + Neprázdný řetězec, pokud není uvedeno manufacturer.id. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.csv.exptected_columns + Možné sloupce: + + + + + project.bom_import.template.csv.table + + + + + Sloupec + Podmínka + Datový typ + Popis + + + + + quantity + Povinné + Desetinné číslo (Float) + Musí být uvedeno a obsahovat hodnotu desetinného čísla (Float) větší než 0.0. + + + name + Optional + String + Název položky v kusovníku. + + + Sloupce začínající part_ + + Pokud má být přiřazena součástka, musí být uveden a vyplněn alespoň jeden z následujících sloupců: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID součástky v Part-DB. + + + part_mpnr + Volitelné + Řetězec (String) + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_ipn nebo part_name. + + + part_ipn + Volitelné + Řetězec (String) + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_mpnr nebo part_name. + + + part_name + Volitelné + Řetězec (String) + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_mpnr nebo part_ipn. + + + Sloupce začínající part_manufacturer_ + + Pokud má být upraven výrobce dílu nebo má být díl jednoznačně identifikován podle hodnoty part_mpnr, musí být uveden a vyplněn alespoň jeden z následujících sloupců: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Volitelné + Celé číslo (Integer) + Celé číslo (Integer) > 0. Odpovídá internímu číselnému ID výrobce. + + + part_manufacturer_name + Volitelné + Řetězec (String) + Musí být uvedeno, pokud není vyplněn sloupec part_manufacturer_id. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Očekávané sloupce: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Poznámka: Nedochází k přiřazení ke konkrétním součástkám ze správy kategorií.

    + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.table + + + + + Pole + Podmínka + Datový typ + Popis + + + + + Id + Volitelné + Celé číslo (Integer) + Volný údaj. Jedinečné identifikační číslo pro každou součástku. + + + Designator + Volitelné + Řetězec (String) + Volný údaj. Jedinečný referenční označovač součástky na desce plošných spojů, např. „R1“ pro odpor 1.
    Je převzat do osazovacího názvu záznamu součástky. + + + Package + Volitelné + Řetězec (String) + Volný údaj. Pouzdro nebo tvar součástky, např. „0805“ pro SMD odpory.
    Pro záznam součástky není převzato. + + + Quantity + Povinné pole + Celé číslo (Integer) + Počet identických komponent, které jsou potřebné k vytvoření instance.
    Je převzat jako počet položky komponenty. + + + Designation + Povinné pole + Řetězec (String) + Popis nebo funkce součástky, např. hodnota odporu „10kΩ“ nebo kapacita kondenzátoru „100nF“.
    Je převzato do názvu záznamu součástky. + + + Supplier and ref + Volitelné + Řetězec (String) + Volný údaj. Může obsahovat např. distribuční specifickou hodnotu.
    Je převzato jako poznámka ke záznamu součástky. + + + + ]]> +
    +
    +
    project.bom_import.flash.invalid_file @@ -11064,6 +11569,18 @@ Element 3 Upravit měrnou jednotku + + + part_custom_state.new + Nový vlastní stav komponenty + + + + + part_custom_state.edit + Upravit vlastní stav komponenty + + user.aboutMe.label @@ -12765,6 +13282,30 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Sloupce, které se mají ve výchozím nastavení zobrazovat v částečných tabulkách. Pořadí položek lze změnit pomocí funkce drag & drop. + + + settings.behavior.table.assemblies_default_columns + Výchozí sloupce pro tabulky sestav + + + + + settings.behavior.table.assemblies_default_columns.help + Sloupce, které by měly být zobrazeny ve výchozím nastavení v tabulkách sestav. Pořadí prvků lze změnit přetažením. + + + + + settings.behavior.table.assemblies_bom_default_columns + Výchozí sloupce pro kusovníky sestav + + + + + settings.behavior.table.assemblies_bom_default_columns.help + Sloupce, které by měly být zobrazeny ve výchozím nastavení v kusovnících sestav. Pořadí prvků lze změnit přetažením. + + settings.ips.oemsecrets @@ -12975,6 +13516,18 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Pokud potřebujete směnné kurzy mezi měnami mimo eurozónu, můžete zde zadat API klíč z fixer.io. + + + settings.misc.assembly + Sestavy + + + + + settings.misc.assembly.useIpnPlaceholderInName + Použít zástupný symbol %%ipn%% v názvu sestavy. Zástupný symbol bude při ukládání nahrazen vstupem IPN. + + settings.behavior.part_info @@ -13479,5 +14032,910 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Minimální šířka náhledu (px) + + + part.table.name.value.for_part + %value% (Součást) + + + + + part.table.name.value.for_assembly + %value% (Sestava) + + + + + part.table.name.value.for_project + %value% (Projekt) + + + + + assembly.label + Sestava + + + + + assembly.caption + Sestava + + + + + perm.assemblies + Sestavy + + + + + assembly_bom_entry.label + Součásti + + + + + assembly.labelp + Sestavy + + + + + assembly.referencedAssembly.labelp + Odkazované 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.build.form.referencedAssembly + Sestava "%name%" + + + + + 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.bom.delete.confirm + Opravdu chcete tuto položku smazat? + + + + + assembly.add_parts_to_assembly + Přidat součásti do sestavy + + + + + part.info.add_part_to_assembly + Přidat tuto součástku do sestavy + + + + + assembly.bom.project + Projekt + + + + + assembly.bom.referencedAssembly + Sestava + + + + + 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.partOrAssembly + Součást nebo sestava + + + + + 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.csv + CSV 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.csv + Importní šablona CSV 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é pole + Číslo s plovoucí desetinnou čárkou (Float) + Musí být vyplněno a obsahovat číselnou hodnotu (Float) větší než 0.0. + + + name + Volitelné + Řetězec + Pokud je uvedeno, musí být neprázdný text. Název položky ve skupině. + + + part + Volitelné + Objekt/Array + + Pokud má být přiřazena součástka, musí být objektem/arrayem a alespoň jedno z následujících polí musí být vyplněno: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Volitelné + Celé číslo (Integer) + Celé číslo > 0. Odpovídá internímu číselnému ID součástky v databázi. + + + part.mpnr + Volitelné + Řetězec + Neprázdný text, pokud není vyplněno part.id, part.ipn ani part.name. + + + part.ipn + Volitelné + Řetězec + Neprázdný text, pokud není vyplněno part.id, part.mpnr ani part.name. + + + part.name + Volitelné + Řetězec + Neprázdný text, pokud není vyplněno part.id, part.mpnr ani part.ipn. + + + part.description + Volitelné + Řetězec nebo null + Pokud je uvedeno, musí být neprázdný řetězec nebo null. Přepíše stávající hodnotu v součástce. + + + part.manufacturer + Volitelné + Objekt/Array + + Pokud má být výrobce součástky upraven nebo má být součástka jednoznačně identifikována pomocí hodnoty part.mpnr, musí být objektem/arrayem a alespoň jedno z následujících polí musí být vyplněno: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Volitelné + Celé číslo (Integer) + Celé číslo > 0. Odpovídá internímu číselnému ID výrobce. + + + manufacturer.name + Volitelné + Řetězec + Neprázdný text, pokud není uveden manufacturer.id. + + + part.category + Volitelné + Objekt/Array + + Pokud má být kategorie součástky upravena, musí být objektem/arrayem a alespoň jedno z následujících polí musí být vyplněno: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Volitelné + Celé číslo (Integer) + Celé číslo > 0. Odpovídá internímu číselnému ID kategorie součástky. + + + category.name + Volitelné + Řetězec + Neprázdný text, pokud není uvedeno category.id. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.csv.exptected_columns + Možné sloupce: + + + + + assembly.bom_import.template.csv.table + + + + + Sloupec + Podmínka + Datový typ + Popis + + + + + quantity + Povinné pole + Číslo s plovoucí desetinnou čárkou (Float) + Musí být vyplněno a obsahovat číselnou hodnotu (Float) větší než 0.0. + + + name + Volitelné + Řetězec + Název položky ve skupině. + + + Sloupce začínající part_ + + Pokud má být přiřazena součástka, jeden z následujících sloupců musí být uveden a vyplněn: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Volitelné + Celé číslo (Integer) + Celé číslo > 0. Odpovídá internímu číselnému ID součástky v databázi. + + + part_mpnr + Volitelné + Řetězec + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_ipn nebo part_name. + + + part_ipn + Volitelné + Řetězec + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_mpnr nebo part_name. + + + part_name + Volitelné + Řetězec + Musí být uvedeno, pokud nejsou vyplněny sloupce part_id, part_mpnr nebo part_ipn. + + + part_description + Volitelné + Řetězec + Bude přeneseno do součástky a přepíše aktuální hodnotu, pokud je uveden neprázdný text. + + + Sloupce začínající part_manufacturer_ + + Pokud má být výrobce upraven nebo součástka jednoznačně identifikována pomocí part_mpnr, jeden z následujících sloupců musí být uveden a vyplněn: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Volitelné + Celé číslo (Integer) + Celé číslo > 0. Odpovídá internímu číselnému ID výrobce. + + + part_manufacturer_name + Volitelné + Řetězec + Musí být uvedeno, pokud není vyplněn sloupec part_manufacturer_id. + + + Sloupce začínající part.category_ + + Pokud má být kategorie upravena, jeden z následujících sloupců musí být uveden a vyplněn: +
      +
    • part_category_id
    • +
    • part_category_name
    • +
    + + + + part_category_id + Volitelné + Celé číslo (Integer) + Celé číslo > 0. Odpovídá internímu číselnému ID kategorie součástky. + + + part_category_name + Volitelné + Řetězec + Musí být uvedeno, pokud není vyplněn sloupec part_category_id. + + + + ]]> +
    +
    +
    + + + 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 + Volné pole. Unikátní identifikační číslo pro každou součástku. + + + Designator + Volitelné + Řetězec + Volné pole. Unikátní referenční označení součástky na PCB, např. „R1“ pro rezistor 1.
    Používá se pro název umístění v rámci součástkové skupiny. + + + Package + Volitelné + Řetězec + Volné pole. Pouzdro nebo formát součástky, např. „0805“ pro SMD rezistory.
    Není použito pro záznam součástky v rámci sestavy. + + + Quantity + Povinné pole + Celé číslo + Počet identických součástek potřebných k vytvoření jedné instance sestavy.
    Použito jako počet záznamu součástky v rámci sestavy. + + + Designation + Povinné pole + Řetězec + Popis nebo funkce součástky, např. hodnota rezistoru „10kΩ“ nebo hodnota kondenzátoru „100nF“.
    Použito jako název záznamu součástky v rámci sestavy. + + + Supplier and ref + Volitelné + Řetězec + Volné pole. Může obsahovat např. specifické informace o distributorovi.
    Používá se jako poznámka k záznamu součástky v rámci sestavy. + + + + ]]> +
    +
    +
    + + + assembly_list.all.title + Všechny sestavy + + + + + assembly.edit.tab.common + Obecné + + + + + assembly.edit.tab.advanced + Pokročilé možnosti + + + + + assembly.edit.tab.attachments + Přílohy + + + + + assembly.filter.dbId + ID databáze + + + + + assembly.filter.ipn + Interní číslo dílu (IPN) + + + + + assembly.filter.name + Název + + + + + assembly.filter.description + Popis + + + + + assembly.filter.comment + Poznámky + + + + + assembly.filter.attachments_count + Počet příloh + + + + + assembly.filter.attachmentName + Název přílohy + + + + + assemblies.create.btn + Vytvořit novou sestavu + + + + + assembly.table.id + ID + + + + + assembly.table.name + Název + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Popis + + + + + assembly.table.addedDate + Přidáno + + + + + assembly.table.lastModified + Naposledy upraveno + + + + + assembly.table.edit + Upravit + + + + + assembly.table.edit.title + Upravit sestavu + + + + + assembly.table.invalid_regex + Neplatný regulární výraz (regex) + + + + + datasource.synonym + %name% (Váš synonymum: %synonym%) + + diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index d72589864..bc1bff594 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -351,6 +351,24 @@ Eksportér alle elementer
    + + + export.readable.label + Læsbar eksport + + + + + export.readable + CSV + + + + + export.readable_bom + PDF + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -548,6 +566,12 @@ Måleenhed + + + part_custom_state.caption + Brugerdefineret komponentstatus + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -1850,6 +1874,66 @@ Underelementer vil blive flyttet opad. Advanceret + + + part.edit.tab.advanced.ipn.commonSectionHeader + Forslag uden del-inkrement + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Forslag med numeriske deleforøgelser + + + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktuel IPN-specifikation for delen + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Næste mulige IPN-specifikation baseret på en identisk delebeskrivelse + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + IPN-præfikset for den direkte kategori er tomt, angiv det i kategorien "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN-præfiks for direkte kategori + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN-præfiks for den direkte kategori og en delspecifik inkrement + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN-præfikser med hierarkisk rækkefølge af overordnede præfikser + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN-præfikser med hierarkisk rækkefølge af overordnede præfikser og en del-specifik inkrement + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Opret først en komponent, og tildel den en kategori: med eksisterende kategorier og deres egne IPN-præfikser kan IPN-betegnelsen for komponenten foreslås automatisk + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -4838,6 +4922,12 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Måleenhed + + + part.table.partCustomState + Brugerdefineret komponentstatus + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5702,6 +5792,12 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Måleenhed + + + part.edit.partCustomState + Brugerdefineret deltilstand + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -5989,6 +6085,12 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Måleenhed + + + part_custom_state.label + Brugerdefineret deltilstand + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6232,6 +6334,12 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Måleenhed + + + tree.tools.edit.part_custom_state + Brugerdefineret komponenttilstand + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -6966,6 +7074,12 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Navnefilter + + + category.edit.part_ipn_prefix + IPN-komponentförstavelse + + obsolete @@ -8502,6 +8616,12 @@ Element 3 Måleenhed + + + perm.part_custom_states + Brugerdefineret komponentstatus + + obsolete @@ -9890,6 +10010,48 @@ Element 3 Arkiveret + + + assembly.edit.status + Samlingens status + + + + + assembly.edit.ipn + Internt Partnummer (IPN) + + + + + 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 @@ -10280,12 +10442,24 @@ Element 3 f.eks. "/Kondensator \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + f.eks. "B12A" + + category.edit.partname_regex.help Et PCRE-kompatibelt regulært udtryk, som delnavnet skal opfylde. + + + category.edit.part_ipn_prefix.help + Et prefix foreslået, når IPN for en del indtastes. + + entity.select.add_hint @@ -11042,6 +11216,18 @@ Oversættelsen Typ + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11054,6 +11240,319 @@ Oversættelsen let eksisterende styklisteposter før import + + + project.import_bom.template.header.json + JSON-importskabelon + + + + + project.import_bom.template.header.csv + CSV-importskabelon + + + + + project.import_bom.template.header.kicad_pcbnew + CSV-importskabelon (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Komponentens navn i projektet + + + + + project.bom_import.template.entry.part.mpnr + Unikt produktnummer hos producenten + + + + + project.bom_import.template.entry.part.ipn + Komponentens unikke IPN + + + + + project.bom_import.template.entry.part.name + Komponentens unikke navn + + + + + project.bom_import.template.entry.part.manufacturer.name + Producentens unikke navn + + + + + project.bom_import.template.json.table + + + + + Felt + Betingelse + Datatype + Beskrivelse + + + + + quantity + Obligatorisk + Decimaltal (Float) + Skal være angivet og skal indeholde en decimaltalsværdi (Float), der er større end 0.0. + + + name + Valgfrit + String + Hvis til stede, skal det være en ikke-tom streng. Navnet på posten i stykliste. + + + part + Valgfrit + Objekt/Array + + Hvis en komponent skal knyttes, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Valgfrit + Heltal (Integer) + Heltal (Integer) > 0. Svarer til det interne numeriske ID for komponenten i Part-DB. + + + part.mpnr + Valgfrit + String + En ikke-tom streng, hvis hverken part.id, part.ipn eller part.name er angivet. + + + part.ipn + Valgfrit + String + En ikke-tom streng, hvis hverken part.id, part.mpnr eller part.name er angivet. + + + part.name + Valgfrit + String + En ikke-tom streng, hvis hverken part.id, part.mpnr eller part.ipn er angivet. + + + part.manufacturer + Valgfrit + Objekt/Array + + Hvis en komponents producent skal justeres, eller hvis komponenten skal findes entydigt via part.mpnr, skal det være et objekt/array, og mindst ét af felterne skal udfyldes: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Valgfrit + Heltal (Integer) + Heltal (Integer) > 0. Svarer til producentens interne numeriske ID. + + + manufacturer.name + Valgfrit + String + En ikke-tom streng, hvis manufacturer.id ikke er angivet. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.csv.exptected_columns + Mulige kolonner: + + + + + project.bom_import.template.csv.table + + + + + Kolonne + Betingelse + Datatype + Beskrivelse + + + + + quantity + Obligatorisk + Decimaltal (Float) + Skal være angivet og indeholde en decimaltalsværdi (Float), som er større end 0,0. + + + name + Optional + String + Hvis tilgængelig, skal det være en ikke-tom streng. Navnet på elementet inden for stykliste. + + + Kolonner, der begynder med part_ + + Hvis en komponent skal tildeles, skal mindst én af følgende kolonner være angivet og udfyldt: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Valgfri + Heltal (Integer) + Heltal (Integer) > 0. Svarer til den interne numeriske ID for komponenten i Part-DB. + + + part_mpnr + Valgfri + Streng (String) + Skal angives, hvis kolonnerne part_id, part_ipn eller part_name ikke er udfyldt. + + + part_ipn + Valgfri + Streng (String) + Skal angives, hvis kolonnerne part_id, part_mpnr eller part_name ikke er udfyldt. + + + part_name + Valgfri + Streng (String) + Skal angives, hvis kolonnerne part_id, part_mpnr eller part_ipn ikke er udfyldt. + + + Kolonner, der begynder med part_manufacturer_ + + Hvis komponentens producent skal ændres eller identificeres entydigt baseret på part_mpnr, skal mindst én af følgende kolonner være angivet og udfyldt: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Valgfri + Heltal (Integer) + Heltal (Integer) > 0. Svarer til den interne numeriske ID for producenten. + + + part_manufacturer_name + Valgfri + Streng (String) + Skal angives, hvis kolonnen part_manufacturer_id ikke er udfyldt. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Forventede kolonner: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Bemærk: Der sker ingen tilknytning til specifikke komponenter fra kategoristyringen.

    + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.table + + + + + Felt + Betingelse + Datatype + Beskrivelse + + + + + Id + Valgfrit + Heltal (Integer) + Fri opgave. Et entydigt identifikationsnummer for hver komponent. + + + Designator + Valgfrit + Streng (String) + Fri opgave. En entydig referencemarkering for komponenten på PCB'et, fx "R1" for modstand 1.
    Bliver overført til monteringsnavnet på komponentindgangen. + + + Package + Valgfrit + Streng (String) + Fri opgave. Komponentens pakning eller form, fx "0805" for SMD-modstande.
    Bliver ikke overført til komponentindgangen. + + + Quantity + Obligatorisk felt + Heltal (Integer) + Antallet af identiske komponenter, der kræves for at oprette en instans.
    Overtages som antallet af komponentposter. + + + Designation + Obligatorisk felt + Streng (String) + Beskrivelse eller funktion af komponenten, fx modstandsværdi "10kΩ" eller kondensatorværdi "100nF".
    Bliver overført til komponentindgangens navn. + + + Supplier and ref + Valgfrit + Streng (String) + Fri opgave. Kan eksempelvis indeholde en distributørspecifik værdi.
    Bliver overført som en note til komponentindgangen. + + + + ]]> +
    +
    +
    project.bom_import.clear_existing_bom.help @@ -11096,6 +11595,18 @@ Oversættelsen Ret måleenhed + + + part_custom_state.new + Ny brugerdefineret komponentstatus + + + + + part_custom_state.edit + Rediger brugerdefineret komponentstatus + + user.aboutMe.label @@ -12196,5 +12707,952 @@ 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% (Samling) + + + + + part.table.name.value.for_project + %value% (Projekt) + + + + + assembly.label + Samling + + + + + assembly.caption + Samling + + + + + perm.assemblies + Samlinger + + + + + assembly_bom_entry.label + Komponenter + + + + + assembly.labelp + Samlinger + + + + + assembly.referencedAssembly.labelp + Refererede 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.build.form.referencedAssembly + Samling "%name%" + + + + + 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.bom.delete.confirm + Vil du virkelig slette denne post? + + + + + assembly.add_parts_to_assembly + Tilføj dele til samlingen + + + + + part.info.add_part_to_assembly + Tilføj denne del til en samling + + + + + assembly.bom.project + Projekt + + + + + assembly.bom.referencedAssembly + Sammenstilling + + + + + 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.partOrAssembly + Del eller samling + + + + + 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.csv + CSV til en samling + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.type.kicad_schematic + KiCAD Skematisk editor BOM (CSV-fil) + + + + + 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.csv + Importskabelon CSV 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 felt + Flydende punkt-tal (Float) + Skal være udfyldt og indeholde en flydende værdi (Float), der er større end 0.0. + + + name + Valgfrit + Streng + Hvis det er angivet, skal det være en ikke-tom streng. Navn på posten inden for samlingen. + + + part + Valgfrit + Objekt/Array + + Hvis en del skal tildeles, skal det være et objekt/array, og mindst et af følgende felter skal være udfyldt: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Valgfrit + Heltal (Integer) + Heltal > 0. Tilsvarer den interne nummer-ID for delen i database. + + + part.mpnr + Valgfrit + Streng + Ikke-tom streng, hvis ingen part.id-, part.ipn- eller part.name-værdi er angivet. + + + part.ipn + Valgfrit + Streng + Ikke-tom streng, hvis ingen part.id-, part.mpnr- eller part.name-værdi er angivet. + + + part.name + Valgfrit + Streng + Ikke-tom streng, hvis ingen part.id-, part.mpnr- eller part.ipn-værdi er angivet. + + + part.description + Valgfrit + Streng eller null + Hvis angivet, skal det være en ikke-tom streng eller null. Værdien bliver overskrevet i delen. + + + part.manufacturer + Valgfrit + Objekt/Array + + Hvis producenten af en del skal ændres eller entydigt søges ved hjælp af part.mpnr-værdien, skal det være et objekt/array og mindst et af følgende felter skal være udfyldt: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Valgfrit + Heltal (Integer) + Heltal > 0. Tilsvarer den interne nummer-ID for producenten. + + + manufacturer.name + Valgfrit + Streng + Ikke-tom streng, hvis ingen manufacturer.id er angivet. + + + part.category + Valgfrit + Objekt/Array + + Hvis en delens kategori skal ændres, skal det være et objekt/array, og mindst et af følgende felter skal være udfyldt: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Valgfrit + Heltal (Integer) + Heltal > 0. Tilsvarer den interne nummer-ID for delens kategori. + + + category.name + Valgfrit + Streng + Ikke-tom streng, hvis ingen category.id er angivet. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.csv.exptected_columns + Mulige kolonner: + + + + + assembly.bom_import.template.csv.table + + + + + Kolonne + Betingelse + Datatype + Beskrivelse + + + + + quantity + Påkrævet felt + Flydende punkt-tal (Float) + Skal være udfyldt og indeholde en flydende værdi (Float), der er større end 0.0. + + + name + Valgfrit + Streng + Navnet på posten inden for stykliste. + + + Kolonner, der starter med part_ + + Hvis en del skal tildeles, skal en af følgende kolonner være angivet og udfyldt: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Valgfrit + Heltal (Integer) + Heltal > 0. Tilsvarer den interne nummer-ID for delen i databasen. + + + part_mpnr + Valgfrit + Streng + Skal angives, hvis ingen af kolonnerne part_id, part_ipn, eller part_name er udfyldt. + + + part_ipn + Valgfrit + Streng + Skal angives, hvis ingen af kolonnerne part_id, part_mpnr eller part_name er udfyldt. + + + part_name + Valgfrit + Streng + Skal angives, hvis ingen af kolonnerne part_id, part_mpnr eller part_ipn er udfyldt. + + + part_description + Valgfrit + Streng + Vil blive overført og overskrive værdien for delen, hvis en ikke-tom streng er angivet. + + + Kolonner, der starter med part_manufacturer_ + + Hvis producenten for en del skal ændres eller søges entydigt ved hjælp af part_mpnr, skal en af følgende kolonner være angivet og udfyldt: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Valgfrit + Heltal (Integer) + Heltal > 0. Tilsvarer den interne nummer-ID for producenten. + + + part_manufacturer_name + Valgfrit + Streng + Skal angives, hvis ingen part_manufacturer_id er udfyldt. + + + Kolonner, der starter med part.category_ + + Hvis en dels kategori skal ændres, skal en af følgende kolonner være angivet og udfyldt: +
      +
    • part_category_id
    • +
    • part_category_name
    • +
    + + + + part_category_id + Valgfrit + Heltal (Integer) + Heltal > 0. Tilsvarer den interne nummer-ID for delens kategori. + + + part_category_name + Valgfrit + Streng + Skal angives, hvis ingen part_category_id er udfyldt. + + + + ]]> +
    +
    +
    + + + 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 + Valgfrit + Heltal + Fri tekst. Et unikt identifikationsnummer for hver komponent. + + + Designator + Valgfrit + Streng + Fri tekst. En unik referencebetegnelse for komponenten på PCB'en, f.eks. “R1” for modstand 1.
    Bruges som navnet på placeringen i komponentgruppen. + + + Package + Valgfrit + Streng + Fri tekst. Komponentens kabinet eller formfaktor, f.eks. “0805” for SMD-modstande.
    Ikke inkluderet som komponentoplysning i samlingen. + + + Quantity + Påkrævet + Heltal + Antallet af identiske komponenter, der kræves for at oprette en enkelt forekomst af samlingen.
    Bruges som antal af komponentoplysning i samlingen. + + + Designation + Påkrævet + Streng + Beskrivelse eller funktion af komponenten, f.eks. modstandsværdi “10kΩ” eller kondensatorværdi “100nF”.
    Bruges som navnet på komponentoplysningen i samlingen. + + + Supplier and ref + Valgfrit + Streng + Fri tekst. Kan indeholde, for eksempel, specifikke oplysninger fra en distributør.
    Bruges som en note til komponentoplysningen i samlingen. + + + + ]]> +
    +
    +
    + + + assembly_list.all.title + Alle samlinger + + + + + assembly.edit.tab.common + Generelt + + + + + assembly.edit.tab.advanced + Avancerede indstillinger + + + + + assembly.edit.tab.attachments + Vedhæftede filer + + + + + assembly.filter.dbId + Database-ID + + + + + assembly.filter.ipn + Internt delnummer (IPN) + + + + + assembly.filter.name + Navn + + + + + assembly.filter.description + Beskrivelse + + + + + assembly.filter.comment + Kommentarer + + + + + assembly.filter.attachments_count + Antal vedhæftninger + + + + + assembly.filter.attachmentName + Vedhæftningens navn + + + + + assemblies.create.btn + Opret ny samling + + + + + assembly.table.id + ID + + + + + assembly.table.name + Navn + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Beskrivelse + + + + + assembly.table.referencedAssemblies + Referencerede forsamlinger + + + + + assembly.table.addedDate + Tilføjet + + + + + assembly.table.lastModified + Sidst ændret + + + + + assembly.table.edit + Rediger + + + + + assembly.table.edit.title + Rediger samling + + + + + assembly.table.invalid_regex + Ugyldigt regulært udtryk (regex) + + + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Navn + + + + + assembly.bom.table.quantity + Mængde + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Beskrivelse + + + + + datasource.synonym + %name% (Dit synonym: %synonym%) + + diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 9fb3f6ef6..8ef5da894 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -548,6 +548,12 @@ Maßeinheit + + + part_custom_state.caption + Benutzerdefinierter Bauteilstatus + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -1004,6 +1010,24 @@ Subelemente werden beim Löschen nach oben verschoben. Unterelemente auch exportieren + + + export.readable.label + Lesbarer Export + + + + + export.readable + CSV + + + + + export.readable_bom + PDF + + Part-DB1\templates\AdminPages\_export_form.html.twig:39 @@ -1841,6 +1865,66 @@ Subelemente werden beim Löschen nach oben verschoben. Erweiterte Optionen + + + part.edit.tab.advanced.ipn.commonSectionHeader + Vorschläge ohne Teil-Inkrement + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Vorschläge mit numerischen Teil-Inkrement + + + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktuelle IPN-Angabe des Bauteils + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Nächstmögliche IPN-Angabe auf Basis der identischen Bauteil-Beschreibung + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + IPN-Präfix der direkten Kategorie leer, geben Sie einen Präfix in Kategorie "%name%" an + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN-Präfix der direkten Kategorie + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN-Präfix der direkten Kategorie und eines teilspezifischen Inkrements + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN-Präfixe mit hierarchischer Kategorienreihenfolge der Elternpräfixe + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN-Präfixe mit hierarchischer Kategorienreihenfolge der Elternpräfixe und ein teilsspezifisches Inkrement + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Bitte erstellen Sie zuerst ein Bauteil und weisen Sie dieses einer Kategorie zu: mit vorhandenen Kategorien und derene eigenen IPN-Präfix kann die IPN-Angabe für das jeweilige Teil automatisch vorgeschlagen werden + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -4740,6 +4824,24 @@ 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.table.name.value.for_project + %value% (Projekt) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -4830,6 +4932,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr Maßeinheit + + + part.table.partCustomState + Benutzerdefinierter Bauteilstatus + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5694,6 +5802,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr Maßeinheit + + + part.edit.partCustomState + Benutzerdefinierter Bauteilstatus + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -5981,6 +6095,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr Maßeinheit + + + part_custom_state.label + Benutzerdefinierter Bauteilstatus + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6224,6 +6344,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr Maßeinheiten + + + tree.tools.edit.part_custom_state + Benutzerdefinierter Bauteilstatus + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -6958,6 +7084,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr Namensfilter + + + category.edit.part_ipn_prefix + Bauteil IPN-Präfix + + obsolete @@ -8497,6 +8629,12 @@ Element 1 -> Element 1.2 Maßeinheiten + + + perm.part_custom_states + Benutzerdefinierter Bauteilstatus + + obsolete @@ -9866,6 +10004,48 @@ Element 1 -> Element 1.2 Archiviert + + + assembly.edit.status + Status Baugruppe + + + + + assembly.edit.ipn + Internal Part Number (IPN) + + + + + 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 @@ -10256,12 +10436,24 @@ Element 1 -> Element 1.2 z.B. "/Kondensator \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + z.B. "B12A" + + category.edit.partname_regex.help Ein PCRE-kompatibler regulärer Ausdruck, den der Bauteilename erfüllen muss. + + + category.edit.part_ipn_prefix.help + Ein Präfix, der bei der IPN-Eingabe eines Bauteils vorgeschlagen wird. + + entity.select.add_hint @@ -10808,6 +11000,12 @@ Element 1 -> Element 1.2 Maßeinheit + + + log.element_edited.changed_fields.partCustomState + Benutzerdefinierter Bauteilstatus + + log.element_edited.changed_fields.expiration_date @@ -11012,6 +11210,18 @@ Element 1 -> Element 1.2 Typ + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11030,6 +11240,319 @@ Element 1 -> Element 1.2 Wenn diese Option ausgewählt ist, werden alle bereits im Projekt existierenden BOM Einträge gelöscht und mit den importierten BOM Daten überschrieben. + + + project.import_bom.template.header.json + Import-Vorlage JSON + + + + + project.import_bom.template.header.csv + Import-Vorlage CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Import-Vorlage CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Name des Bauteils im Projekt + + + + + project.bom_import.template.entry.part.mpnr + Eindeutige Produktnummer innerhalb des Herstellers + + + + + project.bom_import.template.entry.part.ipn + Eideutige IPN des Bauteils + + + + + project.bom_import.template.entry.part.name + Eindeutiger Name des Bauteils + + + + + project.bom_import.template.entry.part.manufacturer.name + Eindeutiger Name des Herstellers + + + + + project.bom_import.template.json.table + + + + + Feld + Bedingung + Datentyp + Beschreibung + + + + + quantity + Pflichtfeld + Gleitkommazahl (Float) + Muss gegeben sein und enthält einen Gleitkommawert (Float), der größer als 0.0 ist. + + + name + Optional + String + Falls vorhanden, muss es ein nicht-leerer String sein. Name des Eintrags innerhalb der Stückliste. + + + part + Optional + Objekt/Array + + Falls ein Bauteil zugeordnet werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. + + + part.mpnr + Optional + String + Nicht-leerer String, falls keine part.id-, part-ipn- bzw. part.name-Angabe gegeben ist. + + + part.ipn + Optional + String + Nicht-leerer String, falls keine part.id-, part.mpnr bzw. part.name-Angabe gegeben ist. + + + part.name + Optional + String + Nicht-leerer String, falls keine part.id-, part.mpnr- bzw. part.ipn-Angabe gegeben ist. + + + part.manufacturer + Optional + Objekt/Array + + Falls der Hersteller eines Bauteils mit angepasst werden oder das Bauteil anhand der part.mpnr-Angabe eindeutig gesucht werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID des Herstellers. + + + manufacturer.name + Optional + String + Nicht-leerer String, falls keine manufacturer.id-Angabe gegeben ist. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.csv.exptected_columns + Mögliche Spalten: + + + + + project.bom_import.template.csv.table + + + + + Spalte + Bedingung + Datentyp + Beschreibung + + + + + quantity + Pflichtfeld + Gleitkommazahl (Float) + Muss gegeben sein und enthält einen Gleitkommawert (Float), der größer als 0.0 ist. + + + name + Optional + String + Name des Eintrags innerhalb der Stückliste. + + + Spalten beginnend mit part_ + + Falls ein Bauteil zugeordnet werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. + + + part_mpnr + Optional + String + Anzugeben, falls keine part_id-, part_ipn- bzw. part_name-Spalte ausgefüllt gegeben ist. + + + part_ipn + Optional + String + Anzugeben, falls keine part_id-, part_mpnr- bzw. part_name-Spalte ausgefüllt gegeben ist. + + + part_name + Optional + String + Anzugeben, falls keine part_id-, part_mpnr- bzw. part_ipn-Spalte ausgefüllt gegeben ist. + + + Spalten beginnend mit part_manufacturer_ + + Falls der Hersteller eines Bauteils mit angepasst werden oder das Bauteil anhand der part_mpnr-Angabe eindeutig gesucht werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID des Herstellers. + + + part_manufacturer_name + Optional + String + Anzugeben, falls keine part_manufacturer_id-Spalte ausgefüllt gegeben ist. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Erwartete Spalten: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Hinweis: Es findet keine Zuordnung zu konkreten Bauteilen aus der Kategorie-Verwaltung statt.

    + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.table + + + + + Feld + Bedingung + Datentyp + Beschreibung + + + + + Id + Optional + Ganzzahl (Integer) + Offene Angabe. Eine eindeutige Identifikationsnummer für jedes Bauteil. + + + Designator + Optional + String + Offene Angabe. Ein eindeutiger Referenzbezeichner des Bauteils auf der Leiterplatte, z.B. „R1“ für Widerstand 1.
    Wird in den Bestückungsnamen des Bauteil-Eintrags übernommen. + + + Package + Optional + String + Offene Angabe. Das Gehäuse oder die Bauform des Bauteils, z.B. „0805“ für SMD-Widerstände.
    Wird für ein Bauteil-Eintrag nicht übernommen. + + + Quantity + Pflichtfeld + Ganzzahl (Integer) + Anzahl der identischen Bauteile, die benötigt werden, um eine Instanz zu erstellen.
    Wird als Anzahl des Bauteil-Eintrags übernommen. + + + Designation + Pflichtfeld + String + Beschreibung oder Funktion des Bauteils, z.B. Widerstandswert „10kΩ“ oder Kondensatorwert „100nF“.
    Wird in den Namen des Bauteil-Eintrags übernommen. + + + Supplier and ref + Optional + String + Offene Angabe. Kann z.B. Distributor spezifischen Wert enthalten.
    Wird als Notiz zum Bauteil-Eintrag übernommen. + + + + ]]> +
    +
    +
    project.bom_import.flash.invalid_file @@ -11066,6 +11589,18 @@ Element 1 -> Element 1.2 Bearbeite Maßeinheit + + + part_custom_state.new + Neuer benutzerdefinierter Bauteilstatus + + + + + part_custom_state.edit + Bearbeite benutzerdefinierten Bauteilstatus + + user.aboutMe.label @@ -12767,6 +13302,30 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Die Spalten, die standardmäßig in Bauteiltabellen angezeigt werden sollen. Die Reihenfolge der Elemente kann per Drag & Drop geändert werden. + + + settings.behavior.table.assemblies_default_columns + Standardmäßige Spalten für Baugruppentabellen + + + + + settings.behavior.table.assemblies_default_columns.help + Die Spalten, die standardmäßig in Baugruppentabellen angezeigt werden sollen. Die Reihenfolge der Elemente kann per Drag & Drop geändert werden. + + + + + settings.behavior.table.assemblies_bom_default_columns + Standardmäßige Spalten für Baugruppen-Stücklisten + + + + + settings.behavior.table.assemblies_bom_default_columns.help + Die Spalten, die standardmäßig in Baugruppen-Stücklisten angezeigt werden sollen. Die Reihenfolge der Elemente kann per Drag & Drop geändert werden. + + settings.ips.oemsecrets @@ -12851,6 +13410,767 @@ 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.referencedAssembly.labelp + Referenzierte 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 + Importiere Bauteil-Liste + + + + + 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 + + + + + asssembly.build.form.referencedAssembly + Baugruppe "%name%" + + + + + 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.bom.delete.confirm + Wollen sie diesen Eintrag wirklich löschen? + + + + + assembly.add_parts_to_assembly + Bauteile zur Baugruppe hinzufügen + + + + + part.info.add_part_to_assembly + Dieses Bauteil zu einer Baugruppe hinzufügen + + + + + assembly.bom.project + Projekt + + + + + assembly.bom.referencedAssembly + Baugruppe + + + + + 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.partOrAssembly + Bauteil oder Baugruppe + + + + + 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.csv + CSV für eine Baugruppe + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.type.kicad_schematic + KiCAD Schaltplaneditor BOM (CSV Datei) + + + + + 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.csv + Import-Vorlage CSV 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. Name des Eintrags innerhalb der Stückliste. + + + part + Optional + Objekt/Array + + Falls ein Bauteil zugeordnet werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. + + + part.mpnr + Optional + String + Nicht-leerer String, falls keine part.id-, part-ipn- bzw. part.name-Angabe gegeben ist. + + + part.ipn + Optional + String + Nicht-leerer String, falls keine part.id-, part.mpnr bzw. part.name-Angabe gegeben ist. + + + part.name + Optional + String + Nicht-leerer String, falls keine part.id-, part.mpnr- bzw. part.ipn-Angabe gegeben ist. + + + part.description + Optional + String oder null + Falls vorhanden, muss es ein nicht-leerer String sein oder null. Wird in das Bauteil übernommen, d.h. der dortige Wert überschrieben. + + + part.manufacturer + Optional + Objekt/Array + + Falls der Hersteller eines Bauteils mit angepasst werden oder das Bauteil anhand der part.mpnr-Angabe eindeutig gesucht werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID des Herstellers. + + + manufacturer.name + Optional + String + Nicht-leerer String, falls keine manufacturer.id-Angabe gegeben ist. + + + part.category + Optional + Objekt/Array + + Falls die Kategorie eine Bauteils mit angepasst werden soll, muss es ein Objekt/Array und mindestens eines der Felder ausgefüllt sein: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + 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.csv.exptected_columns + Mögliche Spalten: + + + + + assembly.bom_import.template.csv.table + + + + + Spalte + Bedingung + Datentyp + Beschreibung + + + + + quantity + Pflichtfeld + Gleitkommazahl (Float) + Muss gegeben sein und enthält einen Gleitkommawert (Float), der größer als 0.0 ist. + + + name + Optional + String + Name des Eintrags innerhalb der Baugruppe. + + + Spalten beginnend mit part_ + + Falls ein Bauteil zugeordnet werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der Part-DB internen numerischen ID des Bauteils. + + + part_mpnr + Optional + String + Anzugeben, falls keine part_id-, part_ipn- bzw. part_name-Spalte ausgefüllt gegeben ist. + + + part_ipn + Optional + String + Anzugeben, falls keine part_id-, part_mpnr- bzw. part_name-Spalte ausgefüllt gegeben ist. + + + part_name + Optional + String + Anzugeben, falls keine part_id-, part_mpnr- bzw. part_ipn-Spalte ausgefüllt gegeben ist. + + + part_description + Optional + String + Wird in das Bauteil übernommen, d.h. der dortige Wert überschrieben sofern ein nicht-leerer String gegeben ist. + + + Spalten beginnend mit part_manufacturer_ + + Falls der Hersteller eines Bauteils mit angepasst werden oder das Bauteil anhand der part_mpnr-Angabe eindeutig gesucht werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID des Herstellers. + + + part_manufacturer_name + Optional + String + Anzugeben, falls keine part_manufacturer_id-Spalte ausgefüllt gegeben ist. + + + Spalten beginnend mit part.category_ + + Falls die Kategorie eines Bauteils mit angepasst werden soll, muss eine der folgenden Spalten gegeben und ausgefüllt sein: +
      +
    • part_category_id
    • +
    • part_category_name
    • +
    + + + + part_category_id + Optional + Ganzzahl (Integer) + Ganzzahl (Integer) > 0. Entspricht der internen numerischen ID der Kategorie des Bauteils. + + + part_category_name + Optional + String + Anzugeben, falls keine part_category_id-Spalte ausgefüllt gegeben ist. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + 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 in den Bestückungsnamen des Bauteil-Eintrags in der Baugruppe übernommen. + + + Package + Optional + String + Offene Angabe. Das Gehäuse oder die Bauform des Bauteils, z.B. „0805“ für SMD-Widerstände.
    Wird für ein Bauteil-Eintrag innerhalb der Baugruppe nicht übernommen. + + + Quantity + Pflichtfeld + Ganzzahl (Integer) + Anzahl der identischen Bauteile, die benötigt werden, um eine Instanz der Baugruppe zu erstellen.
    Wird als Anzahl des Bauteil-Eintrags innerhalb der Baugruppe übernommen. + + + Designation + Pflichtfeld + String + Beschreibung oder Funktion des Bauteils, z.B. Widerstandswert „10kΩ“ oder Kondensatorwert „100nF“.
    Wird in den Namen des Bauteil-Eintrags innerhalb der Baugruppe übernommen. + + + Supplier and ref + Optional + String + Offene Angabe. Kann z.B. Distributor spezifischen Wert enthalten.
    Wird als Notiz zum Bauteil-Eintrag innerhalb der Baugruppe übernommen. + + + + ]]> +
    +
    +
    part.table.actions.error @@ -12977,6 +14297,18 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Wenn Sie Wechselkurse zwischen Nicht-Euro-Währungen benötigen, können Sie hier einen API-Schlüssel von fixer.io eingeben. + + + settings.misc.assembly + Baugruppen + + + + + settings.misc.assembly.useIpnPlaceholderInName + Verwenden Sie einen %%ipn%%-Platzhalter im Namen einer Baugruppe. Der Platzhalter wird beim Speichern durch die eingegebene IPN ersetzt. + + settings.behavior.part_info @@ -13481,5 +14813,173 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Min. Vorschaubilde-Breite (px) + + + assembly_list.all.title + Alle Baugruppen + + + + + assembly.edit.tab.common + Allgemein + + + + + assembly.edit.tab.advanced + Erweiterte Optionen + + + + + assembly.edit.tab.attachments + Dateianhänge + + + + + assembly.filter.dbId + Datenbank ID + + + + + assembly.filter.ipn + Internal Part Number (IPN) + + + + + assembly.filter.name + Name + + + + + assembly.filter.description + Beschreibung + + + + + assembly.filter.comment + Notizen + + + + + assembly.filter.attachments_count + Anzahl der Anhänge + + + + + assembly.filter.attachmentName + Name des Anhangs + + + + + assemblies.create.btn + Neue Baugruppe anlegen + + + + + assembly.table.id + ID + + + + + assembly.table.name + Name + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Beschreibung + + + + + assembly.table.referencedAssemblies + Referenzierte Baugruppen + + + + + assembly.table.addedDate + Hinzugefügt + + + + + assembly.table.lastModified + Zuletzt bearbeitet + + + + + assembly.table.edit + Ändern + + + + + assembly.table.edit.title + Baugruppe ändern + + + + + assembly.table.invalid_regex + Ungültiger regulärer Ausdruck (regex) + + + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Name + + + + + assembly.bom.table.quantity + Stückzahl + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Beschreibung + + + + + datasource.synonym + %name% (Ihr Synonym: %synonym%) + + diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index cc17d9be4..f28c805eb 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -228,6 +228,24 @@ Εξαγωγή όλων των στοιχείων + + + export.readable.label + Αναγνώσιμη εξαγωγή + + + + + export.readable + CSV + + + + + export.readable_bom + PDF + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -318,6 +336,12 @@ Μονάδα μέτρησης + + + part_custom_state.caption + Προσαρμοσμένη κατάσταση εξαρτήματος + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -1535,5 +1559,1084 @@ Επεξεργασία + + + perm.part_custom_states + Προσαρμοσμένη κατάσταση εξαρτήματος + + + + + tree.tools.edit.part_custom_state + Προσαρμοσμένη κατάσταση εξαρτήματος + + + + + part_custom_state.new + Νέα προσαρμοσμένη κατάσταση εξαρτήματος + + + + + part_custom_state.edit + Επεξεργασία προσαρμοσμένης κατάστασης εξαρτήματος + + + + + part_custom_state.label + Προσαρμοσμένη κατάσταση μέρους + + + + + log.element_edited.changed_fields.partCustomState + Προσαρμοσμένη κατάσταση εξαρτήματος + + + + + part.edit.partCustomState + Προσαρμοσμένη κατάσταση εξαρτήματος + + + + + part.table.partCustomState + Προσαρμοσμένη κατάσταση μέρους + + + + + part.table.name.value.for_part + %value% (Μέρος) + + + + + part.table.name.value.for_assembly + %value% (Συναρμολόγηση) + + + + + part.table.name.value.for_project + %value% (Έργο) + + + + + assembly.edit.status + Κατάσταση συναρμολόγησης + + + + + assembly.status.draft + Προσχέδιο + + + + + assembly.status.planning + Υπό σχεδιασμό + + + + + assembly.status.in_production + Σε παραγωγή + + + + + assembly.status.finished + Ολοκληρώθηκε + + + + + assembly.status.archived + Αρχειοθετήθηκε + + + + + assembly.label + Σύνολο + + + + + assembly.caption + Σύνολο + + + + + perm.assemblies + Συναρμολογήσεις + + + + + assembly_bom_entry.label + Μέρη + + + + + assembly.labelp + Συναρμολογήσεις + + + + + assembly.referencedAssembly.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.build.form.referencedAssembly + Συναρμολόγηση "%name%" + + + + + assembly.builds.no_stocked_builds + Αποθηκευμένα κατασκευασμένα κομμάτια + + + + + assembly.info.bom_entries_count + Εξαρτήματα + + + + + assembly.info.sub_assemblies_count + Υποομάδες + + + + + assembly.builds.stocked + σε απόθεμα + + + + + assembly.builds.needed + απαιτούμενο + + + + + assembly.bom.delete.confirm + Θέλετε πραγματικά να διαγράψετε αυτήν την εγγραφή; + + + + + assembly.add_parts_to_assembly + Προσθήκη εξαρτημάτων στη συναρμολόγηση + + + + + part.info.add_part_to_assembly + Προσθέστε αυτό το εξάρτημα σε μια συναρμολόγηση + + + + + assembly.bom.project + έργο + + + + + assembly.bom.referencedAssembly + Συναρμολόγηση + + + + + 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.partOrAssembly + Μέρος ή συναρμολόγηση + + + + + 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.csv + CSV για μια συναρμολόγηση + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.type.kicad_schematic + KiCAD Σχηματικό BOM (CSV αρχείο) + + + + + 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.csv + Πρότυπο CSV εισαγωγής για μια συναρμολόγηση + + + + + 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.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID του εξαρτήματος στη βάση δεδομένων. + + + part.mpnr + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το part.id, part.ipn ή part.name. + + + part.ipn + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το part.id, part.mpnr ή part.name. + + + part.name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το part.id, part.mpnr ή part.ipn. + + + part.description + Προαιρετικό + Χαρακτηριστική ακολουθία ή null + Αν υπάρχει, πρέπει να είναι μη κενό κείμενο ή null. Υπερισχύει της υπάρχουσας τιμής στο εξάρτημα. + + + part.manufacturer + Προαιρετικό + Αντικείμενο/Πίνακας + + Αν ο κατασκευαστής ενός εξαρτήματος χρειάζεται να αλλάξει ή να αναζητηθεί μονοσήμαντα μέσω της τιμής part.mpnr, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα παρακάτω πεδία να είναι συμπληρωμένα: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID του κατασκευαστή. + + + manufacturer.name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το manufacturer.id. + + + part.category + Προαιρετικό + Αντικείμενο/Πίνακας + + Αν χρειάζεται να τροποποιηθεί η κατηγορία του εξαρτήματος, πρέπει να είναι αντικείμενο/πίνακας και τουλάχιστον ένα από τα παρακάτω πεδία να είναι συμπληρωμένα: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID της κατηγορίας εξαρτήματος. + + + category.name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Μη κενό κείμενο, αν δεν έχει συμπληρωθεί το category.id. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.csv.exptected_columns + Δυνατές στήλες: + + + + + assembly.bom_import.template.csv.table + + + + + Στήλη + Προϋπόθεση + Τύπος δεδομένων + Περιγραφή + + + + + quantity + Υποχρεωτικό πεδίο + Αριθμός κινητής υποδιαστολής (Float) + Πρέπει να είναι συμπληρωμένος και να περιέχει μια αριθμητική τιμή (Float) μεγαλύτερη από 0.0. + + + name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Το όνομα του είδους μέσα στη συλλογή. + + + Στήλες που ξεκινούν με part_ + + Αν χρειάζεται να αποδοθεί εξάρτημα, πρέπει να συμπληρωθεί μία από τις παρακάτω στήλες: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID του εξαρτήματος στη βάση δεδομένων. + + + part_mpnr + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσουν οι part_id, part_ipn ή part_name. + + + part_ipn + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσουν οι part_id, part_mpnr ή part_name. + + + part_name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσουν οι part_id, part_mpnr ή part_ipn. + + + part_description + Προαιρετικό + Χαρακτηριστική ακολουθία + Θα μεταφερθεί και θα αντικαταστήσει την τιμή στο εξάρτημα, αν δοθεί μια μη κενή ακολουθία. + + + Στήλες που ξεκινούν με part_manufacturer_ + + Αν ο κατασκευαστής του εξαρτήματος πρέπει να αλλάξει ή να αναζητηθεί μονοσήμαντα μέσω της part_mpnr, πρέπει να συμπληρωθεί μία από τις παρακάτω στήλες: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID του κατασκευαστή. + + + part_manufacturer_name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσει το πεδίο part_manufacturer_id. + + + Στήλες που ξεκινούν με part.category_ + + Αν η κατηγορία του εξαρτήματος πρέπει να αλλάξει, πρέπει να συμπληρωθεί μία από τις παρακάτω στήλες: +
      +
    • part_category_id
    • +
    • part_category_name
    • +
    + + + + part_category_id + Προαιρετικό + Ακέραιος αριθμός (Integer) + Ακέραιος αριθμός > 0. Αντιστοιχεί στο εσωτερικό αριθμητικό ID της κατηγορίας του εξαρτήματος. + + + part_category_name + Προαιρετικό + Χαρακτηριστική ακολουθία (String) + Πρέπει να συμπληρωθεί αν δεν γεμίσει το πεδίο part_category_id. + + + + ]]> +
    +
    +
    + + + 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 + Προαιρετικό + Ακέραιος + Πεδίο ελεύθερης μορφής. Ένας μοναδικός αριθμός ταυτοποίησης για κάθε εξάρτημα. + + + Designator + Προαιρετικό + Συμβολοσειρά + Πεδίο ελεύθερης μορφής. Ένας μοναδικός αναγνωριστικός δείκτης για το εξάρτημα στην πλακέτα PCB, π.χ. "R1" για τον αντιστάτη 1.
    Χρησιμοποιείται για την ονομασία της θέσης στην ομάδα εξαρτημάτων. + + + Package + Προαιρετικό + Συμβολοσειρά + Πεδίο ελεύθερης μορφής. Η θήκη ή ο μορφολογικός τύπος του εξαρτήματος, π.χ. "0805" για τους SMD αντιστάτες.
    Δεν περιλαμβάνεται στις πληροφορίες εξαρτήματος της συναρμολόγησης. + + + Quantity + Απαιτείται + Ακέραιος + Ο αριθμός πανομοιότυπων εξαρτημάτων που απαιτούνται για τη δημιουργία μιας μοναδικής παρουσίας της συναρμολόγησης.
    Χρησιμοποιείται ως ποσότητα στις πληροφορίες εξαρτήματος της συναρμολόγησης. + + + Designation + Απαιτείται + Συμβολοσειρά + Η περιγραφή ή η λειτουργία του εξαρτήματος, π.χ. τιμή αντιστάτη "10kΩ" ή τιμή πυκνωτή "100nF".
    Χρησιμοποιείται ως το όνομα στις πληροφορίες εξαρτήματος της συναρμολόγησης. + + + Supplier and ref + Προαιρετικό + Συμβολοσειρά + Πεδίο ελεύθερης μορφής. Μπορεί να περιλαμβάνει, για παράδειγμα, συγκεκριμένες πληροφορίες για τον προμηθευτή.
    Χρησιμοποιείται ως σημείωση για τις πληροφορίες εξαρτήματος της συναρμολόγησης. + + + + ]]> +
    +
    +
    + + + assembly_list.all.title + Όλες οι συναρμολογήσεις + + + + + assembly.edit.tab.common + Γενικά + + + + + assembly.edit.tab.advanced + Προηγμένες επιλογές + + + + + assembly.edit.tab.attachments + Συνημμένα + + + + + assembly.filter.dbId + Αναγνωριστικό βάσης δεδομένων + + + + + assembly.filter.ipn + Εσωτερικός αριθμός εξαρτήματος (IPN) + + + + + assembly.filter.name + Όνομα + + + + + assembly.filter.description + Περιγραφή + + + + + assembly.filter.comment + Σχόλια + + + + + assembly.filter.attachments_count + Αριθμός συνημμένων + + + + + assembly.filter.attachmentName + Όνομα συνημμένου + + + + + assemblies.create.btn + Δημιουργία νέας συναρμολόγησης + + + + + assembly.table.id + Αναγνωριστικό + + + + + assembly.table.name + Όνομα + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Περιγραφή + + + + + assembly.table.referencedAssemblies + Αναφερόμενες συναρμολογήσεις + + + + + assembly.table.addedDate + Προστέθηκε + + + + + assembly.table.lastModified + Τελευταία επεξεργασία + + + + + assembly.table.edit + Επεξεργασία + + + + + assembly.table.edit.title + Επεξεργασία συναρμολόγησης + + + + + assembly.table.invalid_regex + Μη έγκυρη κανονική έκφραση (regex) + + + + + datasource.synonym + %name% (Το συνώνυμό σας: %synonym%) + + + + + category.edit.part_ipn_prefix + Πρόθεμα εξαρτήματος IPN + + + + + category.edit.part_ipn_prefix.placeholder + π.χ. "B12A" + + + + + category.edit.part_ipn_prefix.help + Μια προτεινόμενη πρόθεμα κατά την εισαγωγή του IPN ενός τμήματος. + + + + + part.edit.tab.advanced.ipn.commonSectionHeader + Προτάσεις χωρίς αύξηση μέρους + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Προτάσεις με αριθμητικές αυξήσεις μερών + + + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Τρέχουσα προδιαγραφή IPN του εξαρτήματος + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Επόμενη δυνατή προδιαγραφή IPN βάσει της ίδιας περιγραφής εξαρτήματος + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Το IPN πρόθεμα της άμεσης κατηγορίας είναι κενό, καθορίστε το στην κατηγορία "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Πρόθεμα IPN για την άμεση κατηγορία + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Πρόθεμα IPN της άμεσης κατηγορίας και μιας ειδικής για μέρος αύξησης + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Προθέματα IPN με ιεραρχική σειρά κατηγοριών των προθέτων γονέων + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Προθέματα IPN με ιεραρχική σειρά κατηγοριών των προθέτων γονέων και συγκεκριμένη αύξηση για το μέρος + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Δημιουργήστε πρώτα ένα εξάρτημα και αντιστοιχίστε το σε μια κατηγορία: με τις υπάρχουσες κατηγορίες και τα δικά τους προθέματα IPN, η ονομασία IPN για το εξάρτημα μπορεί να προταθεί αυτόματα + + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index be1e63488..927d661fc 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -351,6 +351,24 @@ Export all elements + + + export.readable.label + Readable Export + + + + + export.readable + CSV + + + + + export.readable_bom + PDF + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -548,6 +566,12 @@ Measurement Unit + + + part_custom_state.caption + Custom part state + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -1842,6 +1866,66 @@ Sub elements will be moved upwards. Advanced + + + part.edit.tab.advanced.ipn.commonSectionHeader + Suggestions without part increment + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Suggestions with numeric part increment + + + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Current IPN specification of the part + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Next possible IPN specification based on an identical part description + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + IPN prefix of direct category empty, specify one in category "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN prefix of direct category + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN prefix of direct category and part-specific increment + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN prefixes with hierarchical category order of parent-prefix(es) + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN prefixes with hierarchical category order of parent-prefix(es) and part-specific increment + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Please create part at first and assign it to a category: with existing categories and their own IPN prefix, the IPN for the part can be suggested automatically + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -4741,6 +4825,24 @@ 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.table.name.value.for_project + %value% (Project) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -4831,6 +4933,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Measurement Unit + + + part.table.partCustomState + Custom part state + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5695,6 +5803,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Measuring unit + + + part.edit.partCustomState + Custom part state + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -5982,6 +6096,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Measurement unit + + + part_custom_state.label + Custom part state + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6225,6 +6345,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Measurement Unit + + + tree.tools.edit.part_custom_state + Custom part state + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -6959,6 +7085,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Name filter + + + category.edit.part_ipn_prefix + Part IPN Prefix + + obsolete @@ -8498,6 +8630,12 @@ Element 1 -> Element 1.2 Measurement unit + + + perm.part_custom_states + Custom part state + + obsolete @@ -9867,6 +10005,48 @@ Element 1 -> Element 1.2 Archived + + + assembly.edit.status + Assembly status + + + + + assembly.edit.ipn + Internal Part Number (IPN) + + + + + 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 @@ -10257,12 +10437,24 @@ Element 1 -> Element 1.2 e.g "/Capacitor \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + e.g "B12A" + + category.edit.partname_regex.help A PCRE-compatible regular expression, which a part name have to match. + + + category.edit.part_ipn_prefix.help + A prefix suggested when entering the IPN of a part. + + entity.select.add_hint @@ -10809,6 +11001,12 @@ Element 1 -> Element 1.2 Measuring Unit + + + log.element_edited.changed_fields.partCustomState + Custom part state + + log.element_edited.changed_fields.expiration_date @@ -11013,6 +11211,18 @@ Element 1 -> Element 1.2 Type + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11025,6 +11235,319 @@ Element 1 -> Element 1.2 Clear existing BOM entries before importing + + + project.import_bom.template.header.json + JSON Import Template + + + + + project.import_bom.template.header.csv + CSV Import Template + + + + + project.import_bom.template.header.kicad_pcbnew + CSV Import Template (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Component name in the project + + + + + project.bom_import.template.entry.part.mpnr + Unique product number within the manufacturer + + + + + project.bom_import.template.entry.part.ipn + Unique IPN of the component + + + + + project.bom_import.template.entry.part.name + Unique name of the component + + + + + project.bom_import.template.entry.part.manufacturer.name + Unique name of the manufacturer + + + + + project.bom_import.template.json.table + + + + + Field + Condition + Data Type + Description + + + + + quantity + Required + Decimal (Float) + Must be provided and contains a decimal value (Float) greater than 0.0. + + + name + Optional + String + If present, it must be a non-empty string. The name of the entry within the bill of materials. + + + part + Optional + Object/Array + + If a component is to be assigned, it must be an object/array, and at least one of the following fields must be filled in: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the component in the Part-DB. + + + part.mpnr + Optional + String + A non-empty string if no part.id, part.ipn, or part.name is provided. + + + part.ipn + Optional + String + A non-empty string if no part.id, part.mpnr, or part.name is provided. + + + part.name + Optional + String + A non-empty string if no part.id, part.mpnr, or part.ipn is provided. + + + part.manufacturer + Optional + Object/Array + + If a component's manufacturer is to be adjusted, or the component is to be unambiguously identified based on part.mpnr, it must be an object/array, and at least one of the following fields must be filled in: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the manufacturer. + + + manufacturer.name + Optional + String + A non-empty string if no manufacturer.id is provided. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.csv.exptected_columns + Possible columns: + + + + + project.bom_import.template.csv.table + + + + + Column + Condition + Data Type + Description + + + + + quantity + Required + Floating-point number (Float) + Must be provided and contain a floating-point value (Float) greater than 0.0. + + + name + Optional + String + If available, it must be a non-empty string. The name of the entry within the bill of materials. + + + Columns starting with part_ + + If a component is to be assigned, at least one of the following columns must be provided and filled in: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the component in the Part-DB. + + + part_mpnr + Optional + String + Must be provided if the part_id, part_ipn, or part_name columns are not filled in. + + + part_ipn + Optional + String + Must be provided if the part_id, part_mpnr, or part_name columns are not filled in. + + + part_name + Optional + String + Must be provided if the part_id, part_mpnr, or part_ipn columns are not filled in. + + + Columns starting with part_manufacturer_ + + If the manufacturer of a component is to be adjusted or if the component is to be uniquely identified based on the part_mpnr, at least one of the following columns must be provided and filled in: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the manufacturer. + + + part_manufacturer_name + Optional + String + Must be provided if the part_manufacturer_id column is not filled in. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Expected columns: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Note: No assignment to specific components from the category management is performed.

    + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.table + + + + + Field + Condition + Data type + Description + + + + + Id + Optional + Integer + Free entry. A unique identification number for each component. + + + Designator + Optional + String + Free entry. A unique reference identifier of the component on the PCB, e.g., "R1" for resistor 1.
    It is adopted into the assembly name of the component entry. + + + Package + Optional + String + Free entry. The housing or package type of the component, e.g., "0805" for SMD resistors.
    It is not adopted into the component entry. + + + Quantity + Mandatory + Integer + The number of identical components required to create an instance.
    It is adopted as the quantity of the entry. + + + Designation + Mandatory + String + Description or function of the component, e.g., resistor value "10kΩ" or capacitor value "100nF".
    It is adopted into the name of the component entry. + + + Supplier and ref + Optional + String + Free entry. Can contain a distributor-specific value, for example.
    It is adopted as a note to the component entry. + + + + ]]> +
    +
    +
    project.bom_import.clear_existing_bom.help @@ -11067,6 +11590,18 @@ Element 1 -> Element 1.2 Edit Measurement Unit + + + part_custom_state.new + New custom part state + + + + + part_custom_state.edit + Edit custom part state + + user.aboutMe.label @@ -12768,6 +13303,30 @@ Please note, that you can not impersonate a disabled user. If you try you will g The columns to show by default in part tables. Order of items can be changed via drag & drop. + + + settings.behavior.table.assemblies_default_columns + Default columns for assembly tables + + + + + settings.behavior.table.assemblies_default_columns.help + The columns to show by default in assembly tables. Order of items can be changed via drag & drop. + + + + + settings.behavior.table.assemblies_bom_default_columns + Default columns for assembly BOM tables + + + + + settings.behavior.table.assemblies_bom_default_columns.help + The columns to show by default in assembly BOM tables. Order of items can be changed via drag & drop. + + settings.ips.oemsecrets @@ -12852,6 +13411,767 @@ 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.referencedAssembly.labelp + Referenced 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 part list + + + + + 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.build.form.referencedAssembly + Assembly "%name%" + + + + + 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.bom.delete.confirm + Do you really want to delete this entry? + + + + + assembly.add_parts_to_assembly + Add parts to assembly + + + + + part.info.add_part_to_assembly + Add this part to an assembly + + + + + assembly.bom.project + Project + + + + + assembly.bom.referencedAssembly + 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 part list for assembly + + + + + assembly.bom.partOrAssembly + Part or assembly + + + + + 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 an assembly + + + + + assembly.bom_import.type.csv + CSV for an assembly + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.type.kicad_schematic + KiCAD Schematic BOM (CSV file) + + + + + 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.csv + Import template CSV 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 + Mandatory + Floating point number (Float) + Must be provided and contains a floating point value (Float), which is greater than 0.0. + + + name + Optional + String + If present, it must be a non-empty string. Name of the entry within the assembly. + + + part + Optional + Object/Array + + If a part is to be associated, it must be an object/array with at least one of the following fields filled out: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Optional + Integer + Integer > 0. Corresponds to the part database internal numeric ID of the part. + + + part.mpnr + Optional + String + Non-empty string, if no part.id, part.ipn, or part.name is provided. + + + part.ipn + Optional + String + Non-empty string, if no part.id, part.mpnr, or part.name is provided. + + + part.name + Optional + String + Non-empty string, if no part.id, part.mpnr, or part.ipn is provided. + + + part.description + Optional + String or null + If provided, it must be a non-empty string or null. It will be transferred to the part, overwriting its current value. + + + part.manufacturer + Optional + Object/Array + + If the manufacturer of a part is to be adjusted or the part is to be uniquely identified using the part.mpnr, it must be an object/array with at least one of the following fields filled out: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Optional + Integer + Integer > 0. Corresponds to 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 the category of a part is to be adjusted, it must be an object/array with at least one of the following fields filled out: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the part category. + + + category.name + Optional + String + Non-empty string, if no category.id is provided. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.csv.exptected_columns + Possible columns: + + + + + assembly.bom_import.template.csv.table + + + + + Column + Condition + Data Type + Description + + + + + quantity + Mandatory + Floating point number (Float) + Must be provided and contains a floating point value (Float), which is greater than 0.0. + + + name + Optional + String + Name of the entry within the assembly. + + + Columns starting with part_ + + If a part is to be associated, one of the following columns must be provided and filled out: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Optional + Integer + Integer > 0. Corresponds to the part database internal numeric ID of the part. + + + part_mpnr + Optional + String + Must be provided if no part_id, part_ipn, or part_name column is filled out. + + + part_ipn + Optional + String + Must be provided if no part_id, part_mpnr, or part_name column is filled out. + + + part_name + Optional + String + Must be provided if no part_id, part_mpnr, or part_ipn column is filled out. + + + part_description + Optional + String + Will be transferred to the part, overwriting its current value if a non-empty string is provided. + + + Columns starting with part_manufacturer_ + + If the manufacturer of a part is to be adjusted or the part is to be uniquely identified using the part_mpnr, one of the following columns must be provided and filled out: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the manufacturer. + + + part_manufacturer_name + Optional + String + Must be provided if no part_manufacturer_id column is filled out. + + + Columns starting with part.category_ + + If the category of a part is to be adjusted, one of the following columns must be provided and filled out: +
      +
    • part_category_id
    • +
    • part_category_name
    • +
    + + + + part_category_id + Optional + Integer + Integer > 0. Corresponds to the internal numeric ID of the part category. + + + part_category_name + Optional + String + Must be provided if no part_category_id column is filled out. + + + + ]]> +
    +
    +
    + + + 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 part. + + + Designator + Optional + String + Free-form field. A unique reference designator of the part on the PCB, e.g., “R1” for resistor 1.
    Used for the placement name of the part entry in the assembly. + + + Package + Optional + String + Free-form field. The case or form factor of the part, e.g., “0805” for SMD resistors.
    Not adopted for a part entry within the assembly. + + + Quantity + Required field + Integer + The number of identical parts needed to create an instance of the assembly.
    Adopted as the quantity of the part entry within the assembly. + + + Designation + Required field + String + Description or function of the part, e.g., resistor value “10kΩ” or capacitor value “100nF.”
    Adopted into the name of the part entry within the assembly. + + + Supplier and ref + Optional + String + Free-form field. May, for example, contain distributor-specific information.
    Adopted as a note for the part entry within the assembly. + + + + ]]> +
    +
    +
    part.table.actions.error @@ -12978,6 +14298,18 @@ Please note, that you can not impersonate a disabled user. If you try you will g If you need exchange rates between non-euro currencies, you can input an API key from fixer.io here. + + + settings.misc.assembly + Assemblies + + + + + settings.misc.assembly.useIpnPlaceholderInName + Use an %%ipn%% placeholder in the name of an assembly. Placeholder is replaced with the ipn input while saving. + + settings.behavior.part_info @@ -13482,5 +14814,173 @@ Please note, that you can not impersonate a disabled user. If you try you will g Preview image min width (px) + + + assembly_list.all.title + All assemblies + + + + + assembly.edit.tab.common + General + + + + + assembly.edit.tab.advanced + Advanced options + + + + + assembly.edit.tab.attachments + Attachments + + + + + assembly.filter.dbId + Database ID + + + + + assembly.filter.ipn + Internal Part Number (IPN) + + + + + assembly.filter.name + Name + + + + + assembly.filter.description + Description + + + + + assembly.filter.comment + Comments + + + + + assembly.filter.attachments_count + Number of attachments + + + + + assembly.filter.attachmentName + Attachment name + + + + + assemblies.create.btn + Create new assembly + + + + + assembly.table.id + ID + + + + + assembly.table.name + Name + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Description + + + + + assembly.table.referencedAssemblies + Referenced assemblies + + + + + assembly.table.addedDate + Added + + + + + assembly.table.lastModified + Last modified + + + + + assembly.table.edit + Edit + + + + + assembly.table.edit.title + Edit assembly + + + + + assembly.table.invalid_regex + Invalid regular expression (regex) + + + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Name + + + + + assembly.bom.table.quantity + Quantity + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Description + + + + + datasource.synonym + %name% (Your synonym: %synonym%) + + diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index fce38e52f..da66d1141 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -351,6 +351,24 @@ Exportar todos los elementos + + + export.readable.label + Exportación legible + + + + + export.readable + CSV + + + + + export.readable_bom + PDF + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -548,6 +566,12 @@ Unidad de medida + + + part_custom_state.caption + Estado personalizado del componente + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -1842,6 +1866,66 @@ Subelementos serán desplazados hacia arriba. Avanzado + + + part.edit.tab.advanced.ipn.commonSectionHeader + Sugerencias sin incremento de parte + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Sugerencias con incrementos numéricos de partes + + + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Especificación actual de IPN de la pieza + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Siguiente especificación de IPN posible basada en una descripción idéntica de la pieza + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + El prefijo IPN de la categoría directa está vacío, especifíquelo en la categoría "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Prefijo IPN de la categoría directa + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Prefijo IPN de la categoría directa y un incremento específico de la pieza + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Prefijos IPN con orden jerárquico de categorías de prefijos principales + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Prefijos IPN con orden jerárquico de categorías de prefijos principales y un incremento específico para la parte + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Primero cree un componente y asígnele una categoría: con las categorías existentes y sus propios prefijos IPN, el identificador IPN para el componente puede ser sugerido automáticamente + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -4740,6 +4824,24 @@ Subelementos serán desplazados hacia arriba. Nombre + + + part.table.name.value.for_part + %value% (Componente) + + + + + part.table.name.value.for_assembly + %value% (Ensamblaje) + + + + + part.table.name.value.for_project + %value% (Proyecto) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -4830,6 +4932,12 @@ Subelementos serán desplazados hacia arriba. Unidad de Medida + + + part.table.partCustomState + Estado personalizado del componente + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5694,6 +5802,12 @@ Subelementos serán desplazados hacia arriba. Unidad de medida + + + part.edit.partCustomState + Estado personalizado de la pieza + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -5981,6 +6095,12 @@ Subelementos serán desplazados hacia arriba. Unidad de medida + + + part_custom_state.label + Estado personalizado de la pieza + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6224,6 +6344,12 @@ Subelementos serán desplazados hacia arriba. Unidad de medida + + + tree.tools.edit.part_custom_state + Estado personalizado del componente + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -6958,6 +7084,12 @@ Subelementos serán desplazados hacia arriba. Filtro de nombre + + + category.edit.part_ipn_prefix + Prefijo de IPN de la pieza + + obsolete @@ -8494,6 +8626,12 @@ Elemento 3 Unidad de medida + + + perm.part_custom_states + Estado personalizado del componente + + obsolete @@ -9882,6 +10020,48 @@ Elemento 3 Archivado + + + assembly.edit.status + Estado del ensamblaje + + + + + assembly.edit.ipn + Número de Componente Interno (IPN) + + + + + 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 @@ -10272,12 +10452,24 @@ Elemento 3 p.ej. "/Condensador \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + p.ej. "B12A" + + category.edit.partname_regex.help Una expresión regular compatible con PCRE, la cual debe coincidir con el nombre de un componente. + + + category.edit.part_ipn_prefix.help + Un prefijo sugerido al ingresar el IPN de una parte. + + entity.select.add_hint @@ -10824,6 +11016,12 @@ Elemento 3 Unidad de medida + + + log.element_edited.changed_fields.partCustomState + Estado personalizado del componente + + log.element_edited.changed_fields.expiration_date @@ -11028,6 +11226,18 @@ Elemento 3 Tipo + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11040,6 +11250,319 @@ Elemento 3 Eliminar entradas BOM existentes antes de importar + + + project.import_bom.template.header.json + Plantilla de importación JSON + + + + + project.import_bom.template.header.csv + Plantilla de importación CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Plantilla de importación CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Nombre del componente en el proyecto + + + + + project.bom_import.template.entry.part.mpnr + Número de producto único del fabricante + + + + + project.bom_import.template.entry.part.ipn + IPN único del componente + + + + + project.bom_import.template.entry.part.name + Nombre único del componente + + + + + project.bom_import.template.entry.part.manufacturer.name + Nombre único del fabricante + + + + + project.bom_import.template.json.table + + + + + Campo + Condición + Tipo de Datos + Descripción + + + + + quantity + Requerido + Decimal (Float) + Debe ser proporcionado y contener un valor decimal (Float) mayor que 0.0. + + + name + Opcional + Cadena (String) + Si está presente, debe ser una cadena no vacía. El nombre del elemento dentro de la lista de materiales. + + + part + Opcional + Objeto/Array + + Si se debe asignar un componente, debe ser un objeto/array, y al menos uno de los siguientes campos debe estar cumplimentado: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Opcional + Entero (Integer) + Entero (Integer) > 0. Corresponde al ID numérico interno del componente en la base de datos de componentes (Part-DB). + + + part.mpnr + Opcional + Cadena (String) + Una cadena no vacía si no se proporciona part.id, part.ipn o part.name. + + + part.ipn + Opcional + Cadena (String) + Una cadena no vacía si no se proporciona part.id, part.mpnr o part.name. + + + part.name + Opcional + Cadena (String) + Una cadena no vacía si no se proporciona part.id, part.mpnr o part.ipn. + + + part.manufacturer + Opcional + Objeto/Array + + Si se debe ajustar el fabricante de un componente, o si el componente debe identificarse de manera unívoca en base a part.mpnr, debe ser un objeto/array, y al menos uno de los siguientes campos debe estar cumplimentado: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Opcional + Entero (Integer) + Entero (Integer) > 0. Corresponde al ID numérico interno del fabricante. + + + manufacturer.name + Opcional + Cadena (String) + Una cadena no vacía si no se proporciona manufacturer.id. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.csv.exptected_columns + Columnas posibles: + + + + + project.bom_import.template.csv.table + + + + + Columna + Condición + Tipo de dato + Descripción + + + + + quantity + Obligatoria + Número decimal (Float) + Debe proporcionarse y contener un valor decimal (Float) mayor que 0.0. + + + name + Optional + String + Si está disponible, debe ser una cadena no vacía. El nombre del elemento dentro de la lista de materiales. + + + Columnas que comienzan con part_ + + Si se va a asignar un componente, al menos una de las siguientes columnas debe proporcionarse y completarse: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Opcional + Entero + Entero > 0. Corresponde al ID numérico interno del componente en la base de datos de partes (Part-DB). + + + part_mpnr + Opcional + Cadena (String) + Debe proporcionarse si las columnas part_id, part_ipn o part_name no están completas. + + + part_ipn + Opcional + Cadena (String) + Debe proporcionarse si las columnas part_id, part_mpnr o part_name no están completas. + + + part_name + Opcional + Cadena (String) + Debe proporcionarse si las columnas part_id, part_mpnr o part_ipn no están completas. + + + Columnas que comienzan con part_manufacturer_ + + Si el fabricante de un componente debe ajustarse o si el componente debe identificarse de forma única según el valor part_mpnr, al menos una de las siguientes columnas debe proporcionarse y completarse: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Opcional + Entero + Entero > 0. Corresponde al ID numérico interno del fabricante. + + + part_manufacturer_name + Opcional + Cadena (String) + Debe proporcionarse si la columna part_manufacturer_id no está completa. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Columnas esperadas: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Nota: No se realiza ninguna asignación a componentes específicos desde la gestión de categorías.

    + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.table + + + + + Campo + Condición + Tipo de dato + Descripción + + + + + Id + Opcional + Entero + Entrada libre. Un número de identificación único para cada componente. + + + Designator + Opcional + Cadena (String) + Entrada libre. Un identificador de referencia único del componente en el PCB, por ejemplo, "R1" para la resistencia 1.
    Se adopta en el nombre de ensamblaje del registro del componente. + + + Package + Opcional + Cadena (String) + Entrada libre. El encapsulado o tipo de la carcasa del componente, por ejemplo, "0805" para resistencias SMD.
    No se adopta en el registro del componente. + + + Quantity + Obligatorio + Entero + El número de componentes idénticos necesarios para crear una instancia.
    Se toma como la cantidad de la entrada del componente. + + + Designation + Obligatorio + Cadena (String) + Descripción o función del componente, por ejemplo, valor de resistencia "10kΩ" o valor de condensador "100nF".
    Se adopta en el nombre del registro del componente. + + + Supplier and ref + Opcional + Cadena (String) + Entrada libre. Puede contener, por ejemplo, un valor específico del distribuidor.
    Se adopta como una nota en el registro del componente. + + + + ]]> +
    +
    +
    project.bom_import.clear_existing_bom.help @@ -11082,6 +11605,18 @@ Elemento 3 Editar Unidad de Medida + + + part_custom_state.new + Nuevo estado personalizado del componente + + + + + part_custom_state.edit + Editar estado personalizado del componente + + user.aboutMe.label @@ -12368,5 +12903,928 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Este componente contiene más de un stock. Cambie la ubicación manualmente para seleccionar el stock deseado. + + + assembly.label + Ensamblaje + + + + + assembly.caption + Ensamblaje + + + + + perm.assemblies + Ensamblajes + + + + + assembly_bom_entry.label + Componentes + + + + + assembly.labelp + Ensamblajes + + + + + assembly.referencedAssembly.labelp + Conjuntos referenciados + + + + + 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.build.form.referencedAssembly + Ensamblaje "%name%" + + + + + 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.bom.delete.confirm + ¿Realmente desea eliminar esta entrada? + + + + + assembly.add_parts_to_assembly + Añadir piezas al ensamblaje + + + + + part.info.add_part_to_assembly + Agregar esta parte a un ensamblaje + + + + + assembly.bom.project + Proyecto + + + + + assembly.bom.referencedAssembly + 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.partOrAssembly + Parte o conjunto + + + + + 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.csv + CSV 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.csv + Plantilla de importación CSV 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 datos + Descripción + + + + + quantity + Campo obligatorio + Número de punto flotante (Float) + Debe completarse y contener un valor flotante (Float) mayor a 0.0. + + + name + Opcional + Cadena + Si se especifica, debe ser una cadena no vacía. El nombre del elemento dentro del conjunto. + + + part + Opcional + Objeto/Array + + Si se debe asignar una pieza, debe ser un objeto/array, y al menos uno de los siguientes campos debe completarse: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Opcional + Entero (Integer) + Entero > 0. Corresponde al ID interno numérico de la pieza en la base de datos. + + + part.mpnr + Opcional + Cadena + Cadena no vacía si no se rellenan los campos part.id, part.ipn o part.name. + + + part.ipn + Opcional + Cadena + Cadena no vacía si no se rellenan los campos part.id, part.mpnr o part.name. + + + part.name + Opcional + Cadena + Cadena no vacía si no se rellenan los campos part.id, part.mpnr o part.ipn. + + + part.description + Opcional + Cadena o null + Si se especifica, debe ser una cadena no vacía o null. Este valor sobrescribirá el existente en la pieza. + + + part.manufacturer + Opcional + Objeto/Array + + Si se desea cambiar el fabricante de la pieza o buscarlo de forma única utilizando el valor part.mpnr, debe ser un objeto/array y al menos uno de los siguientes campos debe completarse: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Opcional + Entero (Integer) + Entero > 0. Corresponde al ID interno numérico del fabricante. + + + manufacturer.name + Opcional + Cadena + Cadena no vacía si no se proporciona el campo manufacturer.id. + + + part.category + Opcional + Objeto/Array + + Si se desea cambiar la categoría de la pieza, debe ser un objeto/array y al menos uno de los siguientes campos debe completarse: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Opcional + Entero (Integer) + Entero > 0. Corresponde al ID interno numérico de la categoría de la pieza. + + + category.name + Opcional + Cadena + Cadena no vacía si no se proporciona el campo category.id. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.csv.exptected_columns + Columnas posibles: + + + + + assembly.bom_import.template.csv.table + + + + + Columna + Condición + Tipo de datos + Descripción + + + + + quantity + Campo obligatorio + Número de punto flotante (Float) + Debe completarse y contener un valor flotante (Float) mayor a 0.0. + + + name + Opcional + Cadena + El nombre del elemento dentro del conjunto. + + + Columnas que comienzan con part_ + + Si se debe asignar una pieza, al menos una de las siguientes columnas debe completarse: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Opcional + Entero (Integer) + Entero > 0. Corresponde al ID interno numérico de la pieza en la base de datos. + + + part_mpnr + Opcional + Cadena + Debe completarse si no se han rellenado las columnas part_id, part_ipn o part_name. + + + part_ipn + Opcional + Cadena + Debe completarse si no se han rellenado las columnas part_id, part_mpnr o part_name. + + + part_name + Opcional + Cadena + Debe completarse si no se han rellenado las columnas part_id, part_mpnr o part_ipn. + + + part_description + Opcional + Cadena + Se transferirá y reemplazará el valor de la descripción de la pieza si se proporciona una cadena no vacía. + + + Columnas que comienzan con part_manufacturer_ + + Si el fabricante de la pieza debe cambiarse o buscarse exclusivamente mediante part_mpnr, al menos una de las siguientes columnas debe completarse: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Opcional + Entero (Integer) + Entero > 0. Corresponde al ID interno numérico del fabricante. + + + part_manufacturer_name + Opcional + Cadena + Debe completarse si no se ha proporcionado el campo part_manufacturer_id. + + + Columnas que comienzan con part.category_ + + Si se necesita modificar la categoría de la pieza, al menos una de las siguientes columnas debe completarse: +
      +
    • part_category_id
    • +
    • part_category_name
    • +
    + + + + part_category_id + Opcional + Entero (Integer) + Entero > 0. Corresponde al ID interno numérico de la categoría de la pieza. + + + part_category_name + Opcional + Cadena + Debe completarse si no se ha proporcionado el campo part_category_id. + + + + ]]> +
    +
    +
    + + + 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 único de identificación para cada componente. + + + Designator + Opcional + Cadena + Campo libre. Un designador de referencia único para el componente en la PCB, por ejemplo, "R1" para la resistencia 1.
    Se utiliza como el nombre de ubicación en la entrada de componentes dentro del ensamblaje. + + + Package + Opcional + Cadena + Campo libre. El encapsulado o formato del componente, por ejemplo, "0805" para resistencias SMD.
    No se incluye en la entrada del componente dentro del ensamblaje. + + + Quantity + Requerido + Entero + El número de componentes idénticos necesarios para crear una instancia del ensamblaje.
    Se usa como la cantidad en la entrada de componentes dentro del ensamblaje. + + + Designation + Requerido + Cadena + Descripción o función del componente, por ejemplo, valor de resistencia "10kΩ" o valor de condensador "100nF".
    Se usa como el nombre de la entrada del componente dentro del ensamblaje. + + + Supplier and ref + Opcional + Cadena + Campo libre. Puede contener, por ejemplo, información específica del distribuidor.
    Se usa como una nota en la entrada del componente dentro del ensamblaje. + + + + ]]> +
    +
    +
    + + + assembly_list.all.title + Todas las ensamblajes + + + + + assembly.edit.tab.common + General + + + + + assembly.edit.tab.advanced + Opciones avanzadas + + + + + assembly.edit.tab.attachments + Archivos adjuntos + + + + + assembly.filter.dbId + ID de la base de datos + + + + + assembly.filter.ipn + Número interno de pieza (IPN) + + + + + assembly.filter.name + Nombre + + + + + assembly.filter.description + Descripción + + + + + assembly.filter.comment + Comentarios + + + + + assembly.filter.attachments_count + Cantidad de adjuntos + + + + + assembly.filter.attachmentName + Nombre del adjunto + + + + + assemblies.create.btn + Crear una nueva ensamblaje + + + + + assembly.table.id + ID + + + + + assembly.table.name + Nombre + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Descripción + + + + + assembly.table.referencedAssemblies + Ensambles referenciados + + + + + assembly.table.addedDate + Añadido + + + + + assembly.table.lastModified + Última modificación + + + + + assembly.table.edit + Editar + + + + + assembly.table.edit.title + Editar ensamblaje + + + + + assembly.table.invalid_regex + Expresión regular no válida (regex) + + + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Nombre + + + + + assembly.bom.table.quantity + Cantidad + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Descripción + + + + + datasource.synonym + %name% (Tu sinónimo: %synonym%) + + diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 292dbafaa..af0e51954 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -320,6 +320,24 @@ Exporter tous les éléments + + + export.readable.label + Export lisible + + + + + export.readable + CSV + + + + + export.readable_bom + PDF + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -517,6 +535,12 @@ Unité de mesure + + + part_custom_state.caption + État personnalisé du composant + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -1820,6 +1844,66 @@ Show/Hide sidebar Avancé + + + part.edit.tab.advanced.ipn.commonSectionHeader + Suggestions sans incrément de partie + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Propositions avec incréments numériques de parties + + + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Spécification IPN actuelle pour la pièce + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Prochaine spécification IPN possible basée sur une description identique de la pièce + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Le préfixe IPN de la catégorie directe est vide, veuillez le spécifier dans la catégorie "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Préfixe IPN de la catégorie directe + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Préfixe IPN de la catégorie directe et d'un incrément spécifique à la partie + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Préfixes IPN avec un ordre hiérarchique des catégories des préfixes parents + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Préfixes IPN avec un ordre hiérarchique des catégories des préfixes parents et un incrément spécifique à la pièce + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Créez d'abord une pièce et assignez-la à une catégorie : avec les catégories existantes et leurs propres préfixes IPN, l'identifiant IPN pour la pièce peut être proposé automatiquement + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -4703,6 +4787,24 @@ 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% (Assemblage) + + + + + part.table.name.value.for_project + %value% (Projet) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -4793,6 +4895,12 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia Unité de mesure + + + part.table.partCustomState + État personnalisé de la pièce + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5657,6 +5765,12 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia Unité de mesure + + + part.edit.partCustomState + État personnalisé de la pièce + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -5934,6 +6048,12 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia Unité de mesure + + + part_custom_state.label + État personnalisé de la pièce + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6166,6 +6286,12 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia Unité de mesure + + + tree.tools.edit.part_custom_state + État personnalisé de la pièce + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -6900,6 +7026,12 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia Filtre de nom + + + category.edit.part_ipn_prefix + Préfixe de pièce IPN + + obsolete @@ -6947,7 +7079,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 @@ -8423,6 +8555,12 @@ exemple de ville Unités de mesure + + + perm.part_custom_states + État personnalisé du composant + + obsolete @@ -9097,5 +9235,982 @@ 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> + + + assembly.edit.status + Statut de l'assemblage + + + + + assembly.status.draft + Brouillon + + + + + assembly.status.planning + En planification + + + + + assembly.status.in_production + En production + + + + + assembly.status.finished + Terminé + + + + + assembly.status.archived + Archivé + + + + + assembly.label + Assemblage + + + + + assembly.caption + Assemblage + + + + + perm.assemblies + Assemblages + + + + + assembly_bom_entry.label + Composants + + + + + assembly.labelp + Assemblages + + + + + assembly.referencedAssembly.labelp + Assemblages référencés + + + + + 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.build.form.referencedAssembly + Assemblage "%name%" + + + + + 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.bom.delete.confirm + Voulez-vous vraiment supprimer cette entrée ? + + + + + assembly.add_parts_to_assembly + Ajouter des pièces à l'assemblage + + + + + part.info.add_part_to_assembly + Ajouter cette pièce à un assemblage + + + + + assembly.bom.project + Projet + + + + + assembly.bom.referencedAssembly + 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.partOrAssembly + Pièce ou assemblage + + + + + 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.csv + CSV pour un assemblage + + + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew BOM) + + + + + assembly.bom_import.type.kicad_schematic + KiCAD Éditeur Schématique BOM (fichier CSV) + + + + + 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.csv + Modèle d'importation CSV 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 + Champ obligatoire + Nombre à virgule flottante (Float) + Doit être rempli et contenir une valeur décimale (Float) supérieure à 0.0. + + + name + Optionnel + Chaîne + Si renseigné, doit être une chaîne non vide. Le nom de l'élément dans l'assemblage. + + + part + Optionnel + Objet/Tableau + + Si une pièce doit être assignée, cela doit être un objet/tableau et au moins un des champs suivants doit être renseigné : +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Optionnel + Entier (Integer) + Nombre entier > 0. Correspond à l'ID interne numérique de la pièce dans la base de données. + + + part.mpnr + Optionnel + Chaîne + Chaîne non vide si part.id, part.ipn ou part.name ne sont pas renseignés. + + + part.ipn + Optionnel + Chaîne + Chaîne non vide si part.id, part.mpnr ou part.name ne sont pas renseignés. + + + part.name + Optionnel + Chaîne + Chaîne non vide si part.id, part.mpnr ou part.ipn ne sont pas renseignés. + + + part.description + Optionnel + Chaîne ou null + Si renseignée, doit être une chaîne non vide ou null. Ce champ remplacera la valeur existante de la pièce. + + + part.manufacturer + Optionnel + Objet/Tableau + + Si le fabricant de la pièce doit être modifié ou recherché de manière unique à l'aide de la valeur part.mpnr, cela doit être un objet/tableau et au moins un des champs suivants doit être renseigné : +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Optionnel + Entier (Integer) + Nombre entier > 0. Correspond à l'ID interne numérique du fabricant. + + + manufacturer.name + Optionnel + Chaîne + Chaîne non vide si manufacturer.id n'est pas renseigné. + + + part.category + Optionnel + Objet/Tableau + + Si la catégorie de la pièce doit être modifiée, cela doit être un objet/tableau et au moins un des champs suivants doit être renseigné : +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Optionnel + Entier (Integer) + Nombre entier > 0. Correspond à l'ID interne numérique de la catégorie de la pièce. + + + category.name + Optionnel + Chaîne + Chaîne non vide si category.id n'est pas renseigné. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.csv.exptected_columns + Colonnes possibles : + + + + + assembly.bom_import.template.csv.table + + + + + Colonne + Condition + Type de données + Description + + + + + quantity + Champ obligatoire + Nombre à virgule flottante (Float) + Doit être rempli et contenir une valeur décimale (Float) supérieure à 0.0. + + + name + Optionnel + Chaîne + Le nom de l'élément dans l'assemblage. + + + Colonnes commençant par part_ + + Si une pièce doit être assignée, au moins une des colonnes suivantes doit être renseignée : +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Optionnel + Entier (Integer) + Nombre entier > 0. Correspond à l'ID interne numérique de la pièce dans la base de données. + + + part_mpnr + Optionnel + Chaîne + Doit être renseignée si les colonnes part_id, part_ipn ou part_name ne sont pas remplies. + + + part_ipn + Optionnel + Chaîne + Doit être renseignée si les colonnes part_id, part_mpnr ou part_name ne sont pas remplies. + + + part_name + Optionnel + Chaîne + Doit être renseignée si les colonnes part_id, part_mpnr ou part_ipn ne sont pas remplies. + + + part_description + Optionnel + Chaîne + Sera transférée et remplacera la valeur existante de la description si une chaîne non vide est fournie. + + + Colonnes commençant par part_manufacturer_ + + Si le fabricant de la pièce doit être modifié ou recherché uniquement via part_mpnr, au moins une des colonnes suivantes doit être renseignée : +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Optionnel + Entier (Integer) + Nombre entier > 0. Correspond à l'ID interne numérique du fabricant. + + + part_manufacturer_name + Optionnel + Chaîne + Doit être renseignée si part_manufacturer_id n'est pas fourni. + + + Colonnes commençant par part.category_ + + Si la catégorie de la pièce doit être modifiée, au moins une des colonnes suivantes doit être renseignée : +
      +
    • part_category_id
    • +
    • part_category_name
    • +
    + + + + part_category_id + Optionnel + Entier (Integer) + Nombre entier > 0. Correspond à l'ID interne numérique de la catégorie de la pièce. + + + part_category_name + Optionnel + Chaîne + Doit être renseignée si part_category_id n'est pas fourni. + + + + ]]> +
    +
    +
    + + + 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. + + + Designator + Optionnel + Chaîne + Champ libre. Une désignation de référence unique pour le composant sur le PCB, par exemple, "R1" pour la résistance 1.
    Utilisé comme le nom de l'emplacement de l'entrée composant dans l'assemblage. + + + Package + Optionnel + Chaîne + Champ libre. Le boîtier ou le format du composant, par exemple, "0805" pour les résistances CMS.
    Non inclus dans l'entrée composant pour l'assemblage. + + + Quantity + Obligatoire + Entier + Le nombre de composants identiques nécessaires pour créer une instance de l'assemblage.
    Utilisé comme la quantité dans l'entrée composant de l'assemblage. + + + Designation + Obligatoire + Chaîne + La description ou la fonction du composant, par exemple, valeur de résistance "10kΩ" ou valeur de condensateur "100nF".
    Utilisé comme le nom dans l'entrée composant pour l'assemblage. + + + Supplier and ref + Optionnel + Chaîne + Champ libre. Peut contenir par exemple des informations spécifiques sur le fournisseur.
    Utilisé comme une note dans l'entrée composant pour l'assemblage. + + + + ]]> +
    +
    +
    + + + assembly_list.all.title + Toutes les assemblages + + + + + assembly.edit.tab.common + Général + + + + + assembly.edit.tab.advanced + Options avancées + + + + + assembly.edit.tab.attachments + Pièces jointes + + + + + assembly.filter.dbId + ID de la base de données + + + + + assembly.filter.ipn + Numéro de pièce interne (IPN) + + + + + assembly.filter.name + Nom + + + + + assembly.filter.description + Description + + + + + assembly.filter.comment + Commentaires + + + + + assembly.filter.attachments_count + Nombre de pièces jointes + + + + + assembly.filter.attachmentName + Nom de la pièce jointe + + + + + assemblies.create.btn + Créer un nouvel assemblage + + + + + assembly.table.id + ID + + + + + assembly.table.name + Nom + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Description + + + + + assembly.table.referencedAssemblies + Ensembles référencés + + + + + assembly.table.addedDate + Ajouté + + + + + assembly.table.lastModified + Dernière modification + + + + + assembly.table.edit + Modifier + + + + + assembly.table.edit.title + Modifier l'assemblage + + + + + assembly.table.invalid_regex + Expression régulière invalide (regex) + + + + + datasource.synonym + %name% (Votre synonyme : %synonym%) + + + + + category.edit.part_ipn_prefix.placeholder + par ex. "B12A" + + + + + category.edit.part_ipn_prefix.help + Un préfixe suggéré lors de la saisie de l'IPN d'une pièce. + + + + + part_custom_state.new + Nouveau statut personnalisé du composant + + + + + part_custom_state.edit + Modifier le statut personnalisé du composant + + + + + log.element_edited.changed_fields.partCustomState + État personnalisé de la pièce + + diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 828304eba..6ef5b528e 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -351,6 +351,24 @@ Esportare tutti gli elementi + + + export.readable.label + Esporta leggibile + + + + + export.readable + CSV + + + + + export.readable_bom + PDF + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -548,6 +566,12 @@ Unità di misura + + + part_custom_state.caption + Stato personalizzato del componente + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -1842,6 +1866,66 @@ I sub elementi saranno spostati verso l'alto. Avanzate + + + part.edit.tab.advanced.ipn.commonSectionHeader + Suggerimenti senza incremento di parte + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Suggerimenti con incrementi numerici delle parti + + + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Specifica IPN attuale per il pezzo + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Prossima specifica IPN possibile basata su una descrizione identica del pezzo + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Il prefisso IPN della categoria diretta è vuoto, specificarlo nella categoria "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Prefisso IPN della categoria diretta + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Prefisso IPN della categoria diretta e di un incremento specifico della parte + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Prefissi IPN con ordine gerarchico delle categorie dei prefissi padre + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Prefissi IPN con ordine gerarchico delle categorie dei prefissi padre e un incremento specifico per il pezzo + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Crea prima un componente e assegnagli una categoria: con le categorie esistenti e i loro propri prefissi IPN, l'identificativo IPN per il componente può essere suggerito automaticamente + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -4742,6 +4826,24 @@ 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.table.name.value.for_project + %value% (Progetto) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -4832,6 +4934,12 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi Unità di misura + + + part.table.partCustomState + Stato personalizzato del componente + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5696,6 +5804,12 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi Unità di misura + + + part.edit.partCustomState + Stato personalizzato della parte + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -5983,6 +6097,12 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi Unità di misura + + + part_custom_state.label + Stato personalizzato della parte + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6226,6 +6346,12 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi Unità di misura + + + tree.tools.edit.part_custom_state + Stato personalizzato del componente + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -6960,6 +7086,12 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi Filtro nome + + + category.edit.part_ipn_prefix + Prefisso parte IPN + + obsolete @@ -8496,6 +8628,12 @@ Element 3 Unità di misura + + + perm.part_custom_states + Stato personalizzato del componente + + obsolete @@ -9884,6 +10022,48 @@ Element 3 Archiviato + + + assembly.edit.status + Stato dell'assemblaggio + + + + + assembly.edit.ipn + Codice interno (IPN) + + + + + 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 @@ -10274,12 +10454,24 @@ Element 3 es. "/Condensatore \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + es. "B12A" + + category.edit.partname_regex.help Un'espressione regolare compatibile con PCRE che il nome del componente deve soddisfare. + + + category.edit.part_ipn_prefix.help + Un prefisso suggerito durante l'inserimento dell'IPN di una parte. + + entity.select.add_hint @@ -10826,6 +11018,12 @@ Element 3 Unità di misura + + + log.element_edited.changed_fields.partCustomState + Stato personalizzato della parte + + log.element_edited.changed_fields.expiration_date @@ -11030,6 +11228,18 @@ Element 3 Tipo + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11042,6 +11252,319 @@ Element 3 Cancellare le voci della BOM (lista dei materiali) esistenti prima dell'importazione + + + project.import_bom.template.header.json + Modello di importazione JSON + + + + + project.import_bom.template.header.csv + Modello di importazione CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Modello di importazione CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Nome del componente nel progetto + + + + + project.bom_import.template.entry.part.mpnr + Codice prodotto unico del produttore + + + + + project.bom_import.template.entry.part.ipn + IPN unico del componente + + + + + project.bom_import.template.entry.part.name + Nome unico del componente + + + + + project.bom_import.template.entry.part.manufacturer.name + Nome unico del produttore + + + + + project.bom_import.template.json.table + + + + + Campo + Condizione + Tipo di Dati + Descrizione + + + + + quantity + Obbligatorio + Decimale (Float) + Deve essere fornito e contenere un valore decimale (Float) maggiore di 0.0. + + + name + Opzionale + Stringa (String) + Se presente, deve essere una stringa non vuota. Il nome dell'elemento all'interno della distinta materiali. + + + part + Opzionale + Oggetto/Array + + Se un componente deve essere assegnato, deve essere un oggetto/array e almeno uno dei seguenti campi deve essere compilato: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Opzionale + Intero (Integer) + Intero (Integer) > 0. Corrisponde all'ID numerico interno del componente nel database delle parti (Part-DB). + + + part.mpnr + Opzionale + Stringa (String) + Una stringa non vuota se non sono forniti part.id, part.ipn o part.name. + + + part.ipn + Opzionale + Stringa (String) + Una stringa non vuota se non sono forniti part.id, part.mpnr o part.name. + + + part.name + Opzionale + Stringa (String) + Una stringa non vuota se non sono forniti part.id, part.mpnr o part.ipn. + + + part.manufacturer + Opzionale + Oggetto/Array + + Se il produttore di un componente deve essere modificato o se è necessario identificare univocamente il componente basandosi su part.mpnr, deve essere un oggetto/array e almeno uno dei seguenti campi deve essere compilato: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Opzionale + Intero (Integer) + Intero (Integer) > 0. Corrisponde all'ID numerico interno del produttore. + + + manufacturer.name + Opzionale + Stringa (String) + Una stringa non vuota se non è fornito manufacturer.id. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.csv.exptected_columns + Colonne possibili: + + + + + project.bom_import.template.csv.table + + + + + Colonna + Condizione + Tipo di dato + Descrizione + + + + + quantity + Obbligatoria + Numero decimale (Float) + Deve essere fornita e contenere un valore decimale (Float) maggiore di 0.0. + + + name + Optional + String + Se disponibile, deve essere una stringa non vuota. Il nome della voce all'interno della distinta base. + + + Colonne che iniziano con part_ + + Se un componente deve essere assegnato, almeno una delle seguenti colonne deve essere fornita e compilata: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Opzionale + Intero (Integer) + Intero > 0. Corrisponde all'ID numerico interno del componente nel database delle parti (Part-DB). + + + part_mpnr + Opzionale + Stringa (String) + Deve essere fornita se le colonne part_id, part_ipn o part_name non sono compilate. + + + part_ipn + Opzionale + Stringa (String) + Deve essere fornita se le colonne part_id, part_mpnr o part_name non sono compilate. + + + part_name + Opzionale + Stringa (String) + Deve essere fornita se le colonne part_id, part_mpnr o part_ipn non sono compilate. + + + Colonne che iniziano con part_manufacturer_ + + Se il produttore di un componente deve essere modificato o il componente deve essere identificato univocamente in base al valore part_mpnr, almeno una delle seguenti colonne deve essere fornita e compilata: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Opzionale + Intero (Integer) + Intero > 0. Corrisponde all'ID numerico interno del produttore. + + + part_manufacturer_name + Opzionale + Stringa (String) + Deve essere fornita se la colonna part_manufacturer_id non è compilata. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Colonne previste: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Nota: Non viene effettuata alcuna associazione con componenti specifici dalla gestione delle categorie.

    + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.table + + + + + Campo + Condizione + Tipo di dato + Descrizione + + + + + Id + Opzionale + Numero intero + Valore libero. Un numero identificativo univoco per ciascun componente. + + + Designator + Opzionale + Stringa + Valore libero. Un identificatore di riferimento univoco del componente sul PCB, ad esempio "R1" per il resistore 1.
    Viene trasferito nel nome di montaggio del record del componente. + + + Package + Opzionale + Stringa + Valore libero. L'involucro o la forma del componente, ad esempio "0805" per i resistori SMD.
    Non viene trasferito nel record del componente. + + + Quantity + Campo obbligatorio + Numero intero + Il numero dei componenti identici necessari per creare un'istanza.
    Registrato come il numero della voce del componente. + + + Designation + Campo obbligatorio + Stringa + Descrizione o funzione del componente, ad esempio valore del resistore "10kΩ" o valore del condensatore "100nF".
    Viene trasferita nel nome del record del componente. + + + Supplier and ref + Opzionale + Stringa + Valore libero. Può contenere ad esempio un valore specifico del distributore.
    Viene trasferito come nota nel record del componente. + + + + ]]> +
    +
    +
    project.bom_import.clear_existing_bom.help @@ -11084,6 +11607,18 @@ Element 3 Modificare l'unità di misura + + + part_custom_state.new + Nuovo stato personalizzato del componente + + + + + part_custom_state.edit + Modifica stato personalizzato del componente + + user.aboutMe.label @@ -12346,6 +12881,767 @@ 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.referencedAssembly.labelp + Assembly referenziati + + + + + 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.build.form.referencedAssembly + Gruppo "%name%" + + + + + 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.bom.delete.confirm + Vuoi davvero eliminare questa voce? + + + + + assembly.add_parts_to_assembly + Aggiungi componenti al gruppo + + + + + part.info.add_part_to_assembly + Aggiungi questa parte a un assemblaggio + + + + + assembly.bom.project + Progetto + + + + + assembly.bom.referencedAssembly + Assemblaggio + + + + + 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.partOrAssembly + Parte o assieme + + + + + 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.csv + CSV per un'assemblaggio + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.type.kicad_schematic + KiCAD Editor Schematico BOM (file CSV) + + + + + 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.csv + Modello di importazione CSV per un assemblaggio + + + + + 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 dati + Descrizione + + + + + quantity + Campo obbligatorio + Numero decimale (Float) + Deve essere compilato e contenere un valore decimale (Float) maggiore di 0.0. + + + name + Opzionale + Stringa + Se specificato, deve essere una stringa non vuota. Il nome del componente all'interno dell'assemblaggio. + + + part + Opzionale + Oggetto/Array + + Se è necessario assegnare una parte, deve essere un Oggetto/Array e almeno uno dei seguenti campi deve essere compilato: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Opzionale + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico del componente nel database. + + + part.mpnr + Opzionale + Stringa + Stringa non vuota se i campi part.id, part.ipn o part.name non sono compilati. + + + part.ipn + Opzionale + Stringa + Stringa non vuota se i campi part.id, part.mpnr o part.name non sono compilati. + + + part.name + Opzionale + Stringa + Stringa non vuota se i campi part.id, part.mpnr o part.ipn non sono compilati. + + + part.description + Opzionale + Stringa o null + Se specificato, deve essere una stringa non vuota o null. Questo valore sovrascriverà quello esistente nella parte. + + + part.manufacturer + Opzionale + Oggetto/Array + + Se il produttore della parte deve essere cambiato o ricercato esclusivamente utilizzando il valore part.mpnr, deve essere un Oggetto/Array e almeno uno dei seguenti campi deve essere compilato: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Opzionale + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico del produttore. + + + manufacturer.name + Opzionale + Stringa + Stringa non vuota se il campo manufacturer.id non è compilato. + + + part.category + Opzionale + Oggetto/Array + + Se è necessario modificare la categoria della parte, deve essere un Oggetto/Array e almeno uno dei seguenti campi deve essere compilato: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Opzionale + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico della categoria della parte. + + + category.name + Opzionale + Stringa + Stringa non vuota se il campo category.id non è compilato. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.csv.exptected_columns + Colonne possibili: + + + + + assembly.bom_import.template.csv.table + + + + + Colonna + Condizione + Tipo di dati + Descrizione + + + + + quantity + Campo obbligatorio + Numero decimale (Float) + Deve essere compilato e contenere un valore decimale (Float) maggiore di 0.0. + + + name + Opzionale + Stringa + Il nome dell'elemento all'interno dell'assemblaggio. + + + Colonne che iniziano con part_ + + Se è necessario assegnare una parte, almeno una delle colonne seguenti deve essere compilata: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Opzionale + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico del componente nel database. + + + part_mpnr + Opzionale + Stringa + Deve essere compilato se le colonne part_id, part_ipn o part_name non sono compilate. + + + part_ipn + Opzionale + Stringa + Deve essere compilato se le colonne part_id, part_mpnr o part_name non sono compilate. + + + part_name + Opzionale + Stringa + Deve essere compilato se le colonne part_id, part_mpnr o part_ipn non sono compilate. + + + part_description + Opzionale + Stringa + Sarà trasferita e sostituirà il valore esistente della descrizione se viene fornita una stringa non vuota. + + + Colonne che iniziano con part_manufacturer_ + + Se il produttore del componente deve essere modificato o ricercato esclusivamente tramite part_mpnr, almeno una delle seguenti colonne deve essere compilata: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Opzionale + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico del produttore. + + + part_manufacturer_name + Opzionale + Stringa + Deve essere compilata se il campo part_manufacturer_id non è fornito. + + + Colonne che iniziano con part_category_ + + Se è necessario modificare la categoria della parte, almeno una delle seguenti colonne deve essere compilata: +
      +
    • part_category_id
    • +
    • part_category_name
    • +
    + + + + part_category_id + Opzionale + Numero intero + Numero intero > 0. Corrisponde all'ID interno numerico della categoria del componente. + + + part_category_name + Opzionale + Stringa + Deve essere compilata se il campo part_category_id non è fornito. + + + + ]]> +
    +
    +
    + + + 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 univoco per ogni componente. + + + Designator + Opzionale + Stringa + Campo libero. Un riferimento univoco al componente su PCB, ad esempio "R1" per il resistore 1.
    Utilizzato come nome della posizione nella voce componenti all'interno dell'assemblaggio. + + + Package + Opzionale + Stringa + Campo libero. L'involucro o il fattore di forma del componente, ad esempio "0805" per i resistori SMD.
    Non incluso nelle informazioni del componente nell'assemblaggio. + + + Quantity + Obbligatorio + Intero + Il numero di componenti identici richiesti per creare un'istanza dell'assemblaggio.
    Utilizzato come quantità nella voce componenti dell'assemblaggio. + + + Designation + Obbligatorio + Stringa + Descrizione o funzione del componente, ad esempio valore resistore "10kΩ" o valore condensatore "100nF".
    Utilizzato come nome nella voce componenti dell'assemblaggio. + + + Supplier and ref + Opzionale + Stringa + Campo libero. Può contenere, ad esempio, informazioni specifiche del fornitore.
    Utilizzato come nota nelle informazioni del componente nell'assemblaggio. + + + + ]]> +
    +
    +
    part.table.actions.error @@ -12370,5 +13666,173 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Questo componente contiene più di uno stock. Cambia manualmente la posizione per selezionare quale stock scegliere. + + + assembly_list.all.title + Tutti gli assiemi + + + + + assembly.edit.tab.common + Generale + + + + + assembly.edit.tab.advanced + Opzioni avanzate + + + + + assembly.edit.tab.attachments + Allegati + + + + + assembly.filter.dbId + ID del database + + + + + assembly.filter.ipn + Numero interno di parte (IPN) + + + + + assembly.filter.name + Nome + + + + + assembly.filter.description + Descrizione + + + + + assembly.filter.comment + Commenti + + + + + assembly.filter.attachments_count + Numero di allegati + + + + + assembly.filter.attachmentName + Nome dell'allegato + + + + + assemblies.create.btn + Crea un nuovo assieme + + + + + assembly.table.id + ID + + + + + assembly.table.name + Nome + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Descrizione + + + + + assembly.table.referencedAssemblies + Assiemi referenziati + + + + + assembly.table.addedDate + Aggiunto + + + + + assembly.table.lastModified + Ultima modifica + + + + + assembly.table.edit + Modifica + + + + + assembly.table.edit.title + Modifica l'assieme + + + + + assembly.table.invalid_regex + Espressione regolare non valida (regex) + + + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Nome + + + + + assembly.bom.table.quantity + Quantità + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Descrizione + + + + + datasource.synonym + %name% (Il tuo sinonimo: %synonym%) + + diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 4becc319c..417ed6716 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -320,6 +320,24 @@ すべてエクスポートする + + + export.readable.label + 読みやすいエクスポート + + + + + export.readable + CSV + + + + + export.readable_bom + PDF + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -517,6 +535,12 @@ 単位 + + + part_custom_state.caption + 部品のカスタム状態 + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -1820,6 +1844,66 @@ 詳細 + + + part.edit.tab.advanced.ipn.commonSectionHeader + 部品の増加なしの提案。 + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + パーツの数値インクリメントを含む提案 + + + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + 部品の現在のIPN仕様 + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + 同じ部品説明に基づく次の可能なIPN仕様 + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + 直接カテゴリの IPN プレフィックスが空です。「%name%」カテゴリで指定してください + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + 直接カテゴリのIPNプレフィックス + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + 直接カテゴリのIPNプレフィックスと部品特有のインクリメント + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + 親プレフィックスの階層カテゴリ順のIPNプレフィックス + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + 親プレフィックスの階層カテゴリ順とパーツ固有の増分のIPNプレフィックス + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + まずはコンポーネントを作成し、それをカテゴリに割り当ててください:既存のカテゴリとそれぞれのIPNプレフィックスを基に、コンポーネントのIPNを自動的に提案できます + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -4703,6 +4787,24 @@ 名称 + + + part.table.name.value.for_part + %value%(部品) + + + + + part.table.name.value.for_assembly + %value%(アセンブリ) + + + + + part.table.name.value.for_project + %value%(プロジェクト) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -4793,6 +4895,12 @@ 単位 + + + part.table.partCustomState + 部品のカスタム状態 + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5657,6 +5765,12 @@ 単位 + + + part.edit.partCustomState + 部品のカスタム状態 + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -5934,6 +6048,12 @@ 単位 + + + part_custom_state.label + 部品のカスタム状態 + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6166,6 +6286,12 @@ 単位 + + + tree.tools.edit.part_custom_state + 部品のカスタム状態 + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -6901,6 +7027,12 @@ 名前のフィルター + + + category.edit.part_ipn_prefix + 部品 IPN 接頭辞 + + obsolete @@ -8424,6 +8556,12 @@ Exampletown 単位 + + + perm.part_custom_states + 部品のカスタム状態 + + obsolete @@ -8834,5 +8972,958 @@ Exampletown Part-DBについての質問は、<a href="%href%" class="link-external" target="_blank">GitHub</a> にスレッドがあります。 + + + assembly.edit.status + アセンブリのステータス + + + + + assembly.status.draft + 下書き + + + + + assembly.status.planning + 計画中 + + + + + assembly.status.in_production + 製作中 + + + + + assembly.status.finished + 完成 + + + + + assembly.status.archived + アーカイブ済み + + + + + assembly.label + アセンブリ + + + + + assembly.caption + アセンブリ + + + + + perm.assemblies + アセンブリ一覧 + + + + + assembly_bom_entry.label + コンポーネント + + + + + assembly.labelp + アセンブリ一覧 + + + + + assembly.referencedAssembly.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.build.form.referencedAssembly + アセンブリ「%name%」 + + + + + assembly.builds.no_stocked_builds + 在庫のビルド済みアセンブリ数 + + + + + assembly.info.bom_entries_count + 部品 + + + + + assembly.info.sub_assemblies_count + サブアセンブリ + + + + + assembly.builds.stocked + 在庫あり + + + + + assembly.builds.needed + 必要数量 + + + + + assembly.bom.delete.confirm + 本当にこのエントリを削除しますか? + + + + + assembly.add_parts_to_assembly + アセンブリに部品を追加 + + + + + part.info.add_part_to_assembly + このパーツをアセンブリに追加 + + + + + assembly.bom.project + プロジェクト + + + + + assembly.bom.referencedAssembly + アセンブリ + + + + + 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.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.csv + アセンブリ用のCSV + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.type.kicad_schematic + KiCAD 回路図エディタ BOM (CSV ファイル) + + + + + 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.csv + アセンブリ用のCSVインポートテンプレート + + + + + 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 + 任意 + 文字列 + 指定されている場合、空でない文字列でなければなりません。アセンブリ内のアイテムの名前。 + + + part + 任意 + オブジェクト/配列 + + 部品を割り当てる必要がある場合、これはオブジェクト/配列であり、次のフィールドのいずれかを少なくとも1つ入力する必要があります: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + 任意 + 整数 (Integer) + 0より大きい整数。データベース内の部品の内部数値IDに対応します。 + + + part.mpnr + 任意 + 文字列 + part.id、part.ipn、または part.name が入力されていない場合、空でない文字列。 + + + part.ipn + 任意 + 文字列 + part.id、part.mpnr、または part.name が入力されていない場合、空でない文字列。 + + + part.name + 任意 + 文字列 + part.id、part.mpnr、または part.ipn が入力されていない場合、空でない文字列。 + + + part.description + 任意 + 文字列または null + 指定されている場合、空でない文字列または null である必要があります。この値は部品の既存の値を上書きします。 + + + part.manufacturer + 任意 + オブジェクト/配列 + + 部品のメーカーを変更する場合、または part.mpnr の値を利用して一意に検索する場合、これはオブジェクト/配列であり、次のフィールドのいずれかを少なくとも1つ入力する必要があります: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + 任意 + 整数 (Integer) + 0より大きい整数。メーカーの内部数値IDに対応します。 + + + manufacturer.name + 任意 + 文字列 + manufacturer.id が提供されていない場合、空でない文字列。 + + + part.category + 任意 + オブジェクト/配列 + + 部品のカテゴリーを変更する場合、これはオブジェクト/配列であり、次のフィールドのいずれかを少なくとも1つ入力する必要があります: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + 任意 + 整数 (Integer) + 0より大きい整数。部品のカテゴリーに対応する内部数値ID。 + + + category.name + 任意 + 文字列 + category.id が提供されていない場合、空でない文字列。 + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.csv.exptected_columns + 可能なカラム: + + + + + assembly.bom_import.template.csv.table + + + + + カラム + 条件 + データ型 + 説明 + + + + + quantity + 必須項目 + 浮動小数点数 (Float) + 入力必須で、0.0よりも大きい浮動小数点数 (Float) を含む必要があります。 + + + name + 任意 + 文字列 + アセンブリ内のアイテムの名前。 + + + part_ で始まるカラム + + 部品を割り当てる必要がある場合、次のカラムのいずれかが少なくとも1つ入力されなければなりません: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + 任意 + 整数 (Integer) + 0より大きい整数。データベース内の部品の内部数値ID。 + + + part_mpnr + 任意 + 文字列 + part_id、part_ipn、または part_name が入力されていない場合に入力される必要があります。 + + + part_ipn + 任意 + 文字列 + part_id、part_mpnr、または part_name が入力されていない場合に入力される必要があります。 + + + part_name + 任意 + 文字列 + part_id、part_mpnr、または part_ipn が入力されていない場合に入力される必要があります。 + + + part_description + 任意 + 文字列 + 指定されている場合、部品の説明既存の値を上書きする非空の文字列。 + + + part_manufacturer_ で始まるカラム + + 部品の製造元を変更する場合、または part_mpnr を利用して一意に検索する場合、次のカラムのいずれかを少なくとも1つ入力する必要があります: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + 任意 + 整数 (Integer) + 0より大きい整数。製造元の内部数値ID。 + + + part_manufacturer_name + 任意 + 文字列 + part_manufacturer_id が入力されていない場合、入力される必要があります。 + + + part_category_ で始まるカラム + + 部品のカテゴリーを変更する場合、次のカラムのいずれかを少なくとも1つ入力する必要があります: +
      +
    • part_category_id
    • +
    • part_category_name
    • +
    + + + + part_category_id + 任意 + 整数 (Integer) + 0より大きい整数。部品のカテゴリーに対応する内部数値ID。 + + + part_category_name + 任意 + 文字列 + part_category_id が提供されていない場合、入力される必要があります。 + + + + ]]> +
    +
    +
    + + + 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 + 任意 + 整数 + 自由形式のフィールド。各コンポーネントのユニークな識別番号。 + + + Designator + 任意 + 文字列 + 自由形式のフィールド。PCB上のコンポーネントごとの一意のリファレンス識別子。例: 抵抗 "R1"。
    アセンブリ内の部品エントリの配置名として使用。 + + + Package + 任意 + 文字列 + 自由形式のフィールド。コンポーネントのパッケージまたはフォームファクタ。例: 表面実装抵抗器 "0805"。
    アセンブリ内の部品エントリ情報には含まれません。 + + + Quantity + 必須 + 整数 + アセンブリの一つのインスタンスを作るために必要な同一部品の数。
    アセンブリの部品情報で数量として使用。 + + + Designation + 必須 + 文字列 + コンポーネントの説明や機能。例: 抵抗の値 "10kΩ" やコンデンサの値 "100nF"。
    アセンブリの部品情報で名称として使用。 + + + Supplier and ref + 任意 + 文字列 + 自由形式フィールド。例: 供給元に関する特定の情報を含む場合がある。
    アセンブリの部品情報の注記として使用。 + + + + ]]> +
    +
    +
    + + + assembly_list.all.title + すべてのアセンブリ + + + + + assembly.edit.tab.common + 一般 + + + + + assembly.edit.tab.advanced + 詳細オプション + + + + + assembly.edit.tab.attachments + 添付ファイル + + + + + assembly.filter.dbId + データベースID + + + + + assembly.filter.ipn + 内部部品番号(IPN) + + + + + assembly.filter.name + 名前 + + + + + assembly.filter.description + 説明 + + + + + assembly.filter.comment + コメント + + + + + assembly.filter.attachments_count + 添付ファイルの数 + + + + + assembly.filter.attachmentName + 添付ファイル名 + + + + + assemblies.create.btn + 新しいアセンブリを作成 + + + + + assembly.table.id + ID + + + + + assembly.table.name + 名前 + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + 説明 + + + + + assembly.table.referencedAssemblies + 参照されているアセンブリ + + + + + assembly.table.addedDate + 追加日 + + + + + assembly.table.lastModified + 最終変更 + + + + + assembly.table.edit + 編集 + + + + + assembly.table.edit.title + アセンブリを編集 + + + + + assembly.table.invalid_regex + 無効な正規表現(regex) + + + + + datasource.synonym + %name% (あなたの同義語: %synonym%) + + + + + category.edit.part_ipn_prefix.placeholder + 例: "B12A" + + + + + category.edit.part_ipn_prefix.help + 部品のIPN入力時に提案される接頭辞。 + + + + + part_custom_state.new + 部品の新しいカスタム状態 + + + + + part_custom_state.edit + 部品のカスタム状態を編集 + + + + + log.element_edited.changed_fields.partCustomState + 部品のカスタム状態 + + diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 760533d7c..fbe239922 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -351,6 +351,24 @@ Exporteer alle elementen + + + export.readable.label + Leesbare export + + + + + export.readable + CSV + + + + + export.readable_bom + PDF + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -548,6 +566,12 @@ Meeteenheden + + + part_custom_state.caption + Aangepaste status van het onderdeel + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -724,5 +748,1120 @@ Weet u zeker dat u wilt doorgaan? + + + perm.part_custom_states + Aangepaste status van onderdeel + + + + + tree.tools.edit.part_custom_state + Aangepaste status van het onderdeel + + + + + part_custom_state.new + Nieuwe aangepaste status van het onderdeel + + + + + part_custom_state.edit + Aangepaste status van het onderdeel bewerken + + + + + part_custom_state.label + Aangepaste staat van onderdeel + + + + + log.element_edited.changed_fields.partCustomState + Aangepaste staat van het onderdeel + + + + + part.edit.partCustomState + Aangepaste staat van het onderdeel + + + + + part.table.partCustomState + Aangepaste status van onderdeel + + + + + part.table.name.value.for_part + %value% (Onderdeel) + + + + + part.table.name.value.for_assembly + %value% (Assemblage) + + + + + part.table.name.value.for_project + %value% (Project) + + + + + assembly.edit.status + Montagestatus + + + + + assembly.status.draft + Προσχέδιο + + + + + assembly.status.planning + Υπό σχεδιασμό + + + + + assembly.status.in_production + Σε παραγωγή + + + + + assembly.status.finished + Ολοκληρώθηκε + + + + + assembly.status.archived + Αρχειοθετήθηκε + + + + + assembly.label + Assemblage + + + + + assembly.caption + Assemblage + + + + + perm.assemblies + Assemblages + + + + + assembly_bom_entry.label + Componenten + + + + + assembly.labelp + Assemblages + + + + + assembly.referencedAssembly.labelp + Gerefereerde 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.build.form.referencedAssembly + Assemblage "%name%" + + + + + 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.bom.delete.confirm + Weet u zeker dat u dit item wilt verwijderen? + + + + + assembly.add_parts_to_assembly + Onderdelen toevoegen aan assemblage + + + + + part.info.add_part_to_assembly + Dit onderdeel aan een assemblage toevoegen + + + + + assembly.bom.project + Project + + + + + assembly.bom.referencedAssembly + 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.partOrAssembly + Onderdeel of samenstelling + + + + + 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.csv + CSV voor een assemblage + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.type.kicad_schematic + KiCAD Schematische editor BOM (CSV-bestand) + + + + + 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.csv + CSV-importsjabloon voor een 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 worden ingevuld en een kommagetal (Float) bevatten dat groter is dan 0,0. + + + name + Optioneel + Tekst + Wanneer ingevuld, moet het een niet-lege tekst zijn. De naam van het item binnen de assemblage. + + + part + Optioneel + Object/Array + + Als een onderdeel moet worden toegewezen, moet dit een object/array zijn en moet ten minste één van de volgende velden worden ingevuld: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Optioneel + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van het onderdeel in de database. + + + part.mpnr + Optioneel + Tekst + Niet-lege tekst, wanneer part.id, part.ipn of part.name niet zijn ingevuld. + + + part.ipn + Optioneel + Tekst + Niet-lege tekst wanneer part.id, part.mpnr of part.name niet zijn ingevuld. + + + part.name + Optioneel + Tekst + Niet-lege tekst, wanneer part.id, part.mpnr of part.ipn niet zijn ingevuld. + + + part.description + Optioneel + Tekst of null + Indien ingevuld, moet het een niet-lege tekst of null zijn. Deze waarde vervangt de bestaande waarde in het onderdeel. + + + part.manufacturer + Optioneel + Object/Array + + Als de fabrikant van het onderdeel moet worden gewijzigd of uniek moet worden opgezocht met de waarde part.mpnr, moet dit een object/array zijn en moet ten minste één van de volgende velden worden ingevuld: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Optioneel + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van de fabrikant. + + + manufacturer.name + Optioneel + Tekst + Niet-lege tekst als manufacturer.id niet is ingevuld. + + + part.category + Optioneel + Object/Array + + Als de categorie van het onderdeel moet worden gewijzigd, moet dit een object/array zijn en moet ten minste één van de volgende velden worden ingevuld: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Optioneel + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van de categorie van het onderdeel. + + + category.name + Optioneel + Tekst + Niet-lege tekst als category.id niet is ingevuld. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.csv.exptected_columns + Mogelijke kolommen: + + + + + assembly.bom_import.template.csv.table + + + + + Kolom + Voorwaarde + Gegevenstype + Beschrijving + + + + + quantity + Verplicht veld + Kommagetal (Float) + Moet worden ingevuld en moet een kommagetal (Float) bevatten dat groter is dan 0,0. + + + name + Optioneel + Tekst + De naam van het item binnen de assemblage. + + + Kolommen die beginnen met part_ + + Als een onderdeel moet worden toegewezen, moet ten minste één van de volgende kolommen worden ingevuld: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Optioneel + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van het onderdeel in de database. + + + part_mpnr + Optioneel + Tekst + Moet worden ingevuld als part_id, part_ipn of part_name niet zijn ingevuld. + + + part_ipn + Optioneel + Tekst + Moet worden ingevuld als part_id, part_mpnr of part_name niet zijn ingevuld. + + + part_name + Optioneel + Tekst + Moet worden ingevuld als part_id, part_mpnr of part_ipn niet zijn ingevuld. + + + part_description + Optioneel + Tekst + Wordt overgenomen en vervangt de bestaande waarde van de beschrijving als er een niet-lege tekst wordt opgegeven. + + + Kolommen die beginnen met part_manufacturer_ + + Als de fabrikant van een onderdeel moet worden gewijzigd of uniek moet worden gezocht via part_mpnr, moet ten minste één van de volgende kolommen worden ingevuld: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Optioneel + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van de fabrikant. + + + part_manufacturer_name + Optioneel + Tekst + Moet worden ingevuld als part_manufacturer_id niet wordt opgegeven. + + + Kolommen die beginnen met part_category_ + + Als de categorie van een onderdeel moet worden gewijzigd, moet ten minste één van de volgende kolommen worden ingevuld: +
      +
    • part_category_id
    • +
    • part_category_name
    • +
    + + + + part_category_id + Optioneel + Hele getal + Een geheel getal > 0. Komt overeen met de interne numerieke ID van de categorie van het onderdeel. + + + part_category_name + Optioneel + Tekst + Moet worden ingevuld als part_category_id niet wordt opgegeven. + + + + ]]> +
    +
    +
    + + + 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. + + + Designator + Optioneel + String + Vrij veld. Een unieke referentienaam voor het onderdeel op de PCB, bijvoorbeeld "R1" voor weerstand 1.
    Wordt gebruikt als positioneringsnaam in de onderdelenlijst van de assemblage. + + + Package + Optioneel + String + Vrij veld. De behuizing of vormfactor van het onderdeel, bijvoorbeeld "0805" voor SMD-weerstanden.
    Wordt niet opgenomen in de onderdelenlijst binnen de assemblage. + + + Quantity + Vereist + Integer + Het aantal identieke onderdelen dat nodig is om een assemblage-instantie te creëren.
    Wordt gebruikt als hoeveelheid in de onderdelenlijst binnen de assemblage. + + + Designation + Vereist + String + De beschrijving of functie van het onderdeel, zoals weerstandwaarde "10kΩ" of condensatorwaarde "100nF".
    Wordt gebruikt als de naam in de onderdelenlijst binnen de assemblage. + + + Supplier and ref + Optioneel + String + Vrij veld. Kan bijvoorbeeld specifieke informatie over leveranciers bevatten.
    Wordt gebruikt als notitie in de onderdelenlijst binnen de assemblage. + + + + ]]> +
    +
    +
    + + + assembly_list.all.title + Alle assemblages + + + + + assembly.edit.tab.common + Algemeen + + + + + assembly.edit.tab.advanced + Geavanceerde opties + + + + + assembly.edit.tab.attachments + Bijlagen + + + + + assembly.filter.dbId + Database-ID + + + + + assembly.filter.ipn + Intern partnummer (IPN) + + + + + assembly.filter.name + Naam + + + + + assembly.filter.description + Beschrijving + + + + + assembly.filter.comment + Opmerkingen + + + + + assembly.filter.attachments_count + Aantal bijlagen + + + + + assembly.filter.attachmentName + Naam van de bijlage + + + + + assemblies.create.btn + Nieuwe assemblage aanmaken + + + + + assembly.table.id + ID + + + + + assembly.table.name + Naam + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Beschrijving + + + + + assembly.table.referencedAssemblies + Gerefereerde assemblages + + + + + assembly.table.addedDate + Toegevoegd + + + + + assembly.table.lastModified + Laatst gewijzigd + + + + + assembly.table.edit + Bewerken + + + + + assembly.table.edit.title + Assemblage bewerken + + + + + assembly.table.invalid_regex + Ongeldige reguliere expressie (regex) + + + + + datasource.synonym + %name% (Uw synoniem: %synonym%) + + + + + category.edit.part_ipn_prefix + IPN-voorvoegsel van onderdeel + + + + + category.edit.part_ipn_prefix.placeholder + bijv. "B12A" + + + + + category.edit.part_ipn_prefix.help + Een voorgesteld voorvoegsel bij het invoeren van de IPN van een onderdeel. + + + + + part.edit.tab.advanced.ipn.commonSectionHeader + Suggesties zonder toename van onderdelen + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Suggesties met numerieke verhogingen van onderdelen + + + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Huidige IPN-specificatie voor het onderdeel + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Volgende mogelijke IPN-specificatie op basis van een identieke onderdeelbeschrijving + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Het IPN-prefix van de directe categorie is leeg, geef het op in de categorie "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN-prefix van de directe categorie + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN-voorvoegsel van de directe categorie en een onderdeel specifiek increment + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN-prefixen met een hiërarchische volgorde van hoofdcategorieën + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN-prefixen met een hiërarchische volgorde van hoofdcategorieën en een specifieke toename voor het onderdeel + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Maak eerst een component en wijs het toe aan een categorie: met de bestaande categorieën en hun eigen IPN-prefixen kan de IPN voor het component automatisch worden voorgesteld + + diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index b769e2737..594324fc1 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -351,6 +351,24 @@ Eksportuj wszystkie elementy + + + export.readable.label + Eksport czytelny + + + + + export.readable + CSV + + + + + export.readable_bom + PDF + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -548,6 +566,12 @@ Jednostka miary + + + part_custom_state.caption + Stan niestandardowy komponentu + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -1847,6 +1871,66 @@ Po usunięciu pod elementy zostaną przeniesione na górę. Zaawansowane + + + part.edit.tab.advanced.ipn.commonSectionHeader + Sugestie bez zwiększenia części + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Propozycje z numerycznymi przyrostami części + + + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktualna specyfikacja IPN dla części + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Następna możliwa specyfikacja IPN na podstawie identycznego opisu części + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Prefiks IPN kategorii bezpośredniej jest pusty, podaj go w kategorii "%name%". + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Prefiks IPN kategorii bezpośredniej + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Prefiks IPN bezpośredniej kategorii i specyficzny dla części przyrost + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Prefiksy IPN z hierarchiczną kolejnością kategorii prefiksów nadrzędnych + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Prefiksy IPN z hierarchiczną kolejnością kategorii prefiksów nadrzędnych i specyficznym przyrostem dla części + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Najpierw utwórz komponent i przypisz go do kategorii: dzięki istniejącym kategoriom i ich własnym prefiksom IPN identyfikator IPN dla komponentu może być proponowany automatycznie + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -4745,6 +4829,24 @@ 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% (Złożenie) + + + + + part.table.name.value.for_project + %value% (Projekt) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -4835,6 +4937,12 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo Jednostka pomiarowa + + + part.table.partCustomState + Niestandardowy stan części + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5699,6 +5807,12 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo Jednostka pomiarowa + + + part.edit.partCustomState + Własny stan części + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -5986,6 +6100,12 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo Jednostka pomiarowa + + + part_custom_state.label + Własny stan części + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6229,6 +6349,12 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo Jednostka miary + + + tree.tools.edit.part_custom_state + Niestandardowy stan części + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -6963,6 +7089,12 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo Filtr nazwy + + + category.edit.part_ipn_prefix + Prefiks IPN części + + obsolete @@ -8499,6 +8631,12 @@ Element 3 Jednostka miary + + + perm.part_custom_states + Stan niestandardowy komponentu + + obsolete @@ -9887,6 +10025,48 @@ Element 3 Zarchiwizowany + + + assembly.edit.status + Status montażu + + + + + assembly.edit.ipn + Internal Part Number (IPN) + + + + + 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 @@ -10277,12 +10457,24 @@ Element 3 np. "/Kondensator \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + np. "B12A" + + category.edit.partname_regex.help Wyrażenie regularne zgodne z PCRE, do którego musi pasować nazwa komponentu. + + + category.edit.part_ipn_prefix.help + Een voorgesteld voorvoegsel bij het invoeren van de IPN van een onderdeel. + + entity.select.add_hint @@ -10829,6 +11021,12 @@ Element 3 Jednostka pomiarowa + + + log.element_edited.changed_fields.partCustomState + Niestandardowy stan części + + log.element_edited.changed_fields.expiration_date @@ -11033,6 +11231,18 @@ Element 3 Typ + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11045,6 +11255,319 @@ Element 3 Wyczyść istniejące wpisy BOM przed importem + + + project.import_bom.template.header.json + Szablon importu JSON + + + + + project.import_bom.template.header.csv + Szablon importu CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Szablon importu CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Nazwa komponentu w projekcie + + + + + project.bom_import.template.entry.part.mpnr + Unikalny numer produktu producenta + + + + + project.bom_import.template.entry.part.ipn + Unikalny IPN komponentu + + + + + project.bom_import.template.entry.part.name + Unikalna nazwa komponentu + + + + + project.bom_import.template.entry.part.manufacturer.name + Unikalna nazwa producenta + + + + + project.bom_import.template.json.table + + + + + Pole + Warunek + Typ Danych + Opis + + + + + quantity + Wymagane + Dziesiętny (Float) + Musi być podane i zawierać wartość dziesiętną (Float) większą niż 0.0. + + + name + Opcjonalne + Ciąg (String) + Jeśli jest obecny, musi być niepustym ciągiem znaków. Nazwa elementu w wykazie materiałów. + + + part + Opcjonalne + Obiekt/Tablica + + Jeśli komponent musi być przypisany, musi być obiektem/tablą i co najmniej jedno z następujących pól musi zostać wypełnione: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Opcjonalne + Całkowity (Integer) + Całkowity (Integer) > 0. Odpowiada wewnętrznemu numerycznemu identyfikatorowi komponentu w bazie danych części (Part-DB). + + + part.mpnr + Opcjonalne + Ciag (String) + Niepusty ciąg, jeśli part.id, part.ipn ani part.name nie zostały podane. + + + part.ipn + Opcjonalne + Ciag (String) + Niepusty ciąg, jeśli part.id, part.mpnr ani part.name nie zostały podane. + + + part.name + Opcjonalne + Ciag (String) + Niepusty ciąg, jeśli part.id, part.mpnr ani part.ipn nie zostały podane. + + + part.manufacturer + Opcjonalne + Obiekt/Tablica + + Jeśli producent komponentu musi zostać dostosowany lub komponent musi zostać jednoznacznie zidentyfikowany na podstawie part.mpnr, musi być obiektem/tablą, a co najmniej jedno z następujących pól musi zostać wypełnione: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Opcjonalne + Całkowity (Integer) + Całkowity (Integer) > 0. Odpowiada wewnętrznemu numerycznemu identyfikatorowi producenta. + + + manufacturer.name + Opcjonalne + Ciag (String) + Niepusty ciąg, jeśli manufacturer.id nie został podany. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.csv.exptected_columns + Możliwe kolumny: + + + + + project.bom_import.template.csv.table + + + + + Kolumna + Warunek + Typ danych + Opis + + + + + quantity + Wymagana + Liczba zmiennoprzecinkowa (Float) + Liczba identycznych komponentów potrzebnych do utworzenia instancji.
    Traktowane jako liczba wpisów komponentu. + + + name + Optional + String + Jeśli dostępny, musi być niepustym ciągiem znaków. Nazwa elementu w wykazie materiałów. + + + Kolumny zaczynające się od part_ + + Jeśli ma zostać przypisany komponent, co najmniej jedna z poniższych kolumn musi zostać podana i uzupełniona: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Opcjonalna + Liczba całkowita (Integer) + Liczba całkowita > 0. Odpowiada wewnętrznemu ID numerycznemu komponentu w Part-DB. + + + part_mpnr + Opcjonalna + Cišg znaków (String) + Musi być podana, jeśli kolumny part_id, part_ipn ani part_name nie są podane. + + + part_ipn + Opcjonalna + Cišg znaków (String) + Musi być podana, jeśli kolumny part_id, part_mpnr ani part_name nie są podane. + + + part_name + Opcjonalna + Cišg znaków (String) + Musi być podana, jeśli kolumny part_id, part_mpnr ani part_ipn nie są podane. + + + Kolumny zaczynające się od part_manufacturer_ + + Jeśli producent komponentu ma zostać zmieniony lub komponent ma zostać jednoznacznie zidentyfikowany na podstawie wartości part_mpnr, co najmniej jedna z poniższych kolumn musi zostać podana i uzupełniona: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Opcjonalna + Liczba całkowita (Integer) + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID producenta. + + + part_manufacturer_name + Opcjonalna + Cišg znaków (String) + Musi być podana, jeśli kolumna part_manufacturer_id nie jest uzupełniona. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Oczekiwane kolumny: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Uwaga: Nie następuje przypisanie do konkretnych komponentów z zarządzania kategoriami.

    + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.table + + + + + Pole + Warunek + Typ danych + Opis + + + + + Id + Opcjonalne + Liczba całkowita (Integer) + Dowolna wartość. Unikalny numer identyfikacyjny dla każdego komponentu. + + + Designator + Opcjonalne + String + Dowolna wartość. Unikalny identyfikator referencyjny komponentu na płytce PCB, np. „R1” dla rezystora 1.
    Zostaje przeniesiony do nazwy montażowej wpisu komponentu. + + + Package + Opcjonalne + String + Dowolna wartość. Obudowa lub typ komponentu, np. „0805” dla rezystorów SMD.
    Nie zostaje przeniesiony do wpisu komponentu. + + + Quantity + Pole obowiązkowe + Liczba całkowita (Integer) + Liczba identycznych komponentów potrzebnych do stworzenia instancji zestawu.
    Zostaje przeniesiona jako ilość wpisu komponentu. + + + Designation + Pole obowiązkowe + String + Opis lub funkcja komponentu, np. wartość rezystora „10kΩ” lub wartość kondensatora „100nF”.
    Zostaje przeniesiony do nazwy wpisu komponentu. + + + Supplier and ref + Opcjonalne + String + Dowolna wartość. Może zawierać np. wartość specyficzną dla dystrybutora.
    Zostaje przeniesiona jako notatka do wpisu komponentu. + + + + ]]> +
    +
    +
    project.bom_import.clear_existing_bom.help @@ -11087,6 +11610,18 @@ Element 3 Edytuj jednostkę miary + + + part_custom_state.new + Nowy niestandardowy stan części + + + + + part_custom_state.edit + Edytuj niestandardowy stan części + + user.aboutMe.label @@ -12223,5 +12758,934 @@ 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.referencedAssembly.labelp + Odwołane zestawy + + + + + 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.build.form.referencedAssembly + Zespół "%name%" + + + + + 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.bom.delete.confirm + Czy na pewno chcesz usunąć ten element? + + + + + assembly.add_parts_to_assembly + Dodaj części do zespołu + + + + + part.info.add_part_to_assembly + Dodaj tę część do zespołu + + + + + assembly.bom.project + Projekt + + + + + assembly.bom.referencedAssembly + Złożenie + + + + + 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.partOrAssembly + Część lub zespół + + + + + 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.csv + CSV dla zestawienia + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.type.kicad_schematic + KiCAD Schematyczny edytor BOM (plik CSV) + + + + + 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.csv + Szablon importu CSV 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 + Pole obowiązkowe + Liczba zmiennoprzecinkowa (Float) + Musi być wypełnione i zawierać liczbę zmiennoprzecinkową (Float) większą niż 0,0. + + + name + Opcjonalne + Tekst + Jeśli określono, musi to być niepusty tekst. Nazwa elementu w ramach montażu. + + + part + Opcjonalne + Obiekt/Tablica + + Jeśli konieczne jest przypisanie części, musi to być Obiekt/Tablica, a przynajmniej jedno z poniższych pól powinno być wypełnione: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Opcjonalne + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID części w bazie danych. + + + part.mpnr + Opcjonalne + Tekst + Niepusty tekst, jeśli pola part.id, part.ipn lub part.name nie są wypełnione. + + + part.ipn + Opcjonalne + Tekst + Niepusty tekst, jeśli pola part.id, part.mpnr lub part.name nie są wypełnione. + + + part.name + Opcjonalne + Tekst + Niepusty tekst, jeśli pola part.id, part.mpnr lub part.ipn nie są wypełnione. + + + part.description + Opcjonalne + Tekst lub null + Jeśli określono, musi to być niepusty tekst lub null. Ta wartość nadpisze istniejącą wartość w części. + + + part.manufacturer + Opcjonalne + Obiekt/Tablica + + Jeśli producent części ma zostać zmieniony lub wyszukany unikalnie za pomocą wartości part.mpnr, musi to być Obiekt/Tablica i przynajmniej jedno z poniższych pól powinno być wypełnione: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Opcjonalne + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID producenta. + + + manufacturer.name + Opcjonalne + Tekst + Niepusty tekst, jeśli pole manufacturer.id nie jest wypełnione. + + + part.category + Opcjonalne + Obiekt/Tablica + + Jeśli konieczna jest zmiana kategorii części, musi to być Obiekt/Tablica i przynajmniej jedno z poniższych pól powinno być wypełnione: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Opcjonalne + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID kategorii części. + + + category.name + Opcjonalne + Tekst + Niepusty tekst, jeśli pole category.id nie jest wypełnione. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.csv.exptected_columns + Możliwe kolumny: + + + + + assembly.bom_import.template.csv.table + + + + + Kolumna + Warunek + Typ danych + Opis + + + + + quantity + Pole obowiązkowe + Liczba zmiennoprzecinkowa (Float) + Musi być wypełnione i zawierać liczbę zmiennoprzecinkową (Float) większą niż 0,0. + + + name + Opcjonalne + Tekst + Nazwa elementu w ramach montażu. + + + Kolumny zaczynające się od part_ + + Jeśli konieczne jest przypisanie części, przynajmniej jedna z poniższych kolumn powinna być wypełniona: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Opcjonalne + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID części w bazie danych. + + + part_mpnr + Opcjonalne + Tekst + Musi być wypełnione, gdy kolumny part_id, part_ipn lub part_name nie są wypełnione. + + + part_ipn + Opcjonalne + Tekst + Musi być wypełnione, gdy kolumny part_id, part_mpnr lub part_name nie są wypełnione. + + + part_name + Opcjonalne + Tekst + Musi być wypełnione, gdy kolumny part_id, part_mpnr lub part_ipn nie są wypełnione. + + + part_description + Opcjonalne + Tekst + Zostanie przeniesione i zastąpi istniejącą wartość opisu, jeśli określono niepusty tekst. + + + Kolumny zaczynające się od part_manufacturer_ + + Jeśli producent części musi zostać zmieniony lub wyszukany unikalnie za pomocą part_mpnr, przynajmniej jedna z poniższych kolumn powinna być wypełniona: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Opcjonalne + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID producenta. + + + part_manufacturer_name + Opcjonalne + Tekst + Musi być wypełnione, jeśli part_manufacturer_id nie jest określony. + + + Kolumny zaczynające się od part_category_ + + Jeśli konieczna jest zmiana kategorii części, przynajmniej jedna z poniższych kolumn powinna być wypełniona: +
      +
    • part_category_id
    • +
    • part_category_name
    • +
    + + + + part_category_id + Opcjonalne + Liczba całkowita + Liczba całkowita > 0. Odpowiada wewnętrznemu numerycznemu ID kategorii części. + + + part_category_name + Opcjonalne + Tekst + Musi być wypełnione, jeśli part_category_id nie jest określone. + + + + ]]> +
    +
    +
    + + + 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. + + + Designator + Opcjonalne + Tekst + Pole dowolne. Jednoznaczny znacznik referencyjny komponentu na PCB, np. "R1" dla rezystora 1.
    Używane jako nazwa pozycji w pozycji komponentu w montażu. + + + Package + Opcjonalne + Tekst + Pole dowolne. Obudowa lub forma komponentu, np. "0805" dla rezystorów SMD.
    Niewykorzystywane w pozycji komponentu w montażu. + + + Quantity + Wymagane + Liczba całkowita + Liczba identycznych komponentów potrzebna do utworzenia jednej instancji montażu.
    Używane jako ilość w pozycji komponentu w montażu. + + + Designation + Wymagane + Tekst + Opis lub funkcja komponentu, np. wartość rezystora "10kΩ" lub wartość kondensatora "100nF".
    Używane jako nazwa w pozycji komponentu w montażu. + + + Supplier and ref + Opcjonalne + Tekst + Pole dowolne. Może zawierać np. specyficzne informacje o dostawcy.
    Używane jako notatka w pozycji komponentu w montażu. + + + + ]]> +
    +
    +
    + + + assembly_list.all.title + Wszystkie zespoły + + + + + assembly.edit.tab.common + Ogólne + + + + + assembly.edit.tab.advanced + Zaawansowane + + + + + assembly.edit.tab.attachments + Załączniki + + + + + assembly.filter.dbId + ID bazy danych + + + + + assembly.filter.ipn + Wewnętrzny numer części (IPN) + + + + + assembly.filter.name + Nazwa + + + + + assembly.filter.description + Opis + + + + + assembly.filter.comment + Komentarze + + + + + assembly.filter.attachments_count + Liczba załączników + + + + + assembly.filter.attachmentName + Nazwa załącznika + + + + + assemblies.create.btn + Utwórz nowy zespół + + + + + assembly.table.id + ID + + + + + assembly.table.name + Nazwa + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Opis + + + + + assembly.table.referencedAssemblies + Zestawy referencyjne + + + + + assembly.table.addedDate + Dodano + + + + + assembly.table.lastModified + Ostatnia modyfikacja + + + + + assembly.table.edit + Edytuj + + + + + assembly.table.edit.title + Edytuj zespół + + + + + assembly.table.invalid_regex + Nieprawidłowe wyrażenie regularne (regex) + + + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Nazwa + + + + + assembly.bom.table.quantity + Ilość + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Opis + + + + + datasource.synonym + %name% (Twój synonim: %synonym%) + + diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 62570acb0..402b1651a 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -351,6 +351,24 @@ Экспортировать всё + + + export.readable.label + Читаемый экспорт + + + + + export.readable + CSV + + + + + export.readable_bom + PDF + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -548,6 +566,12 @@ Единица измерения + + + part_custom_state.caption + Пользовательское состояние компонента + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -731,7 +755,7 @@ user.edit.tfa.disable_tfa_message - Это выключит <b>все активные двухфакторной способы аутентификации пользователя</b>и удалит <b>резервные коды</b>! + Это выключит <b>все активные двухфакторной способы аутентификации пользователя</b>и удалит <b>резервные коды</b>! <br> Пользователь должен будет снова настроить все методы двухфакторной аутентификации и распечатать новые резервные коды! <br><br> <b>Делайте это только в том случае, если вы абсолютно уверены в личности пользователя (обращающегося за помощью), в противном случае учетная запись может быть взломана злоумышленником!</b> @@ -1850,6 +1874,66 @@ Расширенные + + + part.edit.tab.advanced.ipn.commonSectionHeader + Предложения без увеличения частей. + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Предложения с числовыми приращениями частей + + + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Текущая спецификация IPN для детали + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Следующая возможная спецификация IPN на основе идентичного описания детали + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Префикс IPN для прямой категории пуст, укажите его в категории «%name%». + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Префикс IPN для прямой категории + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Префикс IPN прямой категории и специфическое для части приращение + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN-префиксы с иерархическим порядком категорий родительских префиксов + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN-префиксы с иерархическим порядком категорий родительских префиксов и специфическим увеличением для компонента + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Сначала создайте компонент и назначьте ему категорию: на основе существующих категорий и их собственных IPN-префиксов идентификатор IPN для компонента может быть предложен автоматически + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -3740,7 +3824,7 @@ tfa_backup.reset_codes.confirm_message - Это удалит все предыдущие коды и создаст набор новых. Это не может быть отменено. + Это удалит все предыдущие коды и создаст набор новых. Это не может быть отменено. Не забудьте распечатать новы кода и хранить их в безопасном месте! @@ -4751,6 +4835,24 @@ Имя + + + part.table.name.value.for_part + %value% (Часть) + + + + + part.table.name.value.for_assembly + %value% (Сборка) + + + + + part.table.name.value.for_project + %value% (Проект) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -4841,6 +4943,12 @@ Единица измерения + + + part.table.partCustomState + Пользовательское состояние детали + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5705,6 +5813,12 @@ Единица измерения + + + part.edit.partCustomState + Пользовательское состояние части + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -5992,6 +6106,12 @@ Единица измерения + + + part_custom_state.label + Пользовательское состояние детали + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6235,6 +6355,12 @@ Единица измерения + + + tree.tools.edit.part_custom_state + Пользовательское состояние компонента + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -6970,6 +7096,12 @@ Фильтр по имени + + + category.edit.part_ipn_prefix + Префикс IPN детали + + obsolete @@ -8503,6 +8635,12 @@ Единица измерения + + + perm.part_custom_states + Пользовательское состояние компонента + + obsolete @@ -9891,6 +10029,48 @@ Архивный + + + assembly.edit.status + Статус сборки + + + + + assembly.edit.ipn + Внутренний номер компонента (IPN) + + + + + 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 @@ -10281,12 +10461,24 @@ e.g "/Конденсатор \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + e.g "B12A" + + category.edit.partname_regex.help PCRE-совместимое регулярное выражение которому должно соответствовать имя компонента. + + + category.edit.part_ipn_prefix.help + Предлагаемый префикс при вводе IPN детали. + + entity.select.add_hint @@ -10833,6 +11025,12 @@ Единица измерения + + + log.element_edited.changed_fields.partCustomState + Пользовательское состояние детали + + log.element_edited.changed_fields.expiration_date @@ -11037,6 +11235,18 @@ Тип + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11049,6 +11259,319 @@ Удалить существующие записи BOM перед импортом. + + + project.import_bom.template.header.json + Шаблон импорта JSON + + + + + project.import_bom.template.header.csv + Шаблон импорта CSV + + + + + project.import_bom.template.header.kicad_pcbnew + Шаблон импорта CSV (KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + Название компонента в проекте + + + + + project.bom_import.template.entry.part.mpnr + Уникальный номер продукта производителя + + + + + project.bom_import.template.entry.part.ipn + Уникальный IPN компонента + + + + + project.bom_import.template.entry.part.name + Уникальное название компонента + + + + + project.bom_import.template.entry.part.manufacturer.name + Уникальное название производителя + + + + + project.bom_import.template.json.table + + + + + Поле + Условие + Тип данных + Описание + + + + + quantity + Обязательно + Дробное число (Float) + Должно быть указано и содержать дробное значение (Float), большее 0.0. + + + name + Опционально + Строка (String) + Если присутствует, должно быть непустой строкой. Название элемента в спецификации материалов. + + + part + Опционально + Объект/Массив + + Если необходимо назначить компонент, он должен быть объектом/массивом, и должно быть заполнено хотя бы одно из следующих полей: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Опционально + Целое число (Integer) + Целое число (Integer) > 0. Соответствует внутреннему числовому идентификатору компонента в базе данных компонентов (Part-DB). + + + part.mpnr + Опционально + Строка (String) + Непустая строка, если не указаны part.id, part.ipn или part.name. + + + part.ipn + Опционально + Строка (String) + Непустая строка, если не указаны part.id, part.mpnr или part.name. + + + part.name + Опционально + Строка (String) + Непустая строка, если не указаны part.id, part.mpnr или part.ipn. + + + part.manufacturer + Опционально + Объект/Массив + + Если необходимо указать производителя компонента или однозначно идентифицировать компонент на основе part.mpnr, он должен быть объектом/массивом, и хотя бы одно из следующих полей должно быть заполнено: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Опционально + Целое число (Integer) + Целое число (Integer) > 0. Соответствует внутреннему числовому идентификатору производителя. + + + manufacturer.name + Опционально + Строка (String) + Непустая строка, если manufacturer.id не указан. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.csv.exptected_columns + Возможные колонки: + + + + + project.bom_import.template.csv.table + + + + + Колонка + Условие + Тип данных + Описание + + + + + quantity + Обязательная + Число с плавающей запятой (Float) + Количество идентичных компонентов, необходимых для создания экземпляра.
    Считается количеством записей компонента. + + + name + Optional + String + Если доступно, должна быть непустая строка. Название элемента в спецификации материалов. + + + Колонки, начинающиеся с part_ + + Если нужно назначить компонент, должна быть указана и заполнена по крайней мере одна из следующих колонок: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Необязательная + Целое число (Integer) + Целое > 0. Соответствует внутреннему числовому ID компонента в базе данных компонентов (Part-DB). + + + part_mpnr + Необязательная + Строка (String) + Должна быть указана, если колонки part_id, part_ipn или part_name не заполнены. + + + part_ipn + Необязательная + Строка (String) + Должна быть указана, если колонки part_id, part_mpnr или part_name не заполнены. + + + part_name + Необязательная + Строка (String) + Должна быть указана, если колонки part_id, part_mpnr или part_ipn не заполнены. + + + Колонки, начинающиеся с part_manufacturer_ + + Если требуется указать производителя компонента или уникально идентифицировать компонент на основе значения part_mpnr, должна быть указана и заполнена по крайней мере одна из следующих колонок: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Необязательная + Целое число (Integer) + Целое > 0. Соответствует внутреннему числовому ID производителя. + + + part_manufacturer_name + Необязательная + Строка (String) + Должна быть указана, если колонка part_manufacturer_id не заполнена. + + + + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.exptected_columns + Ожидаемые столбцы: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + Примечание: Не выполняется привязка к конкретным компонентам из управления категориями.

    + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.table + + + + + Поле + Условие + Тип данных + Описание + + + + + Id + Необязательно + Целое число (Integer) + Свободный ввод. Уникальный идентификационный номер для каждого компонента. + + + Designator + Необязательно + Строка (String) + Свободный ввод. Уникальный идентификатор компонента на печатной плате, например, «R1» для резистора 1.
    Добавляется в название сборочного узла записи компонента. + + + Package + Необязательно + Строка (String) + Свободный ввод. Корпус или тип компонента, например, «0805» для SMD резисторов.
    Не добавляется в запись компонента. + + + Quantity + Обязательно + Целое число (Integer) + Число идентичных компонентов, необходимых для создания экземпляра сборки.
    Добавляется как количество записи компонента. + + + Designation + Обязательно + Строка (String) + Описание или функция компонента, например, значение резистора «10kΩ» или значение конденсатора «100nF».
    Добавляется в название записи компонента. + + + Supplier and ref + Необязательно + Строка (String) + Свободный ввод. Может содержать дистрибьюторское значение, например.
    Добавляется как примечание к записи компонента. + + + + ]]> +
    +
    +
    project.bom_import.clear_existing_bom.help @@ -11091,6 +11614,18 @@ Изменить единицу измерения + + + part_custom_state.new + Новое пользовательское состояние компонента + + + + + part_custom_state.edit + Редактировать пользовательское состояние компонента + + user.aboutMe.label @@ -12323,5 +12858,934 @@ Профиль сохранен! + + + assembly.label + Сборка + + + + + assembly.caption + Сборка + + + + + perm.assemblies + Сборки + + + + + assembly_bom_entry.label + Компоненты + + + + + assembly.labelp + Сборки + + + + + assembly.referencedAssembly.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.build.form.referencedAssembly + Сборка "%name%" + + + + + assembly.builds.no_stocked_builds + Собранные экземпляры на складе + + + + + assembly.info.bom_entries_count + Детали + + + + + assembly.info.sub_assemblies_count + Подсборки + + + + + assembly.builds.stocked + На складе + + + + + assembly.builds.needed + Необходимо + + + + + assembly.bom.delete.confirm + Вы действительно хотите удалить этот элемент? + + + + + assembly.add_parts_to_assembly + Добавить детали в сборку + + + + + part.info.add_part_to_assembly + Добавить эту часть в сборку + + + + + assembly.bom.project + Проект + + + + + assembly.bom.referencedAssembly + Сборка + + + + + 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.partOrAssembly + Часть или сборка + + + + + 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.csv + CSV для сборки + + + + + assembly.bom_import.type.kicad_pcbnew + CSV (KiCAD Pcbnew) + + + + + assembly.bom_import.type.kicad_schematic + KiCAD Схематический редактор BOM (CSV файл) + + + + + 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.csv + Шаблон импорта CSV для сборки + + + + + 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 + Необязательное + Строка + Если указано, должно быть непустой строкой. Имя элемента внутри сборки. + + + part + Необязательное + Объект/Массив + + Если необходимо назначить деталь, это должен быть объект/массив, и должно быть заполнено хотя бы одно из следующих полей: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому идентификатору детали в базе данных. + + + part.mpnr + Необязательное + Строка + Непустая строка, если part.id, part.ipn или part.name не указаны. + + + part.ipn + Необязательное + Строка + Непустая строка, если part.id, part.mpnr или part.name не указаны. + + + part.name + Необязательное + Строка + Непустая строка, если part.id, part.mpnr или part.ipn не указаны. + + + part.description + Необязательное + Строка или null + Если указано, должно быть непустой строкой или null. Это значение перезаписывает существующее значение в детали. + + + part.manufacturer + Необязательное + Объект/Массив + + Если необходимо изменить производителя детали или уникально найти по значению part.mpnr, это должен быть объект/массив, и должно быть заполнено хотя бы одно из следующих полей: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому идентификатору производителя. + + + manufacturer.name + Необязательное + Строка + Непустая строка, если manufacturer.id не указано. + + + part.category + Необязательное + Объект/Массив + + Если необходимо изменить категорию детали, это должен быть объект/массив, и должно быть заполнено хотя бы одно из следующих полей: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому идентификатору категории детали. + + + category.name + Необязательное + Строка + Непустая строка, если category.id не указано. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.csv.exptected_columns + Возможные столбцы: + + + + + assembly.bom_import.template.csv.table + + + + + Столбец + Условие + Тип данных + Описание + + + + + quantity + Обязательное поле + Число с плавающей точкой (Float) + Должно быть заполнено и содержать число с плавающей точкой (Float), больше 0,0. + + + name + Необязательное + Строка + Название элемента в рамках сборки. + + + Столбцы, начинающиеся с part_ + + Если необходимо назначить деталь, то хотя бы один из следующих столбцов должен быть заполнен: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому ID детали в базе данных. + + + part_mpnr + Необязательное + Строка + Должно быть заполнено, если part_id, part_ipn или part_name не указаны. + + + part_ipn + Необязательное + Строка + Должно быть заполнено, если part_id, part_mpnr или part_name не указаны. + + + part_name + Необязательное + Строка + Должно быть заполнено, если part_id, part_mpnr или part_ipn не указаны. + + + part_description + Необязательное + Строка + Если указано, заменяет существующее значение описания деталя не пустой строкой. + + + Столбцы, начинающиеся с part_manufacturer_ + + Если необходимо указать производителя детали или найти деталь уникально по part_mpnr, должно быть заполнено хотя бы одно из следующих полей: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому ID производителя. + + + part_manufacturer_name + Необязательное + Строка + Должно быть заполнено, если part_manufacturer_id не указано. + + + Столбцы, начинающиеся с part_category_ + + Если необходимо изменить категорию детали, должно быть заполнено хотя бы одно из следующих полей: +
      +
    • part_category_id
    • +
    • part_category_name
    • +
    + + + + part_category_id + Необязательное + Целое число + Целое число > 0. Соответствует внутреннему числовому ID категории детали. + + + part_category_name + Необязательное + Строка + Должно быть заполнено, если part_category_id не указано. + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns + Ожидаемые столбцы: + + + + + assembly.bom_import.template.kicad_pcbnew.exptected_columns.note + + Примечание: Сопоставление с конкретными компонентами из управления категориями не выполняется.

    + ]]> +
    +
    +
    + + + assembly.bom_import.template.kicad_pcbnew.table + + + + + Поле + Условие + Тип данных + Описание + + + + + Id + Опционально + Целое + Свободное поле. Уникальный идентификационный номер для каждого компонента. + + + Package + Designator + Строка + Свободное поле. Уникальная ссылочная метка компонента на печатной плате, например, "R1" для резистора 1.
    Используется как наименование позиции в компоненте сборки. + + + Package + Опционально + Строка + Свободное поле. Тип корпуса или форм-фактор компонента, например, "0805" для SMD-резисторов.
    Не включается в информацию о компоненте сборки. + + + Quantity + Обязательно + Целое + Количество одинаковых компонентов, необходимых для создания одной версии сборки.
    Используется как количество в информации о компоненте сборки. + + + Designation + Обязательно + Строка + Описание или функция компонента, например, значение резистора "10kΩ" или значение конденсатора "100nF".
    Используется как наименование в информации о компоненте сборки. + + + Supplier and ref + Опционально + Строка + Свободное поле. Может содержать, например, информацию о конкретных поставщиках.
    Используется как примечание в информации о компоненте сборки. + + + + ]]> +
    +
    +
    + + + assembly_list.all.title + Все сборки + + + + + assembly.edit.tab.common + Общие + + + + + assembly.edit.tab.advanced + Дополнительные параметры + + + + + assembly.edit.tab.attachments + Вложения + + + + + assembly.filter.dbId + ID базы данных + + + + + assembly.filter.ipn + Внутренний номер детали (IPN) + + + + + assembly.filter.name + Название + + + + + assembly.filter.description + Описание + + + + + assembly.filter.comment + Комментарии + + + + + assembly.filter.attachments_count + Количество вложений + + + + + assembly.filter.attachmentName + Имя вложения + + + + + assemblies.create.btn + Создать новую сборку + + + + + assembly.table.id + ID + + + + + assembly.table.name + Название + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + Описание + + + + + assembly.table.referencedAssemblies + Ссылочные сборки + + + + + assembly.table.addedDate + Добавлено + + + + + assembly.table.lastModified + Последнее изменение + + + + + assembly.table.edit + Редактировать + + + + + assembly.table.edit.title + Редактировать сборку + + + + + assembly.table.invalid_regex + Неверное регулярное выражение (regex) + + + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + Название + + + + + assembly.bom.table.quantity + Количество + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + Описание + + + + + datasource.synonym + %name% (Ваш синоним: %synonym%) + + diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 668c32f28..2bd5e0135 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -351,6 +351,24 @@ 导出所有元素 + + + export.readable.label + 可读导出 + + + + + export.readable + CSV + + + + + export.readable_bom + PDF + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -548,6 +566,12 @@ 计量单位 + + + part_custom_state.caption + 部件的自定义状态 + + Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -1850,6 +1874,66 @@ 高级 + + + part.edit.tab.advanced.ipn.commonSectionHeader + Sugestie bez zwiększenia części + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + 包含部件数值增量的建议 + + + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + 部件的当前IPN规格 + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + 基于相同部件描述的下一个可能的IPN规格 + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + 直接类别的 IPN 前缀为空,请在类别“%name%”中指定。 + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + 直接类别的IPN前缀 + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + 直接类别的IPN前缀和部件特定的增量 + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + 具有父级前缀层级类别顺序的IPN前缀 + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + 具有父级前缀层级类别顺序和组件特定增量的IPN前缀 + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + 请先创建组件并将其分配到类别:基于现有类别及其专属的IPN前缀,可以自动建议组件的IPN + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -4749,6 +4833,24 @@ 名称 + + + part.table.name.value.for_part + %value%(部件) + + + + + part.table.name.value.for_assembly + %value%(装配) + + + + + part.table.name.value.for_project + %value%(项目) + + Part-DB1\src\DataTables\PartsDataTable.php:178 @@ -4839,6 +4941,12 @@ 计量单位 + + + part.table.partCustomState + 部件的自定义状态 + + Part-DB1\src\DataTables\PartsDataTable.php:236 @@ -5703,6 +5811,12 @@ 计量单位 + + + part.edit.partCustomState + 部件的自定义状态 + + Part-DB1\src\Form\Part\PartBaseType.php:212 @@ -5990,6 +6104,12 @@ 计量单位 + + + part_custom_state.label + 部件自定义状态 + + Part-DB1\src\Services\ElementTypeNameGenerator.php:90 @@ -6233,6 +6353,12 @@ 计量单位 + + + tree.tools.edit.part_custom_state + 部件自定义状态 + + Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 @@ -6967,6 +7093,12 @@ 名称过滤器 + + + category.edit.part_ipn_prefix + 部件 IPN 前缀 + + obsolete @@ -8502,6 +8634,12 @@ Element 3 计量单位 + + + perm.part_custom_states + 部件的自定义状态 + + obsolete @@ -9890,6 +10028,48 @@ Element 3 已存档 + + + assembly.edit.status + 装配状态 + + + + + assembly.edit.ipn + 内部零件号 (IPN) + + + + + 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 @@ -10280,12 +10460,24 @@ Element 3 + + + category.edit.part_ipn_prefix.placeholder + 例如:"B12A" + + category.edit.partname_regex.help 与PCRE兼容的正则表达式,部分名称必须匹配。 + + + category.edit.part_ipn_prefix.help + 输入零件IPN时建议的前缀。 + + entity.select.add_hint @@ -10832,6 +11024,12 @@ Element 3 计量单位 + + + log.element_edited.changed_fields.partCustomState + 部件的自定义状态 + + log.element_edited.changed_fields.expiration_date @@ -11036,6 +11234,18 @@ Element 3 Type + + + assembly.bom_import.type.json + JSON + + + + + assembly.bom_import.type.csv + CSV + + project.bom_import.type.kicad_pcbnew @@ -11048,6 +11258,319 @@ Element 3 导入前删除现有BOM条目 + + + project.import_bom.template.header.json + JSON导入模板 + + + + + project.import_bom.template.header.csv + CSV导入模板 + + + + + project.import_bom.template.header.kicad_pcbnew + CSV导入模板(KiCAD Pcbnew BOM) + + + + + project.bom_import.template.entry.name + 项目中的组件名称 + + + + + project.bom_import.template.entry.part.mpnr + 制造商的唯一产品编号 + + + + + project.bom_import.template.entry.part.ipn + 唯一的组件IPN + + + + + project.bom_import.template.entry.part.name + 组件唯一名称 + + + + + project.bom_import.template.entry.part.manufacturer.name + 制造商唯一名称 + + + + + project.bom_import.template.json.table + + + + + 字段 + 条件 + 数据类型 + 描述 + + + + + quantity + 必填 + 小数 (Float) + 必须提供,并包含大于 0.0 的小数值 (Float)。 + + + name + 可选 + 字符串 (String) + 如果存在,必须是非空字符串。物料清单中元素的名称。 + + + part + 可选 + 对象/数组 + + 如果需要分配组件,则必须是对象/数组,并且以下字段中的至少一个必须填写: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + 可选 + 整数 (Integer) + 整数 (Integer) > 0。对应于零件数据库 (Part-DB) 中组件的内部数字 ID。 + + + part.mpnr + 可选 + 字符串 (String) + 如果未提供 part.id、part.ipn 或 part.name,则为非空字符串。 + + + part.ipn + 可选 + 字符串 (String) + 如果未提供 part.id、part.mpnr 或 part.name,则为非空字符串。 + + + part.name + 可选 + 字符串 (String) + 如果未提供 part.id、part.mpnr 或 part.ipn,则为非空字符串。 + + + part.manufacturer + 可选 + 对象/数组 + + 如果需要调整组件的制造商,或者需要基于 part.mpnr 唯一标识组件,则必须是对象/数组,并且以下字段中的至少一个必须填写: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + 可选 + 整数 (Integer) + 整数 (Integer) > 0。对应于制造商的内部数字 ID。 + + + manufacturer.name + 可选 + 字符串 (String) + 如果未提供 manufacturer.id,则为非空字符串。 + + + + ]]> +
    +
    +
    + + + project.bom_import.template.csv.exptected_columns + 可能的列: + + + + + project.bom_import.template.csv.table + + + + + 列 + 条件 + 数据类型 + 描述 + + + + + quantity + 必填 + 浮点数 (Float) + 创建一个实例所需的相同组件数量。
    记录为组件条目的数量。 + + + name + Optional + String + 如果可用,则必须是非空字符串。材料清单中项目的名称。 + + + 以 part_ 开头的列 + + 如果需要分配一个组件,必须提供并填写以下列之一: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + 可选 + 整数 (Integer) + 整数 > 0。对应于零件数据库 (Part-DB) 中组件的内部数字 ID。 + + + part_mpnr + 可选 + 字符串 (String) + 如果 part_id、part_ipn 或 part_name 列未填写,则必须提供此列。 + + + part_ipn + 可选 + 字符串 (String) + 如果 part_id、part_mpnr 或 part_name 列未填写,则必须提供此列。 + + + part_name + 可选 + 字符串 (String) + 如果 part_id、part_mpnr 或 part_ipn 列未填写,则必须提供此列。 + + + 以 part_manufacturer_ 开头的列 + + 如果需要调整组件的制造商,或者组件需要根据 part_mpnr 值唯一标识,必须提供并填写以下列之一: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + 可选 + 整数 (Integer) + 整数 > 0。对应于制造商的内部数字 ID。 + + + part_manufacturer_name + 可选 + 字符串 (String) + 如果 part_manufacturer_id 列未填写,则必须提供此列。 + + + + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.exptected_columns + 预期的列: + + + + + project.bom_import.template.kicad_pcbnew.exptected_columns.note + + 注意:分类管理中不会执行与特定组件的映射。

    + ]]> +
    +
    +
    + + + project.bom_import.template.kicad_pcbnew.table + + + + + 字段 + 条件 + 数据类型 + 描述 + + + + + Id + 可选 + 整数 + 自由输入。每个组件的唯一标识编号。 + + + Designator + 可选 + 字符串 (String) + 自由输入。PCB 上组件的唯一参考标识符,例如“R1”代表电阻 1。
    会被采用到组件记录的装配名称中。 + + + Package + 可选 + 字符串 (String) + 自由输入。组件的封装或形状,例如 "0805" 表示 SMD 电阻。
    不会被采用到组件记录中。 + + + Quantity + 必填 + 整数 + 创建组件实例所需的相同组件的数量。
    会被采用为组件记录的数量。 + + + Designation + 必填 + 字符串 (String) + 组件的描述或功能,例如电阻值 “10kΩ” 或电容值 “100nF”。
    会被采用到组件记录的名称中。 + + + Supplier and ref + 可选 + 字符串 (String) + 自由输入。例如,可以包含供应商的特定值。
    会被采用为组件记录的备注。 + + + + ]]> +
    +
    +
    project.bom_import.clear_existing_bom.help @@ -11090,6 +11613,18 @@ Element 3 编辑度量单位 + + + part_custom_state.new + 部件的新自定义状态 + + + + + part_custom_state.edit + 编辑部件的自定义状态 + + user.aboutMe.label @@ -12208,5 +12743,934 @@ Element 3 成功创建 %COUNT% 个元素。 + + + assembly.label + 装配 + + + + + assembly.caption + 装配 + + + + + perm.assemblies + 装配列表 + + + + + assembly_bom_entry.label + 组件 + + + + + assembly.labelp + 装配列表 + + + + + assembly.referencedAssembly.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.build.form.referencedAssembly + 组件“%name%” + + + + + assembly.builds.no_stocked_builds + 已构建并库存的数量 + + + + + assembly.info.bom_entries_count + 条目 + + + + + assembly.info.sub_assemblies_count + 子组件 + + + + + assembly.builds.stocked + 库存中 + + + + + assembly.builds.needed + 需要 + + + + + assembly.bom.delete.confirm + 您确定要删除此项目吗? + + + + + assembly.add_parts_to_assembly + 添加零件到组件 + + + + + part.info.add_part_to_assembly + 将此零件添加到装配体中 + + + + + assembly.bom.project + 项目 + + + + + assembly.bom.referencedAssembly + 组件 + + + + + 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.partOrAssembly + 部件或组件 + + + + + 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.csv + 装配的CSV + + + + + assembly.bom_import.type.kicad_pcbnew + CSV 文件(KiCAD Pcbnew) + + + + + assembly.bom_import.type.kicad_schematic + KiCAD 原理图编辑器 BOM(CSV 文件) + + + + + 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.csv + 用于装配的CSV导入模板 + + + + + 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 + 可选 + 文本 + 如果填写,必须是非空文本。表示装配中的项目名称。 + + + part + 可选 + 对象/数组 + + 如果需要分配零件,它必须是一个对象/数组,并且至少需要填写以下字段之一: +
      +
    • part.id
    • +
    • part.mpnr
    • +
    • part.ipn
    • +
    • part.name
    • +
    + + + + part.id + 可选 + 整数 + 一个大于 0 的整数。对应数据库中零件的内部数字ID。 + + + part.mpnr + 可选 + 文本 + 如果 part.id、part.ipn 或 part.name 未填写,则必须是非空文本。 + + + part.ipn + 可选 + 文本 + 如果 part.id、part.mpnr 或 part.name 未填写,则必须是非空文本。 + + + part.name + 可选 + 文本 + 如果 part.id、part.mpnr 或 part.ipn 未填写,则必须是非空文本。 + + + part.description + 可选 + 文本或 null + 如果填写,必须是非空文本或 null。该值将替换零件中的现有值。 + + + part.manufacturer + 可选 + 对象/数组 + + 如果需要更改零件制造商或通过值 part.mpnr 唯一查找零件,它必须是一个对象/数组,并且至少需要填写以下字段之一: +
      +
    • manufacturer.id
    • +
    • manufacturer.name
    • +
    + + + + manufacturer.id + 可选 + 整数 + 一个大于 0 的整数。对应制造商的内部数字 ID。 + + + manufacturer.name + 可选 + 文本 + 如果 manufacturer.id 未填写,则必须是非空文本。 + + + part.category + 可选 + 对象/数组 + + 如果需要更改零件的类别,它必须是一个对象/数组,并且至少需要填写以下字段之一: +
      +
    • category.id
    • +
    • category.name
    • +
    + + + + category.id + 可选 + 整数 + 一个大于 0 的整数。对应零件类别的内部数字 ID。 + + + category.name + 可选 + 文本 + 如果 category.id 未填写,则必须是非空文本。 + + + + ]]> +
    +
    +
    + + + assembly.bom_import.template.csv.exptected_columns + 可用列: + + + + + assembly.bom_import.template.csv.table + + + + + 列名 + 条件 + 数据类型 + 描述 + + + + + quantity + 必填字段 + 浮点数 (Float) + 必须填写且包含大于 0.0 的浮点数 (Float)。 + + + name + 可选 + 文本 + 装配中的项目名称。 + + + 以 part_ 开头的列 + + 如果需要分配零件,则至少需要填写以下列之一: +
      +
    • part_id
    • +
    • part_mpnr
    • +
    • part_ipn
    • +
    • part_name
    • +
    + + + + part_id + 可选 + 整数 + 一个大于 0 的整数。对应数据库中零件的内部数字 ID。 + + + part_mpnr + 可选 + 文本 + 如果 part_id、part_ipn 或 part_name 未填写,则必须是非空文本。 + + + part_ipn + 可选 + 文本 + 如果 part_id、part_mpnr 或 part_name 未填写,则必须是非空文本。 + + + part_name + 可选 + 文本 + 如果 part_id、part_mpnr 或 part_ipn 未填写,则必须是非空文本。 + + + part_description + 可选 + 文本 + 如果指定,将取代现有描述值,且必须为非空文本。 + + + 以 part_manufacturer_ 开头的列 + + 如果需要更改零件的制造商或通过 part_mpnr 唯一查找,至少需要填写以下列之一: +
      +
    • part_manufacturer_id
    • +
    • part_manufacturer_name
    • +
    + + + + part_manufacturer_id + 可选 + 整数 + 一个大于 0 的整数。对应制造商的内部数字 ID。 + + + part_manufacturer_name + 可选 + 文本 + 如果 part_manufacturer_id 未填写,则必须是非空文本。 + + + 以 part_category_ 开头的列 + + 如果需要更改零件的类别,则至少需要填写以下列之一: +
      +
    • part_category_id
    • +
    • part_category_name
    • +
    + + + + part_category_id + 可选 + 整数 + 一个大于 0 的整数。对应零件类别的内部数字 ID。 + + + part_category_name + 可选 + 文本 + 如果 part_category_id 未填写,则必须是非空文本。 + + + + ]]> +
    +
    +
    + + + 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 + 可选 + 整数 + 自由字段。每个组件的唯一标识号。 + + + Designator + 可选 + 字符串 + 自由字段。PCB上组件的唯一参考标识符,例如电阻 "R1"。
    用于装配部件条目中的位置名称。 + + + Package + 可选 + 字符串 + 自由字段。组件的封装类型或外形规格,例如表面贴装电阻器 "0805"。
    不包含在装配部件条目信息中。 + + + Quantity + 必填 + 整数 + 创建一个装配实例所需的相同部件的数量。
    用于装配部件条目中的数量。 + + + Designation + 必填 + 字符串 + 组件的描述或功能,例如电阻的值 "10kΩ" 或电容的值 "100nF"。
    用于装配部件条目中的名称。 + + + 供应商及参考 + 可选 + 字符串 + 自由字段。例如,可以包含有关供应商的特定信息。
    用作装配部件信息中的备注。 + + + + ]]> +
    +
    +
    + + + assembly_list.all.title + 所有组件 + + + + + assembly.edit.tab.common + 通用 + + + + + assembly.edit.tab.advanced + 高级选项 + + + + + assembly.edit.tab.attachments + 附件 + + + + + assembly.filter.dbId + 数据库ID + + + + + assembly.filter.ipn + 内部零件编号(IPN) + + + + + assembly.filter.name + 名称 + + + + + assembly.filter.description + 描述 + + + + + assembly.filter.comment + 评论 + + + + + assembly.filter.attachments_count + 附件数量 + + + + + assembly.filter.attachmentName + 附件名称 + + + + + assemblies.create.btn + 创建新组件 + + + + + assembly.table.id + ID + + + + + assembly.table.name + 名称 + + + + + assembly.table.ipn + IPN + + + + + assembly.table.description + 描述 + + + + + assembly.table.referencedAssemblies + 引用的组件 + + + + + assembly.table.addedDate + 添加日期 + + + + + assembly.table.lastModified + 最后修改 + + + + + assembly.table.edit + 编辑 + + + + + assembly.table.edit.title + 编辑组件 + + + + + assembly.table.invalid_regex + 无效的正则表达式(regex) + + + + + assembly.bom.table.id + ID + + + + + assembly.bom.table.name + 名称 + + + + + assembly.bom.table.quantity + 数量 + + + + + assembly.bom.table.ipn + IPN + + + + + assembly.bom.table.description + 描述 + + + + + datasource.synonym + %name% (您的同义词: %synonym%) + + diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index c298266af..652cc2d5f 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -239,13 +239,19 @@ Interní číslo dílu musí být jedinečné. {{ value }} se již používá! + + + assembly.ipn.must_be_unique + Interní číslo dílu musí být jedinečné. {{ value }} se již používá! + + validator.project.bom_entry.name_or_part_needed Musíte vybrat díl pro položku BOM dílu nebo nastavit název pro položku BOM bez dílu. - + project.bom_entry.name_already_in_bom Již existuje položka BOM s tímto názvem! @@ -365,5 +371,113 @@ Neplatný kód. Zkontrolujte, zda je vaše ověřovací aplikace správně nastavena a zda je čas správně nastaven jak na serveru, tak na ověřovacím zařízení. + + + validator.bom_importer.invalid_import_type + Neplatný typ importu! + + + + + validator.bom_importer.invalid_file_extension + Neplatná přípona souboru "%extension%" pro typ importu "%importType%". Povolené přípony souborů: %allowedExtensions%. + + + + + assembly.bom_entry.part_already_in_bom + Tato součást již existuje ve skupině! + + + + + assembly.bom_entry.assembly_already_in_bom + Tato sestava již existuje jako položka v seznamu materiálů! + + + + + assembly.bom_entry.assembly_cycle + Byl zjištěn cyklus: Sestava "%name%" nepřímo odkazuje sama na sebe. + + + + + assembly.bom_entry.invalid_child_entry + Sestava nesmí ve svém seznamu materiálů (BOM) odkazovat na podskupinu, která je součástí její vlastní hierarchie. + + + + + assembly.bom_entry.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! + + + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Je povoleno vybrat pouze jednu součástku nebo sestavu. Upravit prosím svůj výběr! + + + + + validator.bom_importer.json_csv.quantity.required + Musíte zadat množství > 0! + + + + + validator.bom_importer.json_csv.quantity.float + očekává se jako float větší než 0,0 + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty + očekává se jako neprázdný řetězec + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty.null + očekává se jako neprázdný řetězec nebo null + + + + + validator.bom_importer.json_csv.parameter.array + očekává se jako pole (array) + + + + + validator.bom_importer.json_csv.parameter.subproperties + musí mít alespoň jeden z následujících pod-parametrů: %propertyString% + + + + + validator.bom_importer.json_csv.parameter.notFoundFor + nenalezeno pro %value% + + + + + validator.bom_importer.json_csv.parameter.noExactMatch + se přesně neshoduje. Pro import zadáno: %importValue%, nalezeno (%foundId%): %foundValue% + + + + + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties + musí obsahovat jako pod-parametr buď: "id" jako celé číslo větší než 0 nebo "name" jako neprázdný řetězec + + diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 21149f0e7..98d107755 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -239,6 +239,12 @@ Det interne partnummer skal være unikt. {{ value }} værdien er allerede i brug! + + + assembly.ipn.must_be_unique + Det interne partnummer skal være unikt. {{ value }} værdien er allerede i brug! + + validator.project.bom_entry.name_or_part_needed @@ -341,5 +347,113 @@ Denne leverandørstregkodeværdi er allerede brugt til en anden beholdning. Stregkoden skal være unik! + + + validator.bom_importer.invalid_import_type + Ugyldig importtype! + + + + + validator.bom_importer.invalid_file_extension + Ugyldig filtypenavn "%extension%" for importtypen "%importType%". Tilladte filtypenavne: %allowedExtensions%. + + + + + assembly.bom_entry.part_already_in_bom + Denne del eksisterer allerede i gruppen! + + + + + assembly.bom_entry.assembly_already_in_bom + Denne samling findes allerede som en post! + + + + + assembly.bom_entry.assembly_cycle + En cyklus blev opdaget: Samlingen "%name%" refererer indirekte til sig selv. + + + + + assembly.bom_entry.invalid_child_entry + En samling må ikke referere til en undergruppe fra sin egen hierarki i BOM-listerne. + + + + + 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! + + + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! + + + + + validator.bom_importer.json_csv.quantity.required + Du skal angive en mængde > 0! + + + + + validator.bom_importer.json_csv.quantity.float + forventet som en float større end 0,0 + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty + forventet som en ikke-tom streng + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty.null + forventet som en ikke-tom streng eller null + + + + + validator.bom_importer.json_csv.parameter.array + forventet som en array + + + + + validator.bom_importer.json_csv.parameter.subproperties + skal have mindst én af følgende underparametre: %propertyString% + + + + + validator.bom_importer.json_csv.parameter.notFoundFor + ikke fundet for %value% + + + + + validator.bom_importer.json_csv.parameter.noExactMatch + stemmer ikke helt overens. Givet til import: %importValue%, fundet (%foundId%): %foundValue% + + + + + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties + skal indeholde som en underparameter enten: "id" som et heltal større end 0 eller "name" som en ikke-tom streng + + diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 9c123fd85..d7b9591f5 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -239,10 +239,16 @@ Die Internal Part Number (IPN) muss einzigartig sein. Der Wert {{value}} wird bereits benutzt! + + + assembly.ipn.must_be_unique + Die Internal Part Number (IPN) muss einzigartig sein. Der Wert {{value}} wird bereits benutzt! + + validator.project.bom_entry.name_or_part_needed - 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! @@ -365,5 +371,113 @@ Ungültiger Code. Überprüfen Sie, dass die Authenticator App korrekt eingerichtet ist und dass der Server und das Gerät beide die korrekte Uhrzeit eingestellt haben. + + + validator.bom_importer.invalid_import_type + Ungültiger Importtyp! + + + + + validator.bom_importer.invalid_file_extension + Ungültige Dateierweiterung "%extension%" für den Importtyp "%importType%". Erlaubte Dateierweiterungen: %allowedExtensions%. + + + + + assembly.bom_entry.part_already_in_bom + Dieses Bauteil existiert bereits in der Gruppe! + + + + + assembly.bom_entry.assembly_already_in_bom + Diese Baugruppe existiert bereits als Eintrag! + + + + + assembly.bom_entry.assembly_cycle + Ein Zyklus wurde entdeckt: Die Baugruppe "%name%" referenziert sich indirekt selbst. + + + + + assembly.bom_entry.invalid_child_entry + Eine Baugruppe darf keine Unterbaugruppe aus seiner eigenen Hierarchie in den BOM-Einträgen referenzieren. + + + + + 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 den Eintrag setzen! + + + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Es darf nur ein Bauteil oder eine Baugruppe ausgewählt werden. Bitte passen Sie Ihre Auswahl an! + + + + + validator.bom_importer.json_csv.quantity.required + Sie müssen eine Stückzahl > 0 angeben! + + + + + validator.bom_importer.json_csv.quantity.float + wird als float größer als 0.0 erwartet + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty + als nicht leere Zeichenkette erwartet + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty.null + als nicht leere Zeichenkette oder null erwartet + + + + + validator.bom_importer.json_csv.parameter.array + als array erwartet + + + + + validator.bom_importer.json_csv.parameter.subproperties + muss mindestens eines der folgenden Unter-Parameter haben: %propertyString% + + + + + validator.bom_importer.json_csv.parameter.notFoundFor + nicht gefunden für %value% + + + + + validator.bom_importer.json_csv.parameter.noExactMatch + stimmt nicht genau überein. Für den Import gegeben: %importValue%, gefunden (%foundId%): %foundValue% + + + + + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties + muss entweder als Unter-Parameter zugewiesen haben: "id" als Ganzzahl größer als 0 oder "name" als nicht leere Zeichenfolge + + diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index 9ef5b3de4..320155fff 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -7,5 +7,119 @@ Ο εσωτερικός αριθμός εξαρτήματος πρέπει να είναι μοναδικός. {{ value }} χρησιμοποιείται ήδη! + + + assembly.ipn.must_be_unique + Ο εσωτερικός αριθμός εξαρτήματος πρέπει να είναι μοναδικός. {{ value }} χρησιμοποιείται ήδη! + + + + + validator.bom_importer.invalid_import_type + Μη έγκυρος τύπος εισαγωγής! + + + + + validator.bom_importer.invalid_file_extension + Μη έγκυρη επέκταση αρχείου "%extension%" για τον τύπο εισαγωγής "%importType%". Επιτρεπόμενες επεκτάσεις αρχείων: %allowedExtensions%. + + + + + assembly.bom_entry.part_already_in_bom + Αυτό το εξάρτημα υπάρχει ήδη στην ομάδα! + + + + + assembly.bom_entry.assembly_already_in_bom + Αυτή η συναρμολόγηση υπάρχει ήδη ως εγγραφή! + + + + + assembly.bom_entry.assembly_cycle + Εντοπίστηκε κύκλος: Η συναρμολόγηση "%name%" αναφέρεται έμμεσα στον εαυτό της. + + + + + assembly.bom_entry.invalid_child_entry + Μία συναρμολόγηση δεν πρέπει να αναφέρεται σε μία υποσυναρμολόγηση από την ίδια την ιεραρχία της στη λίστα BOM. + + + + + assembly.bom_entry.name_already_in_bom + Υπάρχει ήδη ένα εξάρτημα με αυτό το όνομα! + + + + + validator.assembly.bom_entry.name_or_part_needed + Πρέπει να επιλέξετε ένα εξάρτημα ή να βάλετε ένα όνομα για ένα μη εξάρτημα! + + + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg! + + + + + validator.bom_importer.json_csv.quantity.required + Πρέπει να εισαγάγετε ποσότητα > 0! + + + + + validator.bom_importer.json_csv.quantity.float + αναμένεται ως δεκαδικός αριθμός (float) μεγαλύτερος από 0,0 + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty + αναμένεται ως μη κενή συμβολοσειρά + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty.null + αναμένεται ως μη κενή συμβολοσειρά ή null + + + + + validator.bom_importer.json_csv.parameter.array + αναμένεται ως array + + + + + validator.bom_importer.json_csv.parameter.subproperties + πρέπει να έχει τουλάχιστον μία από τις ακόλουθες υπο-παραμέτρους: %propertyString% + + + + + validator.bom_importer.json_csv.parameter.notFoundFor + δεν βρέθηκε για %value% + + + + + validator.bom_importer.json_csv.parameter.noExactMatch + δεν ταιριάζει απόλυτα. Δόθηκε για εισαγωγή: %importValue%, βρέθηκε (%foundId%): %foundValue% + + + + + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties + πρέπει να περιέχει ως υπο-παράμετρο είτε: "id" ως ακέραιο αριθμό μεγαλύτερο από 0 είτε "name" ως μη κενή συμβολοσειρά + + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 6ad144607..95a558bff 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -239,10 +239,16 @@ The internal part number must be unique. {{ value }} is already in use! + + + assembly.ipn.must_be_unique + The internal part number must be unique. {{ value }} is already in use! + + validator.project.bom_entry.name_or_part_needed - 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! @@ -365,5 +371,113 @@ Invalid code. Check that your authenticator app is set up correctly and that both the server and authentication device has the time set correctly. + + + validator.bom_importer.invalid_import_type + Invalid import type! + + + + + validator.bom_importer.invalid_file_extension + Invalid file extension "%extension%" for import type %importType%". Allowed file extensions: %allowedExtensions%. + + + + + assembly.bom_entry.part_already_in_bom + This part already exists in the list! + + + + + assembly.bom_entry.assembly_already_in_bom + This assembly already exists as an entry! + + + + + assembly.bom_entry.assembly_cycle + A cycle was detected: the assembly "%name%" indirectly references itself. + + + + + assembly.bom_entry.invalid_child_entry + An assembly must not reference a subassembly from its own hierarchy in the BOM entries. + + + + + assembly.bom_entry.name_already_in_bom + There is already a part with this name! + + + + + validator.assembly.bom_entry.name_or_part_needed + You must select a part or set a name for the entry! + + + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Only one part or assembly may be selected. Please modify your selection! + + + + + validator.bom_importer.json_csv.quantity.required + you must specify a quantity > 0! + + + + + validator.bom_importer.json_csv.quantity.float + expected as float greater than 0.0 + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty + expected as non-empty string + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty.null + als nicht leere Zeichenkette oder null erwartet + + + + + validator.bom_importer.json_csv.parameter.array + expectd as array + + + + + validator.bom_importer.json_csv.parameter.subproperties + must have at least one of the following sub-properties: %propertyString% + + + + + validator.bom_importer.json_csv.parameter.notFoundFor + not found for %value% + + + + + validator.bom_importer.json_csv.parameter.noExactMatch + does not match exactly. Given for import: %importValue%, found (%foundId%): %foundValue% + + + + + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties + must have either assigned as sub-property: "id" as an integer greater than 0, or "name" as a non-empty string + + diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index e86ab9ccc..d8e824af9 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -203,5 +203,113 @@ L'emplacement de stockage a été marqué comme "Composant seul", par conséquent aucun nouveau composant ne peut être ajouté. + + + validator.bom_importer.invalid_import_type + Type d'importation invalide ! + + + + + validator.bom_importer.invalid_file_extension + Extension de fichier "%extension%" invalide pour le type d'importation "%importType%". Extensions de fichier autorisées : %allowedExtensions%. + + + + + assembly.bom_entry.part_already_in_bom + Cette pièce existe déjà dans le groupe! + + + + + assembly.bom_entry.assembly_already_in_bom + Cet assemblage existe déjà en tant qu'entrée ! + + + + + assembly.bom_entry.assembly_cycle + Un cycle a été détecté : L'assemblage "%name%" se réfère indirectement à lui-même. + + + + + assembly.bom_entry.invalid_child_entry + Un assemblage ne doit pas référencer un sous-assemblage de sa propre hiérarchie dans les entrées de la nomenclature (BOM). + + + + + assembly.bom_entry.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! + + + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Seule une pièce ou un assemblage peut être sélectionné. Veuillez ajuster votre sélection! + + + + + validator.bom_importer.json_csv.quantity.required + Vous devez entrer une quantité > 0 ! + + + + + validator.bom_importer.json_csv.quantity.float + attendu comme un nombre décimal (float) supérieur à 0,0 + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty + attendu comme une chaîne de caractères non vide + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty.null + attendu comme une chaîne de caractères non vide ou null + + + + + validator.bom_importer.json_csv.parameter.array + attendu comme un tableau (array) + + + + + validator.bom_importer.json_csv.parameter.subproperties + doit contenir au moins l'un des sous-paramètres suivants : %propertyString% + + + + + validator.bom_importer.json_csv.parameter.notFoundFor + non trouvé pour %value% + + + + + validator.bom_importer.json_csv.parameter.noExactMatch + ne correspond pas exactement. Donné pour l'importation : %importValue%, trouvé (%foundId%) : %foundValue% + + + + + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties + doit contenir comme sous-paramètre soit : "id" comme entier supérieur à 0 ou "name" comme chaîne de caractères non vide + + diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 29e32a16c..23ea0e849 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -239,6 +239,12 @@ Internal part number (IPN) mora biti jedinstven. {{ value }} je već u uporabi! + + + assembly.ipn.must_be_unique + Internal part number (IPN) mora biti jedinstven. {{ value }} je već u uporabi! + + validator.project.bom_entry.name_or_part_needed @@ -359,5 +365,113 @@ Neispravan kod. Provjerite je li vaša aplikacija za autentifikaciju ispravno postavljena i jesu li poslužitelj i uređaj za autentifikaciju ispravno postavili vrijeme. + + + validator.bom_importer.invalid_import_type + Nevažeći tip uvoza! + + + + + validator.bom_importer.invalid_file_extension + Nevažeća ekstenzija datoteke "%extension%" za tip uvoza "%importType%". Dopuštene ekstenzije datoteka: %allowedExtensions%. + + + + + assembly.bom_entry.part_already_in_bom + Ovaj dio već postoji u grupi! + + + + + assembly.bom_entry.assembly_already_in_bom + Ova se montaža već nalazi kao zapis! + + + + + assembly.bom_entry.assembly_cycle + Otkriven je ciklus: Sklop "%name%" neizravno referencira samog sebe. + + + + + assembly.bom_entry.invalid_child_entry + Sklop ne smije referencirati podsklop iz vlastite hijerarhije u unosima BOM-a. + + + + + 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! + + + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Dozvoljeno je odabrati samo jednu komponentu ili sklop. Molimo prilagodite svoj odabir! + + + + + validator.bom_importer.json_csv.quantity.required + Morate unijeti količinu > 0! + + + + + validator.bom_importer.json_csv.quantity.float + očekuje se decimalni broj (float) veći od 0,0 + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty + očekuje se kao neprazan niz znakova + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty.null + očekuje se kao neprazan niz znakova ili null + + + + + validator.bom_importer.json_csv.parameter.array + očekuje se kao niz + + + + + validator.bom_importer.json_csv.parameter.subproperties + mora sadržavati barem jedan od sljedećih pod-parametara: %propertyString% + + + + + validator.bom_importer.json_csv.parameter.notFoundFor + nije pronađeno za %value% + + + + + validator.bom_importer.json_csv.parameter.noExactMatch + ne podudara se točno. Uneseno za uvoz: %importValue%, pronađeno (%foundId%): %foundValue% + + + + + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties + mora sadržavati kao pod-parametar bilo: "id" kao cijeli broj veći od 0 ili "name" kao neprazan niz znakova + + diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index 7043f4f34..4851ab6eb 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -239,6 +239,12 @@ Il codice interno (IPN) deve essere univoco. Il valore {{value}} è già in uso! + + + assembly.ipn.must_be_unique + Il codice interno (IPN) deve essere univoco. Il valore {{value}} è già in uso! + + validator.project.bom_entry.name_or_part_needed @@ -359,5 +365,113 @@ Codice non valido. Controlla che la tua app di autenticazione sia impostata correttamente e che sia il server che il dispositivo di autenticazione abbiano l'ora impostata correttamente. + + + validator.bom_importer.invalid_import_type + Tipo di importazione non valido! + + + + + validator.bom_importer.invalid_file_extension + Estensione del file "%extension%" non valida per il tipo di importazione "%importType%". Estensioni consentite: %allowedExtensions%. + + + + + assembly.bom_entry.part_already_in_bom + Questa parte è già presente nel gruppo! + + + + + assembly.bom_entry.assembly_already_in_bom + Questo assemblaggio è già presente come voce! + + + + + assembly.bom_entry.assembly_cycle + È stato rilevato un ciclo: L'assemblaggio "%name%" fa riferimento indirettamente a sé stesso. + + + + + assembly.bom_entry.invalid_child_entry + Un assemblaggio non deve fare riferimento a un sottoassemblaggio nella propria gerarchia nelle voci della distinta base (BOM). + + + + + 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! + + + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + È consentito selezionare solo una parte o un assieme. Si prega di modificare la selezione! + + + + + validator.bom_importer.json_csv.quantity.required + Devi inserire una quantità > 0! + + + + + validator.bom_importer.json_csv.quantity.float + atteso come numero decimale (float) maggiore di 0,0 + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty + atteso come stringa non vuota + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty.null + atteso come stringa non vuota o null + + + + + validator.bom_importer.json_csv.parameter.array + atteso come array + + + + + validator.bom_importer.json_csv.parameter.subproperties + deve avere almeno uno dei seguenti sotto-parametri: %propertyString% + + + + + validator.bom_importer.json_csv.parameter.notFoundFor + non trovato per %value% + + + + + validator.bom_importer.json_csv.parameter.noExactMatch + non corrisponde esattamente. Valore dato per l'importazione: %importValue%, trovato (%foundId%): %foundValue% + + + + + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties + deve contenere come sotto-parametro: "id" come intero maggiore di 0 o "name" come stringa non vuota + + diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index 01cc3f77b..aed026eac 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -203,5 +203,113 @@ 新しい部品を追加できません。保管場所は「1つの部品のみ」とマークされています。 + + + validator.bom_importer.invalid_import_type + 無効なインポートタイプです! + + + + + validator.bom_importer.invalid_file_extension + インポートタイプ "%importType%" に対して無効なファイル拡張子 "%extension%"。許可されているファイル拡張子: %allowedExtensions%。 + + + + + assembly.bom_entry.part_already_in_bom + この部品はすでにグループに存在します! + + + + + assembly.bom_entry.assembly_already_in_bom + このアセンブリはすでにエントリとして存在します! + + + + + assembly.bom_entry.assembly_cycle + 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + + + + assembly.bom_entry.invalid_child_entry + アセンブリは、BOMエントリで自身の階層内のサブアセンブリを参照してはいけません。 + + + + + assembly.bom_entry.name_already_in_bom + この名前の部品はすでに存在します! + + + + + validator.assembly.bom_entry.name_or_part_needed + 部品を選択するか、非部品の名前を入力する必要があります! + + + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + 部品またはアセンブリのみ選択可能です。選択内容を調整してください! + + + + + validator.bom_importer.json_csv.quantity.required + 数量 > 0 を入力する必要があります! + + + + + validator.bom_importer.json_csv.quantity.float + 0.0 より大きい小数 (float) である必要があります + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty + 空でない文字列が期待されます + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty.null + 空でない文字列または null が期待されます + + + + + validator.bom_importer.json_csv.parameter.array + 配列として期待されます + + + + + validator.bom_importer.json_csv.parameter.subproperties + 以下のサブパラメーターのいずれかを含む必要があります:%propertyString% + + + + + validator.bom_importer.json_csv.parameter.notFoundFor + %value% に対する項目が見つかりません + + + + + validator.bom_importer.json_csv.parameter.noExactMatch + 完全には一致しません。インポートされた値:%importValue%、見つかった値 (%foundId%):%foundValue% + + + + + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties + サブパラメーターとして次のいずれかを含む必要があります:"id" は 0 より大きい整数、または "name" は空でない文字列 + + diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 6c9977983..68d65a4ad 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -239,6 +239,12 @@ Wewnętrzny numer części musi być unikalny. {{value }} jest już w użyciu! + + + assembly.ipn.must_be_unique + Wewnętrzny numer części musi być unikalny. {{value }} jest już w użyciu! + + validator.project.bom_entry.name_or_part_needed @@ -359,5 +365,113 @@ Nieprawidłowy kod. Sprawdź, czy aplikacja uwierzytelniająca jest poprawnie skonfigurowana i czy zarówno serwer, jak i urządzenie uwierzytelniające mają poprawnie ustawiony czas. + + + validator.bom_importer.invalid_import_type + Nieprawidłowy typ importu! + + + + + validator.bom_importer.invalid_file_extension + Nieprawidłowe rozszerzenie pliku "%extension%" dla typu importu "%importType%". Dozwolone rozszerzenia plików: %allowedExtensions%. + + + + + assembly.bom_entry.part_already_in_bom + Ten element już istnieje w grupie! + + + + + assembly.bom_entry.assembly_already_in_bom + To zestawienie jest już dodane jako wpis! + + + + + assembly.bom_entry.assembly_cycle + 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + + + + assembly.bom_entry.invalid_child_entry + Zespół nie może odwoływać się do podzespołu w swojej własnej hierarchii w wpisach BOM. + + + + + assembly.bom_entry.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! + + + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Można wybrać tylko jedną część lub zespół. Proszę dostosować swój wybór! + + + + + validator.bom_importer.json_csv.quantity.required + Musisz wprowadzić ilość > 0! + + + + + validator.bom_importer.json_csv.quantity.float + oczekiwano liczby zmiennoprzecinkowej (float) większej od 0,0 + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty + oczekiwano jako niepusty ciąg znaków + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty.null + oczekiwano jako niepusty ciąg znaków lub null + + + + + validator.bom_importer.json_csv.parameter.array + oczekiwano jako tablicę + + + + + validator.bom_importer.json_csv.parameter.subproperties + musi zawierać co najmniej jeden z następujących podparametrów: %propertyString% + + + + + validator.bom_importer.json_csv.parameter.notFoundFor + nie znaleziono dla %value% + + + + + validator.bom_importer.json_csv.parameter.noExactMatch + brak dokładnego dopasowania. Wprowadzone do importu: %importValue%, znalezione (%foundId%): %foundValue% + + + + + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties + musi zawierać jako podparametr: "id" jako liczbę całkowitą większą od 0 lub "name" jako niepusty ciąg znaków + + diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 0f97c4781..f80d91d72 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -239,6 +239,12 @@ Внутренний номер детали (IPN) должен быть уникальным. Значение {{value}} уже используется! + + + assembly.ipn.must_be_unique + Внутренний номер детали (IPN) должен быть уникальным. Значение {{value}} уже используется! + + validator.project.bom_entry.name_or_part_needed @@ -359,5 +365,113 @@ Неверный код. Проверьте, что приложение аутентификации настроено правильно и что на сервере и устройстве аутентификации установлено правильное время. + + + validator.bom_importer.invalid_import_type + Недопустимый тип импорта! + + + + + validator.bom_importer.invalid_file_extension + Недопустимое расширение файла "%extension%" для типа импорта "%importType%". Допустимые расширения файлов: %allowedExtensions%. + + + + + assembly.bom_entry.part_already_in_bom + Эта деталь уже существует в группе! + + + + + assembly.bom_entry.assembly_already_in_bom + Этот сборочный узел уже добавлен как запись! + + + + + assembly.bom_entry.assembly_cycle + Обнаружен цикл: Сборка «%name%» косвенно ссылается на саму себя. + + + + + assembly.bom_entry.invalid_child_entry + Сборка не должна ссылаться на подсборку внутри своей собственной иерархии в записях спецификации (BOM). + + + + + assembly.bom_entry.name_already_in_bom + Деталь с таким названием уже существует! + + + + + validator.assembly.bom_entry.name_or_part_needed + Необходимо выбрать деталь или ввести название для недетали! + + + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + Можно выбрать только деталь или сборку. Пожалуйста, измените ваш выбор! + + + + + validator.bom_importer.json_csv.quantity.required + Необходимо указать количество > 0! + + + + + validator.bom_importer.json_csv.quantity.float + ожидается число с плавающей запятой (float), большее 0,0 + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty + ожидается непустая строка + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty.null + ожидается непустая строка или null + + + + + validator.bom_importer.json_csv.parameter.array + ожидается массив + + + + + validator.bom_importer.json_csv.parameter.subproperties + должен содержать хотя бы один из следующих под-параметров: %propertyString% + + + + + validator.bom_importer.json_csv.parameter.notFoundFor + не найдено для %value% + + + + + validator.bom_importer.json_csv.parameter.noExactMatch + точное совпадение отсутствует. Указано для импорта: %importValue%, найдено (%foundId%): %foundValue% + + + + + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties + должен содержать под-параметр: "id" как целое число больше 0 или "name" как непустая строка + + diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 08c9f014e..c7425081b 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -239,6 +239,12 @@ 内部部件号是唯一的。{{ value }} 已被使用! + + + assembly.ipn.must_be_unique + 内部部件号是唯一的。{{ value }} 已被使用! + + validator.project.bom_entry.name_or_part_needed @@ -347,5 +353,113 @@ 由于技术限制,在32位系统中无法选择2038年1月19日之后的日期! + + + validator.bom_importer.invalid_import_type + 无效的导入类型! + + + + + validator.bom_importer.invalid_file_extension + 导入类型“%importType%”的文件扩展名“%extension%”无效。允许的扩展名: %allowedExtensions%。 + + + + + assembly.bom_entry.part_already_in_bom + 此零件已存在于组中! + + + + + assembly.bom_entry.assembly_already_in_bom + 此装配已经作为条目存在! + + + + + assembly.bom_entry.assembly_cycle + 检测到循环:装配体“%name%”间接引用了其自身。 + + + + + assembly.bom_entry.invalid_child_entry + Сборка не должна ссылаться на подсборку внутри своей собственной иерархии в записях спецификации (BOM). + + + + + assembly.bom_entry.name_already_in_bom + 具有此名称的零件已存在! + + + + + validator.assembly.bom_entry.name_or_part_needed + 必须选择零件或为非零件指定名称! + + + + + validator.assembly.bom_entry.only_part_or_assembly_allowed + 只能选择一个零件或组件。请修改您的选择! + + + + + validator.bom_importer.json_csv.quantity.required + 必须输入数量 > 0! + + + + + validator.bom_importer.json_csv.quantity.float + 应为大于 0.0 的浮点数 (float) + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty + 应为非空字符串 + + + + + validator.bom_importer.json_csv.parameter.string.notEmpty.null + 应为非空字符串或 null + + + + + validator.bom_importer.json_csv.parameter.array + 应为数组 + + + + + validator.bom_importer.json_csv.parameter.subproperties + 必须包含以下子参数之一:%propertyString% + + + + + validator.bom_importer.json_csv.parameter.notFoundFor + 未找到对应值 %value% + + + + + validator.bom_importer.json_csv.parameter.noExactMatch + 未精确匹配。用于导入的值:%importValue%,找到的值 (%foundId%):%foundValue% + + + + + validator.bom_importer.json_csv.parameter.manufacturerOrCategoryWithSubProperties + 必须包含子参数:"id" 为大于 0 的整数,或 "name" 为非空字符串 + +