{% trans %}project.bom_import.field_mapping.csv_field{% endtrans %}
-
{% trans %}project.bom_import.field_mapping.maps_to{% endtrans %}
-
{% trans %}project.bom_import.field_mapping.suggestion{% endtrans %}
-
{% trans %}project.bom_import.field_mapping.priority{% endtrans %}
-
+
+
{% trans %}project.bom_import.field_mapping.csv_field{% endtrans %}
+
{% trans %}project.bom_import.field_mapping.maps_to{% endtrans %}
+
{% trans %}project.bom_import.field_mapping.suggestion{% endtrans %}
+
{% trans %}project.bom_import.field_mapping.priority{% endtrans %}
+
- {% for field in detected_fields %}
-
-
- {{ field }}
-
-
+ {% for field in detected_fields %}
+
+
+ {{ field }}
+
+
+ {# TODO: This is more a workaround than a proper fix. Ideally the controller should be fixed in a way, that we get the correct fields here #}
+ {% if field_name_mapping[field] is defined %}
+ {% set field_name = field_name_mapping[field] %}
{{ form_widget(form['mapping_' ~ field_name_mapping[field]], {
'attr': {
'class': 'form-select field-mapping-select',
'data-field': field
}
}) }}
-
-
- {% if suggested_mapping[field] is defined %}
-
+ {% else %}
+
+ {% trans %}project.bom_import.field_mapping.error.check_delimiter{% endtrans %}
+
+ {% endif %}
+
+
+ {% if suggested_mapping[field] is defined %}
+
{{ suggested_mapping[field] }}
- {% else %}
-
+ {% else %}
+
{% trans %}project.bom_import.field_mapping.no_suggestion{% endtrans %}
- {% endif %}
-
-
-
-
-
- {% endfor %}
+ {% endif %}
+
+
+
+
+
+ {% endfor %}
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index 180d9e5e..d92af86d 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -12947,5 +12947,11 @@ Buerklin-API Authentication server:
[Part_lot] created from barcode: Please check if the data is correct and desired.
+
+
+ project.bom_import.field_mapping.error.check_delimiter
+ Mapping error: Check if you have selected the right delimiter!
+
+
From a070ebb2ce0214b6babedeb0114bf3ae24baa052 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Sun, 15 Mar 2026 22:02:10 +0100
Subject: [PATCH 014/108] Fixed 500 error with displaying part prices, when a
user has a currency preference different of base currency, and there is no
conversion rate known for it
This fixes issue #1317
---
templates/parts/info/_order_infos.html.twig | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/templates/parts/info/_order_infos.html.twig b/templates/parts/info/_order_infos.html.twig
index 59b904df..9aa9d888 100644
--- a/templates/parts/info/_order_infos.html.twig
+++ b/templates/parts/info/_order_infos.html.twig
@@ -47,17 +47,17 @@
+ {% endif %}
{% if not loop.last %}
diff --git a/tests/ApplicationAvailabilityFunctionalTest.php b/tests/ApplicationAvailabilityFunctionalTest.php
index c7449411..3bb222d0 100644
--- a/tests/ApplicationAvailabilityFunctionalTest.php
+++ b/tests/ApplicationAvailabilityFunctionalTest.php
@@ -60,6 +60,7 @@ final class ApplicationAvailabilityFunctionalTest extends WebTestCase
//User related things
yield ['/user/settings'];
yield ['/user/info'];
+ yield ['/settings/misc/kicad-lists'];
//Login/logout
yield ['/login'];
diff --git a/tests/Controller/KicadListEditorControllerTest.php b/tests/Controller/KicadListEditorControllerTest.php
new file mode 100644
index 00000000..0aa05aa1
--- /dev/null
+++ b/tests/Controller/KicadListEditorControllerTest.php
@@ -0,0 +1,162 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\Tests\Controller;
+
+use App\Entity\UserSystem\User;
+use App\Settings\MiscSettings\KiCadEDASettings;
+use Jbtronics\SettingsBundle\Manager\SettingsManagerInterface;
+use PHPUnit\Framework\Attributes\Group;
+use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+
+#[Group('slow')]
+#[Group('DB')]
+final class KicadListEditorControllerTest extends WebTestCase
+{
+ private string $footprintsPath;
+ private string $symbolsPath;
+ private string $customFootprintsPath;
+ private string $customSymbolsPath;
+ private string $originalFootprints;
+ private string $originalSymbols;
+ private string $originalCustomFootprints;
+ private string $originalCustomSymbols;
+ private bool $originalUseCustomList;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $projectDir = dirname(__DIR__, 2);
+ $this->footprintsPath = $projectDir . '/public/kicad/footprints.txt';
+ $this->symbolsPath = $projectDir . '/public/kicad/symbols.txt';
+ $this->customFootprintsPath = $projectDir . '/public/kicad/footprints_custom.txt';
+ $this->customSymbolsPath = $projectDir . '/public/kicad/symbols_custom.txt';
+ $this->originalFootprints = (string) file_get_contents($this->footprintsPath);
+ $this->originalSymbols = (string) file_get_contents($this->symbolsPath);
+ $this->originalCustomFootprints = is_file($this->customFootprintsPath) ? (string) file_get_contents($this->customFootprintsPath) : '';
+ $this->originalCustomSymbols = is_file($this->customSymbolsPath) ? (string) file_get_contents($this->customSymbolsPath) : '';
+
+ static::bootKernel();
+ /** @var SettingsManagerInterface $settingsManager */
+ $settingsManager = static::getContainer()->get(SettingsManagerInterface::class);
+ /** @var KiCadEDASettings $settings */
+ $settings = $settingsManager->get(KiCadEDASettings::class);
+ $this->originalUseCustomList = $settings->useCustomList;
+ static::ensureKernelShutdown();
+ }
+
+ protected function tearDown(): void
+ {
+ file_put_contents($this->footprintsPath, $this->originalFootprints);
+ file_put_contents($this->symbolsPath, $this->originalSymbols);
+ file_put_contents($this->customFootprintsPath, $this->originalCustomFootprints);
+ file_put_contents($this->customSymbolsPath, $this->originalCustomSymbols);
+
+ static::bootKernel();
+ /** @var SettingsManagerInterface $settingsManager */
+ $settingsManager = static::getContainer()->get(SettingsManagerInterface::class);
+ /** @var KiCadEDASettings $settings */
+ $settings = $settingsManager->get(KiCadEDASettings::class);
+ $settings->useCustomList = $this->originalUseCustomList;
+ $settingsManager->save($settings);
+ static::ensureKernelShutdown();
+
+ parent::tearDown();
+ }
+
+ public function testEditorRequiresAuthentication(): void
+ {
+ $client = static::createClient();
+ $client->request('GET', '/en/settings/misc/kicad-lists');
+
+ $this->assertResponseStatusCodeSame(401);
+ }
+
+ public function testEditorAccessibleByAdmin(): void
+ {
+ $client = static::createClient();
+ $this->loginAsUser($client, 'admin');
+
+ $client->request('GET', '/en/settings/misc/kicad-lists');
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSelectorExists('form[name="kicad_list_editor"]');
+ }
+
+ public function testEditorShowsDefaultAndCustomFiles(): void
+ {
+ $client = static::createClient();
+ $this->loginAsUser($client, 'admin');
+
+ file_put_contents($this->footprintsPath, "DefaultFootprint\n");
+ file_put_contents($this->symbolsPath, "DefaultSymbol\n");
+ file_put_contents($this->customFootprintsPath, "CustomFootprint\n");
+ file_put_contents($this->customSymbolsPath, "CustomSymbol\n");
+
+ $crawler = $client->request('GET', '/en/settings/misc/kicad-lists');
+
+ $this->assertSame("CustomFootprint\n", $crawler->filter('#kicad_list_editor_customFootprints')->getNode(0)->nodeValue);
+ $this->assertSame("CustomSymbol\n", $crawler->filter('#kicad_list_editor_customSymbols')->getNode(0)->nodeValue);
+ $this->assertSame("DefaultFootprint\n", $crawler->filter('#kicad_list_editor_defaultFootprints')->getNode(0)->nodeValue);
+ $this->assertSame("DefaultSymbol\n", $crawler->filter('#kicad_list_editor_defaultSymbols')->getNode(0)->nodeValue);
+ }
+
+ public function testEditorSavesCustomFilesAndSetting(): void
+ {
+ $client = static::createClient();
+ $this->loginAsUser($client, 'admin');
+
+ $crawler = $client->request('GET', '/en/settings/misc/kicad-lists');
+ $form = $crawler->filter('form[name="kicad_list_editor"]')->form();
+ $form['kicad_list_editor[customFootprints]'] = "Package_DIP:DIP-8_W7.62mm\n";
+ $form['kicad_list_editor[customSymbols]'] = "Device:R\n";
+ $form['kicad_list_editor[useCustomList]']->tick();
+
+ $client->submit($form);
+
+ $this->assertResponseRedirects('/en/settings/misc/kicad-lists');
+ $this->assertSame("Package_DIP:DIP-8_W7.62mm\n", (string) file_get_contents($this->customFootprintsPath));
+ $this->assertSame("Device:R\n", (string) file_get_contents($this->customSymbolsPath));
+ $this->assertSame($this->originalFootprints, (string) file_get_contents($this->footprintsPath));
+ $this->assertSame($this->originalSymbols, (string) file_get_contents($this->symbolsPath));
+
+ /** @var SettingsManagerInterface $settingsManager */
+ $settingsManager = $client->getContainer()->get(SettingsManagerInterface::class);
+ /** @var KiCadEDASettings $settings */
+ $settings = $settingsManager->reload(KiCadEDASettings::class);
+ $this->assertTrue($settings->useCustomList);
+ }
+
+ private function loginAsUser($client, string $username): void
+ {
+ $entityManager = $client->getContainer()->get('doctrine')->getManager();
+ $userRepository = $entityManager->getRepository(User::class);
+ $user = $userRepository->findOneBy(['name' => $username]);
+
+ if (!$user) {
+ $this->markTestSkipped(sprintf('User "%s" not found in fixtures', $username));
+ }
+
+ $client->loginUser($user);
+ }
+}
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index a8db61ac..176c6650 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -10029,6 +10029,90 @@ Please note, that you can not impersonate a disabled user. If you try you will g
When enabled, the datasheet field in KiCad will link to the actual PDF file (if found). When disabled, it will link to the Part-DB page instead. The Part-DB page link is always available as a separate "Part-DB URL" field.
+
+
+ settings.misc.kicad_eda.editor.title
+ KiCad autocomplete lists
+
+
+
+
+ settings.misc.kicad_eda.editor.link
+ Autocomplete settings
+
+
+
+
+ settings.misc.kicad_eda.editor.description
+ Configure whether KiCad autocomplete uses the autogenerated default lists or your custom override files. The custom files are editable here, while the default files are shown read-only for reference.
+
+
+
+
+ settings.misc.kicad_eda.editor.footprints
+ Footprints list
+
+
+
+
+ settings.misc.kicad_eda.editor.footprints.help
+ One entry per line. Used as autocomplete suggestions for KiCad footprint fields.
+
+
+
+
+ settings.misc.kicad_eda.editor.symbols
+ Symbols list
+
+
+
+
+ settings.misc.kicad_eda.editor.symbols.help
+ One entry per line. Used as autocomplete suggestions for KiCad symbol fields.
+
+
+
+
+ settings.misc.kicad_eda.use_custom_list
+ Use custom autocomplete lists
+
+
+
+
+ settings.misc.kicad_eda.use_custom_list.help
+ When enabled, KiCad autocomplete uses public/kicad/footprints_custom.txt and public/kicad/symbols_custom.txt instead of the autogenerated default files.
+
+
+
+
+ settings.misc.kicad_eda.editor.custom_footprints
+ Custom footprints list
+
+
+
+
+ settings.misc.kicad_eda.editor.custom_symbols
+ Custom symbols list
+
+
+
+
+ settings.misc.kicad_eda.editor.default_footprints
+ Default footprints list
+
+
+
+
+ settings.misc.kicad_eda.editor.default_symbols
+ Default symbols list
+
+
+
+
+ settings.misc.kicad_eda.editor.default_files_help
+ Autogenerated file shown for reference only. Changes must be made in the custom list.
+
+ settings.behavior.sidebar
From 5b86d6f652966ce2c88ed904718a9d743b6c24d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Wed, 15 Apr 2026 00:04:52 +0200
Subject: [PATCH 041/108] Require full authentication for the system settings,
as some of the settings are quite critical
---
src/Controller/SettingsController.php | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/Controller/SettingsController.php b/src/Controller/SettingsController.php
index 15c945f6..5fed1571 100644
--- a/src/Controller/SettingsController.php
+++ b/src/Controller/SettingsController.php
@@ -44,6 +44,7 @@ class SettingsController extends AbstractController
public function systemSettings(Request $request, TagAwareCacheInterface $cache): Response
{
$this->denyAccessUnlessGranted('@config.change_system_settings');
+ $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
//Create a clone of the settings object
$settings = $this->settingsManager->createTemporaryCopy(AppSettings::class);
From c17cf5e83c25dc27f611ed855d74bae117a72731 Mon Sep 17 00:00:00 2001
From: Niklas <44636701+MayNiklas@users.noreply.github.com>
Date: Wed, 15 Apr 2026 22:13:07 +0200
Subject: [PATCH 042/108] Add price columns to project BOM table and build
price summary (#1345)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Add unit price and extended price columns to project BOM table
Adds two optional columns to the project BOM datatable (hidden by
default, toggleable via column visibility):
- **Price**: unit price for the BOM entry in the base currency,
looked up via PricedetailHelper. For parts whose BOM quantity falls
below the minimum order amount the minimum order amount is used for
the price tier lookup so that a price is always returned.
- **Extended Price**: unit price multiplied by the BOM quantity.
Prices are rendered via MoneyFormatter (locale-aware, with currency
symbol). Both columns round up to 2 decimal places to avoid displaying
0.00 for very small prices.
* Add translation key for project.bom.ext_price
Adds the English translation "Extended Price" for the new BOM extended
price column. Other languages are marked needs-translation and will be
picked up by Crowdin.
* Add build price summary to project info tab
Displays the total BOM price for N builds on the project info page,
using the existing price-tier logic from PricedetailHelper. The user
can adjust the number of builds via a small form; the unit price is
also shown when N > 1.
New backend:
- ProjectBuildHelper gains calculateTotalBuildPrice(),
calculateUnitBuildPrice(), roundedTotalBuildPrice(), and
roundedUnitBuildPrice() — bulk-order quantities are factored in so
that price tiers apply correctly across N builds.
- ProjectController::info() now reads ?n= and passes number_of_builds
to the template.
Template (_info.html.twig):
- Adds price badge (hidden when no pricing data is available).
- Adds number-of-builds form that reloads the info page.
* Add tests for build price calculation in ProjectBuildHelper
Covers calculateTotalBuildPrice(), calculateUnitBuildPrice(),
roundedTotalBuildPrice(), and the private getBomEntryUnitPrice()
helper. Scenarios tested: empty project, no pricing data, non-part BOM
entries with manual prices, part entries with pricedetails, mixed
entries, rounding-up of sub-cent prices, and minimum order amount
floor for price tier lookup.
* Deduplicate BOM entry price logic into ProjectBuildHelper
The private getBomEntryUnitPrice() in ProjectBomEntriesDataTable was
identical to the one in ProjectBuildHelper. Replaced it with a new
public getEntryUnitPrice() on ProjectBuildHelper (returns BigDecimal,
never null) and delegate to it from the DataTable.
This eliminates the duplicate code and brings the DataTable lines under
the existing ProjectBuildHelper test coverage. Added three tests for
getEntryUnitPrice() covering the no-pricing, non-part, and part cases.
* Added type hint to service
---------
Co-authored-by: Jan Böhmer
---
src/Controller/ProjectController.php | 3 +
src/DataTables/ProjectBomEntriesDataTable.php | 30 ++-
.../ProjectSystem/ProjectBuildHelper.php | 87 ++++++++-
templates/projects/info/_info.html.twig | 36 +++-
.../ProjectSystem/ProjectBuildHelperTest.php | 180 +++++++++++++++++-
.../ProjectBuildPartHelperTest.php | 3 +-
translations/messages.cs.xlf | 6 +
translations/messages.da.xlf | 6 +
translations/messages.de.xlf | 6 +
translations/messages.en.xlf | 18 ++
translations/messages.es.xlf | 6 +
translations/messages.hu.xlf | 6 +
translations/messages.it.xlf | 6 +
translations/messages.pl.xlf | 6 +
translations/messages.ru.xlf | 6 +
translations/messages.zh.xlf | 6 +
16 files changed, 398 insertions(+), 13 deletions(-)
diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php
index d2c35efd..531deb3f 100644
--- a/src/Controller/ProjectController.php
+++ b/src/Controller/ProjectController.php
@@ -69,10 +69,13 @@ class ProjectController extends AbstractController
return $table->getResponse();
}
+ $number_of_builds = max(1, $request->query->getInt('n', 1));
+
return $this->render('projects/info/info.html.twig', [
'buildHelper' => $buildHelper,
'datatable' => $table,
'project' => $project,
+ 'number_of_builds' => $number_of_builds,
]);
}
diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php
index 04d8206b..2d5c4ebc 100644
--- a/src/DataTables/ProjectBomEntriesDataTable.php
+++ b/src/DataTables/ProjectBomEntriesDataTable.php
@@ -29,12 +29,15 @@ use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\MarkdownColumn;
use App\DataTables\Helpers\PartDataTableHelper;
use App\Doctrine\Helpers\FieldHelper;
-use App\Entity\Parts\Part;
use App\Entity\Parts\ManufacturingStatus;
+use App\Entity\Parts\Part;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Services\ElementTypeNameGenerator;
use App\Services\EntityURLGenerator;
use App\Services\Formatters\AmountFormatter;
+use App\Services\Formatters\MoneyFormatter;
+use App\Services\ProjectSystem\ProjectBuildHelper;
+use Brick\Math\RoundingMode;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
@@ -50,7 +53,9 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
protected EntityURLGenerator $entityURLGenerator,
protected TranslatorInterface $translator,
protected AmountFormatter $amountFormatter,
- protected PartDataTableHelper $partDataTableHelper
+ protected PartDataTableHelper $partDataTableHelper,
+ protected ProjectBuildHelper $projectBuildHelper,
+ protected MoneyFormatter $moneyFormatter,
) {
}
@@ -202,6 +207,27 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
return '';
}
])
+ ->add('price', TextColumn::class, [
+ 'label' => 'project.bom.price',
+ 'visible' => false,
+ 'render' => function ($value, ProjectBOMEntry $context) {
+ $price = $this->projectBuildHelper->getEntryUnitPrice($context);
+ return $this->moneyFormatter->format($price->toScale(2, RoundingMode::UP)->toFloat(), null, 2, true);
+ },
+ ])
+ ->add('ext_price', TextColumn::class, [
+ 'label' => 'project.bom.ext_price',
+ 'visible' => false,
+ 'render' => function ($value, ProjectBOMEntry $context) {
+ $price = $this->projectBuildHelper->getEntryUnitPrice($context);
+ return $this->moneyFormatter->format(
+ $price->multipliedBy($context->getQuantity())->toScale(2, RoundingMode::UP)->toFloat(),
+ null,
+ 2,
+ true
+ );
+ },
+ ])
->add('addedDate', LocaleDateTimeColumn::class, [
'label' => $this->translator->trans('part.table.addedDate'),
diff --git a/src/Services/ProjectSystem/ProjectBuildHelper.php b/src/Services/ProjectSystem/ProjectBuildHelper.php
index a541c29d..ee5b8c68 100644
--- a/src/Services/ProjectSystem/ProjectBuildHelper.php
+++ b/src/Services/ProjectSystem/ProjectBuildHelper.php
@@ -25,16 +25,22 @@ namespace App\Services\ProjectSystem;
use App\Entity\Parts\Part;
use App\Entity\ProjectSystem\Project;
use App\Entity\ProjectSystem\ProjectBOMEntry;
+use App\Entity\PriceInformations\Currency;
use App\Helpers\Projects\ProjectBuildRequest;
use App\Services\Parts\PartLotWithdrawAddHelper;
+use App\Services\Parts\PricedetailHelper;
+use Brick\Math\BigDecimal;
+use Brick\Math\RoundingMode;
/**
* @see \App\Tests\Services\ProjectSystem\ProjectBuildHelperTest
*/
final readonly class ProjectBuildHelper
{
- public function __construct(private PartLotWithdrawAddHelper $withdraw_add_helper)
- {
+ public function __construct(
+ private PartLotWithdrawAddHelper $withdraw_add_helper,
+ private PricedetailHelper $pricedetailHelper,
+ ) {
}
/**
@@ -168,4 +174,81 @@ final readonly class ProjectBuildHelper
$this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message);
}
}
+
+ /**
+ * Calculates the total price to build the given project N times, taking bulk pricing into account.
+ * Returns null if no BOM entry has any pricing information.
+ */
+ public function calculateTotalBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
+ {
+ $total = BigDecimal::zero();
+ $has_price = false;
+
+ foreach ($project->getBomEntries() as $entry) {
+ $unit_price = $this->getBomEntryUnitPrice($entry, $number_of_builds, $currency);
+ if ($unit_price === null) {
+ continue;
+ }
+ $has_price = true;
+ $total = $total->plus($unit_price->multipliedBy($entry->getQuantity())->multipliedBy($number_of_builds));
+ }
+
+ return $has_price ? $total : null;
+ }
+
+ /**
+ * Calculates the price to build one unit of the given project when ordering for N builds in total.
+ * Returns null if no BOM entry has any pricing information.
+ */
+ public function calculateUnitBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
+ {
+ $total = $this->calculateTotalBuildPrice($project, $number_of_builds, $currency);
+ if ($total === null) {
+ return null;
+ }
+ return $total->dividedBy($number_of_builds, 10, RoundingMode::HALF_UP);
+ }
+
+ /**
+ * Returns the total build price rounded up to 2 decimal places, ready for display.
+ */
+ public function roundedTotalBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
+ {
+ return $this->calculateTotalBuildPrice($project, $number_of_builds, $currency)
+ ?->toScale(2, RoundingMode::UP);
+ }
+
+ /**
+ * Returns the unit build price rounded up to 2 decimal places, ready for display.
+ */
+ public function roundedUnitBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
+ {
+ return $this->calculateUnitBuildPrice($project, $number_of_builds, $currency)
+ ?->toScale(2, RoundingMode::UP);
+ }
+
+ /**
+ * Returns the effective unit price for a single piece of the given BOM entry,
+ * taking bulk pricing and minimum order amounts into account for N builds.
+ * Returns BigDecimal::zero() when no pricing data is available.
+ */
+ public function getEntryUnitPrice(ProjectBOMEntry $entry, int $number_of_builds = 1, ?Currency $currency = null): BigDecimal
+ {
+ return $this->getBomEntryUnitPrice($entry, $number_of_builds, $currency) ?? BigDecimal::zero();
+ }
+
+ /**
+ * Returns the effective unit price for a single piece of the given BOM entry,
+ * taking bulk pricing into account for N builds.
+ */
+ private function getBomEntryUnitPrice(ProjectBOMEntry $entry, int $number_of_builds, ?Currency $currency): ?BigDecimal
+ {
+ if ($entry->getPart() instanceof Part) {
+ $total_qty = $entry->getQuantity() * $number_of_builds;
+ $min_order = $this->pricedetailHelper->getMinOrderAmount($entry->getPart());
+ $effective_qty = ($min_order !== null) ? max($total_qty, $min_order) : $total_qty;
+ return $this->pricedetailHelper->calculateAvgPrice($entry->getPart(), $effective_qty, $currency);
+ }
+ return $entry->getPrice();
+ }
}
diff --git a/templates/projects/info/_info.html.twig b/templates/projects/info/_info.html.twig
index b95be253..c3a8e86d 100644
--- a/templates/projects/info/_info.html.twig
+++ b/templates/projects/info/_info.html.twig
@@ -55,6 +55,32 @@
+ {% set n = number_of_builds ?? 1 %}
+ {% set total_build_price = buildHelper.roundedTotalBuildPrice(project, n, app.user.currency ?? null) %}
+ {% set unit_build_price = buildHelper.roundedUnitBuildPrice(project, n, app.user.currency ?? null) %}
+ {% if total_build_price is not null %}
+
+
+
+
+ {% trans %}project.info.total_build_price{% endtrans %}:
+ {{ total_build_price | format_money(app.user.currency ?? null, 2) }}
+ {% if n > 1 and unit_build_price is not null %}
+
+ ({% trans %}project.info.per_unit_price{% endtrans %}: {{ unit_build_price | format_money(app.user.currency ?? null, 2) }})
+
+ {% endif %}
+
+
+
+ {% endif %}
+
{% if project.children is not empty %}
@@ -69,9 +95,9 @@
{% if project.comment is not empty %}
-
-
{% trans %}comment.label{% endtrans %}:
- {{ project.comment|format_markdown }}
-
+
+
{% trans %}comment.label{% endtrans %}:
+ {{ project.comment|format_markdown }}
+
{% endif %}
-
\ No newline at end of file
+
diff --git a/tests/Services/ProjectSystem/ProjectBuildHelperTest.php b/tests/Services/ProjectSystem/ProjectBuildHelperTest.php
index fb31b51e..b80adb2f 100644
--- a/tests/Services/ProjectSystem/ProjectBuildHelperTest.php
+++ b/tests/Services/ProjectSystem/ProjectBuildHelperTest.php
@@ -26,13 +26,15 @@ use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Entity\ProjectSystem\Project;
use App\Entity\ProjectSystem\ProjectBOMEntry;
+use App\Entity\PriceInformations\Orderdetail;
+use App\Entity\PriceInformations\Pricedetail;
use App\Services\ProjectSystem\ProjectBuildHelper;
+use Brick\Math\BigDecimal;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class ProjectBuildHelperTest extends WebTestCase
{
- /** @var ProjectBuildHelper */
- protected $service;
+ protected ProjectBuildHelper $service;
protected function setUp(): void
{
@@ -130,6 +132,180 @@ final class ProjectBuildHelperTest extends WebTestCase
$project->addBomEntry($bom_entry1);
$this->assertSame('∞', $this->service->getMaximumBuildableCountAsString($project));
+ }
+ // --- Build price tests ---
+
+ private function makePartWithPrice(float $pricePerPiece, float $minQty = 1.0): Part
+ {
+ $part = new Part();
+ $orderdetail = new Orderdetail();
+ $pricedetail = (new Pricedetail())
+ ->setMinDiscountQuantity($minQty)
+ ->setPrice(BigDecimal::of((string) $pricePerPiece));
+ $orderdetail->addPricedetail($pricedetail);
+ $part->addOrderdetail($orderdetail);
+ return $part;
+ }
+
+ public function testCalculateTotalBuildPriceEmptyProject(): void
+ {
+ $project = new Project();
+ $this->assertNull($this->service->calculateTotalBuildPrice($project));
+ }
+
+ public function testCalculateTotalBuildPriceNoPricingData(): void
+ {
+ $project = new Project();
+ // Part with no orderdetails — no pricing
+ $entry = (new ProjectBOMEntry())->setPart(new Part())->setQuantity(2);
+ $project->addBomEntry($entry);
+
+ $this->assertNull($this->service->calculateTotalBuildPrice($project));
+ }
+
+ public function testCalculateTotalBuildPriceNonPartEntry(): void
+ {
+ $project = new Project();
+ $entry = new ProjectBOMEntry();
+ $entry->setName('Custom wire');
+ $entry->setQuantity(3);
+ $entry->setPrice(BigDecimal::of('2.00'));
+ $project->addBomEntry($entry);
+
+ // 3 × 2.00 = 6.00 for 1 build
+ $result = $this->service->calculateTotalBuildPrice($project, 1);
+ $this->assertNotNull($result);
+ $this->assertTrue(BigDecimal::of('6.00')->isEqualTo($result));
+ }
+
+ public function testCalculateTotalBuildPriceNonPartEntryMultipleBuilds(): void
+ {
+ $project = new Project();
+ $entry = new ProjectBOMEntry();
+ $entry->setName('Custom wire');
+ $entry->setQuantity(3);
+ $entry->setPrice(BigDecimal::of('2.00'));
+ $project->addBomEntry($entry);
+
+ // 3 × 2.00 × 5 = 30.00 for 5 builds
+ $result = $this->service->calculateTotalBuildPrice($project, 5);
+ $this->assertNotNull($result);
+ $this->assertTrue(BigDecimal::of('30.00')->isEqualTo($result));
+ }
+
+ public function testCalculateTotalBuildPriceWithPart(): void
+ {
+ $project = new Project();
+ $entry = new ProjectBOMEntry();
+ $entry->setPart($this->makePartWithPrice(1.50));
+ $entry->setQuantity(4);
+ $project->addBomEntry($entry);
+
+ // 4 × 1.50 = 6.00 for 1 build
+ $result = $this->service->calculateTotalBuildPrice($project, 1);
+ $this->assertNotNull($result);
+ $this->assertTrue(BigDecimal::of('6.00')->isEqualTo($result));
+ }
+
+ public function testCalculateUnitBuildPriceEqualsTotal(): void
+ {
+ $project = new Project();
+ $entry = new ProjectBOMEntry();
+ $entry->setName('Screw');
+ $entry->setQuantity(10);
+ $entry->setPrice(BigDecimal::of('0.10'));
+ $project->addBomEntry($entry);
+
+ // unit = 10 × 0.10 = 1.00; total for 3 builds = 3.00
+ $unit = $this->service->calculateUnitBuildPrice($project, 3);
+ $total = $this->service->calculateTotalBuildPrice($project, 3);
+ $this->assertNotNull($unit);
+ $this->assertNotNull($total);
+ $this->assertTrue($total->isEqualTo($unit->multipliedBy(3)));
+ }
+
+ public function testRoundedTotalBuildPriceRoundsUp(): void
+ {
+ $project = new Project();
+ $entry = new ProjectBOMEntry();
+ $entry->setName('Tiny part');
+ $entry->setQuantity(1);
+ $entry->setPrice(BigDecimal::of('0.001'));
+ $project->addBomEntry($entry);
+
+ // 0.001 rounded up to 2dp = 0.01
+ $result = $this->service->roundedTotalBuildPrice($project, 1);
+ $this->assertNotNull($result);
+ $this->assertTrue(BigDecimal::of('0.01')->isEqualTo($result));
+ }
+
+ public function testCalculateTotalBuildPriceMixedEntries(): void
+ {
+ $project = new Project();
+
+ // Part entry: 2 × 3.00 = 6.00
+ $partEntry = new ProjectBOMEntry();
+ $partEntry->setPart($this->makePartWithPrice(3.00));
+ $partEntry->setQuantity(2);
+ $project->addBomEntry($partEntry);
+
+ // Non-part entry with price: 5 × 1.00 = 5.00
+ $nonPartEntry = new ProjectBOMEntry();
+ $nonPartEntry->setName('Solder');
+ $nonPartEntry->setQuantity(5);
+ $nonPartEntry->setPrice(BigDecimal::of('1.00'));
+ $project->addBomEntry($nonPartEntry);
+
+ // Total = 11.00
+ $result = $this->service->calculateTotalBuildPrice($project, 1);
+ $this->assertNotNull($result);
+ $this->assertTrue(BigDecimal::of('11.00')->isEqualTo($result));
+ }
+
+ public function testGetEntryUnitPriceReturnsZeroForNoPricingData(): void
+ {
+ $entry = new ProjectBOMEntry();
+ $entry->setPart(new Part()); // part with no orderdetails
+ $entry->setQuantity(5);
+
+ $result = $this->service->getEntryUnitPrice($entry);
+ $this->assertTrue(BigDecimal::zero()->isEqualTo($result));
+ }
+
+ public function testGetEntryUnitPriceNonPartEntry(): void
+ {
+ $entry = new ProjectBOMEntry();
+ $entry->setName('Wire');
+ $entry->setQuantity(2);
+ $entry->setPrice(BigDecimal::of('1.25'));
+
+ $result = $this->service->getEntryUnitPrice($entry);
+ $this->assertTrue(BigDecimal::of('1.25')->isEqualTo($result));
+ }
+
+ public function testGetEntryUnitPriceWithPart(): void
+ {
+ $entry = new ProjectBOMEntry();
+ $entry->setPart($this->makePartWithPrice(2.00));
+ $entry->setQuantity(3);
+
+ $result = $this->service->getEntryUnitPrice($entry);
+ $this->assertTrue(BigDecimal::of('2.00')->isEqualTo($result));
+ }
+
+ public function testCalculateTotalBuildPriceRespectsMinOrderAmount(): void
+ {
+ $project = new Project();
+ // Part has a minimum order quantity of 10 at 0.50/piece
+ $entry = new ProjectBOMEntry();
+ $entry->setPart($this->makePartWithPrice(0.50, 10.0));
+ $entry->setQuantity(1); // BOM only needs 1, but MOQ is 10
+ $project->addBomEntry($entry);
+
+ // Price lookup uses qty=10 (MOQ), returns 0.50. Cost = 1 × 0.50 = 0.50
+ $result = $this->service->calculateTotalBuildPrice($project, 1);
+ $this->assertNotNull($result);
+ $this->assertTrue(BigDecimal::of('0.50')->isEqualTo($result));
}
}
diff --git a/tests/Services/ProjectSystem/ProjectBuildPartHelperTest.php b/tests/Services/ProjectSystem/ProjectBuildPartHelperTest.php
index 894f6315..8126c83d 100644
--- a/tests/Services/ProjectSystem/ProjectBuildPartHelperTest.php
+++ b/tests/Services/ProjectSystem/ProjectBuildPartHelperTest.php
@@ -28,8 +28,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class ProjectBuildPartHelperTest extends WebTestCase
{
- /** @var ProjectBuildPartHelper */
- protected $service;
+ protected ProjectBuildPartHelper $service;
protected function setUp(): void
{
diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf
index 74ca2a26..0f4cf2c8 100644
--- a/translations/messages.cs.xlf
+++ b/translations/messages.cs.xlf
@@ -7241,6 +7241,12 @@ Element 3
Cena
+
+
+ project.bom.ext_price
+ Extended Price
+
+ part.info.withdraw_modal.title.withdraw
diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf
index 9878a09e..85faf9c2 100644
--- a/translations/messages.da.xlf
+++ b/translations/messages.da.xlf
@@ -7184,6 +7184,12 @@ Element 3
Pris
+
+
+ project.bom.ext_price
+ Extended Price
+
+ part.info.withdraw_modal.title.withdraw
diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf
index db595136..680d4b7f 100644
--- a/translations/messages.de.xlf
+++ b/translations/messages.de.xlf
@@ -7235,6 +7235,12 @@ Element 1 -> Element 1.2
Preis
+
+
+ project.bom.ext_price
+ Extended Price
+
+ part.info.withdraw_modal.title.withdraw
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index 176c6650..9e5ed157 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -7212,6 +7212,18 @@ Element 1 -> Element 1.2
Subprojects
+
+
+ project.info.total_build_price
+ Total build price
+
+
+
+
+ project.info.per_unit_price
+ per unit
+
+ project.info.bom_add_parts
@@ -7236,6 +7248,12 @@ Element 1 -> Element 1.2
Price
+
+
+ project.bom.ext_price
+ Extended Price
+
+ part.info.withdraw_modal.title.withdraw
diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf
index 17b2156b..c580a491 100644
--- a/translations/messages.es.xlf
+++ b/translations/messages.es.xlf
@@ -7259,6 +7259,12 @@ Elemento 3
Precio
+
+
+ project.bom.ext_price
+ Extended Price
+
+ part.info.withdraw_modal.title.withdraw
diff --git a/translations/messages.hu.xlf b/translations/messages.hu.xlf
index ba47c2e2..86dd9f6c 100644
--- a/translations/messages.hu.xlf
+++ b/translations/messages.hu.xlf
@@ -7198,6 +7198,12 @@
Ár
+
+
+ project.bom.ext_price
+ Extended Price
+
+ part.info.withdraw_modal.title.withdraw
diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf
index cfaee7a2..70fdbdfa 100644
--- a/translations/messages.it.xlf
+++ b/translations/messages.it.xlf
@@ -7186,6 +7186,12 @@ Element 3
Prezzo
+
+
+ project.bom.ext_price
+ Extended Price
+
+ part.info.withdraw_modal.title.withdraw
diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf
index a4eb1cda..0237d46e 100644
--- a/translations/messages.pl.xlf
+++ b/translations/messages.pl.xlf
@@ -7256,6 +7256,12 @@ Element 3
Cena
+
+
+ project.bom.ext_price
+ Extended Price
+
+ part.info.withdraw_modal.title.withdraw
diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf
index 4fd2aa82..f0d18558 100644
--- a/translations/messages.ru.xlf
+++ b/translations/messages.ru.xlf
@@ -7260,6 +7260,12 @@
Цена
+
+
+ project.bom.ext_price
+ Extended Price
+
+ part.info.withdraw_modal.title.withdraw
diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf
index 9455240c..fde08cfe 100644
--- a/translations/messages.zh.xlf
+++ b/translations/messages.zh.xlf
@@ -7259,6 +7259,12 @@ Element 3
价格
+
+
+ project.bom.ext_price
+ Extended Price
+
+ part.info.withdraw_modal.title.withdraw
From 146e85f84c22f8b8cec3d668be752caedf3c2ed6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Wed, 15 Apr 2026 22:13:54 +0200
Subject: [PATCH 043/108] Update KiCad symbols and footprints lists (#1333)
* Update KiCad symbols and footprints lists
* Update KiCad symbols and footprints lists
* Update KiCad symbols and footprints lists
---------
Co-authored-by: github-actions[bot]
---
public/kicad/footprints.txt | 2 +-
public/kicad/symbols.txt | 22 +++++++++++++++++++---
2 files changed, 20 insertions(+), 4 deletions(-)
diff --git a/public/kicad/footprints.txt b/public/kicad/footprints.txt
index a34b9406..551d7d9c 100644
--- a/public/kicad/footprints.txt
+++ b/public/kicad/footprints.txt
@@ -1,4 +1,4 @@
-# Generated on Mon Mar 9 04:23:25 UTC 2026
+# Generated on Mon Apr 13 05:19:27 UTC 2026
# This file contains all footprints available in the offical KiCAD library
Audio_Module:Reverb_BTDR-1H
Audio_Module:Reverb_BTDR-1V
diff --git a/public/kicad/symbols.txt b/public/kicad/symbols.txt
index 54fd79d2..34e246a5 100644
--- a/public/kicad/symbols.txt
+++ b/public/kicad/symbols.txt
@@ -1,4 +1,4 @@
-# Generated on Mon Mar 9 04:24:12 UTC 2026
+# Generated on Mon Apr 13 05:20:06 UTC 2026
# This file contains all symbols available in the offical KiCAD library
4xxx:14528
4xxx:14529
@@ -899,6 +899,7 @@ Amplifier_Buffer:BUF634AxD
Amplifier_Buffer:BUF634AxDDA
Amplifier_Buffer:BUF634AxDRB
Amplifier_Buffer:BUF634U
+Amplifier_Buffer:BUF802
Amplifier_Buffer:EL2001CN
Amplifier_Buffer:LH0002H
Amplifier_Buffer:LM6321H
@@ -1667,7 +1668,6 @@ Analog_ADC:CA3300
Analog_ADC:HX711
Analog_ADC:ICL7106CPL
Analog_ADC:ICL7107CPL
-Analog_ADC:INA234AxYBJ
Analog_ADC:LTC1406CGN
Analog_ADC:LTC1406IGN
Analog_ADC:LTC1594CS
@@ -2198,6 +2198,7 @@ Audio:WM8731SEDS
Audio:YM2149
Audio:YM2612
Audio:YM3438
+Auxiliary_Items:Generic_Outline
Auxiliary_Items:Jumper_Shunt
Auxiliary_Items:MountingScrew
Battery_Management:ADP5063
@@ -2254,6 +2255,11 @@ Battery_Management:BQ76200PW
Battery_Management:BQ76920PW
Battery_Management:BQ76930DBT
Battery_Management:BQ76940DBT
+Battery_Management:BQ7695201PFBR
+Battery_Management:BQ7695202PFBR
+Battery_Management:BQ7695203PFBR
+Battery_Management:BQ7695204PFBR
+Battery_Management:BQ76952PFBR
Battery_Management:BQ78350DBT
Battery_Management:BQ78350DBT-R1
Battery_Management:CN3063
@@ -2763,6 +2769,8 @@ Connector:DIN41612_02x32_AC
Connector:DIN41612_02x32_AE
Connector:DIN41612_02x32_ZB
Connector:DIN41612_03x32_C_Split
+Connector:DP_Sink
+Connector:DP_Source
Connector:DVI-D_Dual_Link
Connector:DVI-I_Dual_Link
Connector:ExpressCard
@@ -2901,6 +2909,7 @@ Connector:TestPoint_Alt
Connector:TestPoint_Flag
Connector:TestPoint_Probe
Connector:TestPoint_Small
+Connector:TestPoint_Square
Connector:UEXT_Host
Connector:UEXT_Slave
Connector:USB3_A
@@ -7772,6 +7781,7 @@ FPGA_Lattice:ICE40HX1K-TQ144
FPGA_Lattice:ICE40HX4K-BG121
FPGA_Lattice:ICE40HX4K-TQ144
FPGA_Lattice:ICE40HX8K-BG121
+FPGA_Lattice:ICE40LP384-SG32
FPGA_Lattice:ICE40UL1K-SWG16
FPGA_Lattice:ICE40UP5K-SG48ITR
FPGA_Lattice:ICE5LP1K-SG48
@@ -15731,6 +15741,7 @@ Power_Management:RT9742AGJ5F
Power_Management:RT9742ANGJ5F
Power_Management:RT9742BGJ5F
Power_Management:RT9742BNGJ5F
+Power_Management:RT9742SNGV
Power_Management:SN6505ADBV
Power_Management:SN6505BDBV
Power_Management:SN6507DGQ
@@ -18692,6 +18703,7 @@ Regulator_Linear:TPS7A0530PDBZ
Regulator_Linear:TPS7A0531PDBV
Regulator_Linear:TPS7A0533PDBV
Regulator_Linear:TPS7A0533PDBZ
+Regulator_Linear:TPS7A20xxxDBV
Regulator_Linear:TPS7A20xxxDQN
Regulator_Linear:TPS7A3301RGW
Regulator_Linear:TPS7A39
@@ -20301,7 +20313,6 @@ Sensor:BME280
Sensor:BME680
Sensor:CHT11
Sensor:DHT11
-Sensor:INA260
Sensor:LTC2990
Sensor:MAX30102
Sensor:Nuclear-Radiation_Detector
@@ -20588,9 +20599,12 @@ Sensor_Energy:INA219BxD
Sensor_Energy:INA219BxDCN
Sensor_Energy:INA226
Sensor_Energy:INA228
+Sensor_Energy:INA229
Sensor_Energy:INA233
+Sensor_Energy:INA234AxYBJ
Sensor_Energy:INA237
Sensor_Energy:INA238
+Sensor_Energy:INA260
Sensor_Energy:LTC4151xMS
Sensor_Energy:MCP39F521
Sensor_Energy:PAC1931x-xJ6CX
@@ -20872,6 +20886,7 @@ Sensor_Proximity:BPR-105
Sensor_Proximity:BPR-105F
Sensor_Proximity:BPR-205
Sensor_Proximity:CNY70
+Sensor_Proximity:FDC1004DGS
Sensor_Proximity:GP2S700HCP
Sensor_Proximity:ITR1201SR10AR
Sensor_Proximity:ITR8307
@@ -21791,6 +21806,7 @@ Transistor_BJT:Q_NPN_Darlington_ECBC
Transistor_BJT:Q_NPN_EBC
Transistor_BJT:Q_NPN_ECB
Transistor_BJT:Q_NPN_ECBC
+Transistor_BJT:Q_PNP_ACAB
Transistor_BJT:Q_PNP_BCE
Transistor_BJT:Q_PNP_BCEC
Transistor_BJT:Q_PNP_BEC
From 29db029d69282d0eb23144bb5b1741cf1f478c3a Mon Sep 17 00:00:00 2001
From: Wieland Schopohl <55855374+wschopohl@users.noreply.github.com>
Date: Wed, 15 Apr 2026 22:56:34 +0200
Subject: [PATCH 044/108] Add SI-prefix-aware sorting column for parts
tableFeature/si value sort (#1344)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Add SI-prefix-aware sorting column for the parts table
Adds an optional "Name (SI)" column that parses numeric values with SI
prefixes (p, n, u/µ, m, k/K, M, G, T) from part names and sorts by the
resulting physical value. This is useful for electronic components where
alphabetical sorting produces wrong results — e.g. 100nF, 10pF, 1uF
should sort as 10pF < 100nF < 1uF.
Implementation:
- New SiValueSort DQL function with platform-specific SQL generation
for PostgreSQL (POSIX regex), MySQL/MariaDB (REGEXP_SUBSTR), and
SQLite (PHP callback registered via the existing middleware).
- The regex is start-anchored: only names beginning with a number are
matched. Part numbers like "MCP2515" or "Crystal 20MHz" are ignored.
- When SI sort is active, NATSORT is appended as a secondary sort so
that non-matching parts fall back to natural string ordering instead
of appearing in arbitrary order.
- The column is opt-in (not in default columns) and displays the parsed
float value, or an empty cell for non-matching names.
* Rename SI column from "Name (SI)" to "SI Value"
The column now shows the parsed numeric value rather than the part name,
so the label should reflect that.
* Support comma as decimal separator in SI value parsing
Part names using European decimal notation (e.g. "4,7 kΩ", "2,2uF")
were parsed incorrectly because the regex only recognized dots. Now
commas are normalized to dots before parsing, matching the existing
pattern used elsewhere in the codebase (PartNormalizer, price providers).
---
config/packages/doctrine.yaml | 1 +
src/DataTables/PartsDataTable.php | 25 +++
src/Doctrine/Functions/SiValueSort.php | 196 ++++++++++++++++++
.../SQLiteRegexExtensionMiddlewareDriver.php | 4 +
.../BehaviorSettings/PartTableColumns.php | 2 +
tests/Doctrine/Functions/SiValueSortTest.php | 193 +++++++++++++++++
translations/messages.en.xlf | 6 +
7 files changed, 427 insertions(+)
create mode 100644 src/Doctrine/Functions/SiValueSort.php
create mode 100644 tests/Doctrine/Functions/SiValueSortTest.php
diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml
index 5261c295..164ac717 100644
--- a/config/packages/doctrine.yaml
+++ b/config/packages/doctrine.yaml
@@ -56,6 +56,7 @@ doctrine:
natsort: App\Doctrine\Functions\Natsort
array_position: App\Doctrine\Functions\ArrayPosition
ilike: App\Doctrine\Functions\ILike
+ si_value_sort: App\Doctrine\Functions\SiValueSort
when@test:
doctrine:
diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php
index 8bb5f6aa..ca2abd45 100644
--- a/src/DataTables/PartsDataTable.php
+++ b/src/DataTables/PartsDataTable.php
@@ -38,6 +38,7 @@ use App\DataTables\Filters\PartFilter;
use App\DataTables\Filters\PartSearchFilter;
use App\DataTables\Helpers\ColumnSortHelper;
use App\DataTables\Helpers\PartDataTableHelper;
+use App\Doctrine\Functions\SiValueSort;
use App\Doctrine\Helpers\FieldHelper;
use App\Entity\Parts\ManufacturingStatus;
use App\Entity\Parts\Part;
@@ -118,6 +119,17 @@ final class PartsDataTable implements DataTableTypeInterface
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderName($context),
'orderField' => 'NATSORT(part.name)'
])
+ ->add('si_value', TextColumn::class, [
+ 'label' => $this->translator->trans('part.table.si_value'),
+ 'render' => function ($value, Part $context): string {
+ $siValue = SiValueSort::sqliteSiValue($context->getName());
+ if ($siValue !== null) {
+ return htmlspecialchars(sprintf('%g', $siValue));
+ }
+ return '';
+ },
+ 'orderField' => 'SI_VALUE_SORT(part.name)',
+ ])
->add('id', TextColumn::class, [
'label' => $this->translator->trans('part.table.id'),
])
@@ -484,6 +496,19 @@ final class PartsDataTable implements DataTableTypeInterface
//$builder->addGroupBy('_bulkImportJob');
}
+ //When sorting by SI value, add NATSORT as a secondary sort so that parts without
+ //an SI-prefixed value fall back to natural string ordering seamlessly.
+ $orderByParts = $builder->getDQLPart('orderBy');
+ foreach ($orderByParts as $orderBy) {
+ foreach ($orderBy->getParts() as $part) {
+ if (str_contains($part, 'SI_VALUE_SORT')) {
+ $direction = str_contains($part, 'DESC') ? 'DESC' : 'ASC';
+ $builder->addOrderBy('NATSORT(part.name)', $direction);
+ break 2;
+ }
+ }
+ }
+
return $builder;
}
diff --git a/src/Doctrine/Functions/SiValueSort.php b/src/Doctrine/Functions/SiValueSort.php
new file mode 100644
index 00000000..1bba1b9f
--- /dev/null
+++ b/src/Doctrine/Functions/SiValueSort.php
@@ -0,0 +1,196 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\Doctrine\Functions;
+
+use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
+use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
+use Doctrine\DBAL\Platforms\SQLitePlatform;
+use Doctrine\ORM\Query\AST\Functions\FunctionNode;
+use Doctrine\ORM\Query\AST\Node;
+use Doctrine\ORM\Query\Parser;
+use Doctrine\ORM\Query\SqlWalker;
+use Doctrine\ORM\Query\TokenType;
+
+/**
+ * Custom DQL function that extracts the first numeric value with an optional SI prefix
+ * from a string and returns the scaled numeric value for sorting.
+ *
+ * Usage: SI_VALUE_SORT(part.name)
+ *
+ * This enables sorting parts by their physical value. For example, capacitors
+ * named "100nF", "1uF", "10pF" will be sorted by actual value: 10pF < 100nF < 1uF.
+ *
+ * Supported SI prefixes: p (pico, 1e-12), n (nano, 1e-9), u/µ (micro, 1e-6),
+ * m (milli, 1e-3), k/K (kilo, 1e3), M (mega, 1e6), G (giga, 1e9), T (tera, 1e12).
+ *
+ * Only matches numbers at the very beginning of the string (ignoring leading whitespace).
+ * Names like "Crystal 20MHz" will NOT match since the number is not at the start.
+ * Names without a recognizable numeric+prefix pattern return NULL and sort last.
+ */
+class SiValueSort extends FunctionNode
+{
+ private ?Node $field = null;
+
+ /**
+ * SI prefix multipliers. Used by the SQLite PHP callback.
+ */
+ private const SI_MULTIPLIERS = [
+ 'p' => 1e-12,
+ 'n' => 1e-9,
+ 'u' => 1e-6,
+ 'µ' => 1e-6,
+ 'm' => 1e-3,
+ 'k' => 1e3,
+ 'K' => 1e3,
+ 'M' => 1e6,
+ 'G' => 1e9,
+ 'T' => 1e12,
+ ];
+
+ public function parse(Parser $parser): void
+ {
+ $parser->match(TokenType::T_IDENTIFIER);
+ $parser->match(TokenType::T_OPEN_PARENTHESIS);
+
+ $this->field = $parser->ArithmeticExpression();
+
+ $parser->match(TokenType::T_CLOSE_PARENTHESIS);
+ }
+
+ public function getSql(SqlWalker $sqlWalker): string
+ {
+ assert($this->field !== null, 'Field is not set');
+
+ $platform = $sqlWalker->getConnection()->getDatabasePlatform();
+ $rawField = $this->field->dispatch($sqlWalker);
+
+ // Normalize comma decimal separator to dot for SQL platforms (European locale support)
+ $fieldSql = "REPLACE({$rawField}, ',', '.')";
+
+ if ($platform instanceof PostgreSQLPlatform) {
+ return $this->getPostgreSQLSql($fieldSql);
+ }
+
+ if ($platform instanceof AbstractMySQLPlatform) {
+ return $this->getMySQLSql($fieldSql);
+ }
+
+ // SQLite: comma normalization is handled in the PHP callback
+ $fieldSql = $rawField;
+
+ if ($platform instanceof SQLitePlatform) {
+ return "SI_VALUE({$fieldSql})";
+ }
+
+ // Fallback: return NULL (no SI sorting available)
+ return 'NULL';
+ }
+
+ /**
+ * PostgreSQL implementation using substring() with POSIX regex.
+ */
+ private function getPostgreSQLSql(string $field): string
+ {
+ // Extract the numeric part using POSIX regex, anchored at start (with optional leading whitespace)
+ $numericPart = "CAST(substring({$field} FROM '^\\s*(\\d+\\.?\\d*)\\s*[pnuµmkKMGT]?') AS DOUBLE PRECISION)";
+
+ // Extract the SI prefix character
+ $prefixPart = "substring({$field} FROM '^\\s*\\d+\\.?\\d*\\s*([pnuµmkKMGT])')";
+
+ return $this->buildCaseExpression($numericPart, $prefixPart);
+ }
+
+ /**
+ * MySQL/MariaDB implementation using REGEXP_SUBSTR.
+ */
+ private function getMySQLSql(string $field): string
+ {
+ // Extract the numeric part, anchored at start (with optional leading whitespace)
+ $numericPart = "CAST(REGEXP_SUBSTR({$field}, '^[[:space:]]*[0-9]+\\.?[0-9]*') AS DECIMAL(30,15))";
+
+ // Extract the prefix: get the full number+prefix match anchored at start, then take the last char
+ $fullMatch = "REGEXP_SUBSTR({$field}, '^[[:space:]]*[0-9]+\\.?[0-9]*[[:space:]]*[pnuµmkKMGT]')";
+ $prefixPart = "RIGHT({$fullMatch}, 1)";
+
+ return $this->buildCaseExpression($numericPart, $prefixPart);
+ }
+
+ /**
+ * Build a CASE expression that maps an SI prefix character to a multiplier
+ * and multiplies it with the numeric value.
+ *
+ * @param string $numericExpr SQL expression that evaluates to the numeric part
+ * @param string $prefixExpr SQL expression that evaluates to the SI prefix character
+ * @return string SQL CASE expression
+ */
+ private function buildCaseExpression(string $numericExpr, string $prefixExpr): string
+ {
+ return "(CASE" .
+ " WHEN {$numericExpr} IS NULL THEN NULL" .
+ " WHEN {$prefixExpr} = 'p' THEN {$numericExpr} * 1e-12" .
+ " WHEN {$prefixExpr} = 'n' THEN {$numericExpr} * 1e-9" .
+ " WHEN {$prefixExpr} = 'u' THEN {$numericExpr} * 1e-6" .
+ " WHEN {$prefixExpr} = 'µ' THEN {$numericExpr} * 1e-6" .
+ " WHEN {$prefixExpr} = 'm' THEN {$numericExpr} * 1e-3" .
+ " WHEN {$prefixExpr} = 'k' THEN {$numericExpr} * 1e3" .
+ " WHEN {$prefixExpr} = 'K' THEN {$numericExpr} * 1e3" .
+ " WHEN {$prefixExpr} = 'M' THEN {$numericExpr} * 1e6" .
+ " WHEN {$prefixExpr} = 'G' THEN {$numericExpr} * 1e9" .
+ " WHEN {$prefixExpr} = 'T' THEN {$numericExpr} * 1e12" .
+ " ELSE {$numericExpr} * 1" .
+ " END)";
+ }
+
+ /**
+ * PHP callback for SQLite's SI_VALUE function.
+ * Extracts the first numeric value with an optional SI prefix and returns the scaled value.
+ *
+ * @param string|null $value The input string
+ * @return float|null The scaled numeric value, or null if no number found
+ */
+ public static function sqliteSiValue(?string $value): ?float
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ // Normalize comma decimal separator to dot (European locale support)
+ $value = str_replace(',', '.', $value);
+
+ // Match a number at the very start (allowing leading whitespace), optionally followed by an SI prefix
+ if (!preg_match('/^\s*(\d+\.?\d*)\s*([pnuµmkKMGT])?/u', $value, $matches)) {
+ return null;
+ }
+
+ $number = (float) $matches[1];
+ $prefix = $matches[2] ?? '';
+
+ if ($prefix === '') {
+ return $number;
+ }
+
+ $multiplier = self::SI_MULTIPLIERS[$prefix] ?? 1.0;
+
+ return $number * $multiplier;
+ }
+}
diff --git a/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php
index ad572d4c..aa6108c9 100644
--- a/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php
+++ b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Doctrine\Middleware;
+use App\Doctrine\Functions\SiValueSort;
use App\Exceptions\InvalidRegexException;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
@@ -51,6 +52,9 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
//Create a new collation for natural sorting
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
+
+ //Create a function for SI prefix value sorting
+ $native_connection->sqliteCreateFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, \PDO::SQLITE_DETERMINISTIC);
}
}
diff --git a/src/Settings/BehaviorSettings/PartTableColumns.php b/src/Settings/BehaviorSettings/PartTableColumns.php
index 3b30e0a4..32f6100b 100644
--- a/src/Settings/BehaviorSettings/PartTableColumns.php
+++ b/src/Settings/BehaviorSettings/PartTableColumns.php
@@ -52,6 +52,8 @@ enum PartTableColumns : string implements TranslatableInterface
case TAGS = "tags";
case ATTACHMENTS = "attachments";
+ case SI_VALUE = "si_value";
+
case EDA_REFERENCE = "eda_reference";
case EDA_VALUE = "eda_value";
diff --git a/tests/Doctrine/Functions/SiValueSortTest.php b/tests/Doctrine/Functions/SiValueSortTest.php
new file mode 100644
index 00000000..dbdd9d28
--- /dev/null
+++ b/tests/Doctrine/Functions/SiValueSortTest.php
@@ -0,0 +1,193 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\Tests\Doctrine\Functions;
+
+use App\Doctrine\Functions\SiValueSort;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
+use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
+use Doctrine\DBAL\Platforms\SQLitePlatform;
+
+final class SiValueSortTest extends AbstractDoctrineFunctionTestCase
+{
+ public function testPostgreSQLGeneratesCaseExpression(): void
+ {
+ $function = new SiValueSort('SI_VALUE_SORT');
+ $this->setObjectProperty($function, 'field', $this->createNode('part_name'));
+
+ $sql = $function->getSql($this->createSqlWalker(new PostgreSQLPlatform()));
+
+ $this->assertStringContainsString('CASE', $sql);
+ $this->assertStringContainsString("REPLACE(part_name, ',', '.')", $sql);
+ $this->assertStringContainsString('1e-12', $sql);
+ $this->assertStringContainsString('1e-9', $sql);
+ $this->assertStringContainsString('1e-6', $sql);
+ $this->assertStringContainsString('1e-3', $sql);
+ $this->assertStringContainsString('1e3', $sql);
+ $this->assertStringContainsString('1e6', $sql);
+ $this->assertStringContainsString('1e9', $sql);
+ $this->assertStringContainsString('1e12', $sql);
+ }
+
+ public function testMySQLGeneratesCaseExpression(): void
+ {
+ $function = new SiValueSort('SI_VALUE_SORT');
+ $this->setObjectProperty($function, 'field', $this->createNode('part_name'));
+
+ $sql = $function->getSql($this->createSqlWalker(new MySQLPlatform()));
+
+ $this->assertStringContainsString('CASE', $sql);
+ $this->assertStringContainsString("REPLACE(part_name, ',', '.')", $sql);
+ $this->assertStringContainsString('1e-12', $sql);
+ $this->assertStringContainsString('1e6', $sql);
+ }
+
+ public function testSQLiteUsesSiValueFunction(): void
+ {
+ $function = new SiValueSort('SI_VALUE_SORT');
+ $this->setObjectProperty($function, 'field', $this->createNode('part_name'));
+
+ $sql = $function->getSql($this->createSqlWalker(new SQLitePlatform()));
+
+ $this->assertSame('SI_VALUE(part_name)', $sql);
+ }
+
+ /**
+ * @dataProvider sqliteSiValueProvider
+ */
+ public function testSqliteSiValue(?string $input, ?float $expected): void
+ {
+ $result = SiValueSort::sqliteSiValue($input);
+
+ if ($expected === null) {
+ $this->assertNull($result);
+ } else {
+ $this->assertEqualsWithDelta($expected, $result, $expected * 1e-9);
+ }
+ }
+
+ /**
+ * @return iterable
+ */
+ public static function sqliteSiValueProvider(): iterable
+ {
+ // Basic SI prefix values
+ yield 'pico' => ['10pF', 10e-12];
+ yield 'nano' => ['100nF', 100e-9];
+ yield 'micro_u' => ['1uF', 1e-6];
+ yield 'micro_µ' => ['1µF', 1e-6];
+ yield 'milli' => ['4.7mH', 4.7e-3];
+ yield 'kilo_lower' => ['4.7k', 4.7e3];
+ yield 'kilo_upper' => ['4.7K', 4.7e3];
+ yield 'mega' => ['1M', 1e6];
+ yield 'giga' => ['2.2G', 2.2e9];
+ yield 'tera' => ['1T', 1e12];
+
+ // No prefix (plain number)
+ yield 'plain_integer' => ['100', 100.0];
+ yield 'plain_decimal' => ['4.7', 4.7];
+
+ // Decimal values with prefix (dot separator)
+ yield 'decimal_nano' => ['4.7nF', 4.7e-9];
+ yield 'decimal_micro' => ['0.1uF', 0.1e-6];
+ yield 'decimal_kilo' => ['2.2k', 2.2e3];
+
+ // Comma decimal separator (European locale)
+ yield 'comma_kilo' => ['4,7k', 4.7e3];
+ yield 'comma_micro' => ['2,2uF', 2.2e-6];
+ yield 'comma_kilo_space' => ['1,2 kΩ', 1.2e3];
+
+ // Number NOT at the start — should return NULL
+ yield 'prefixed_name' => ['CAP-100nF', null];
+ yield 'name_with_number' => ['R 4.7k 1%', null];
+ yield 'crystal' => ['Crystal 20MHz', null];
+
+ // Number at start with trailing text
+ yield 'number_with_suffix' => ['10nF 25V', 10e-9];
+
+ // Space between number and prefix
+ yield 'space_before_prefix' => ['100 nF', 100e-9];
+
+ // Leading whitespace before number
+ yield 'leading_whitespace' => [' 10uF', 10e-6];
+
+ // No number at all
+ yield 'no_number' => ['Connector', null];
+ yield 'text_only' => ['LED red', null];
+
+ // Null input
+ yield 'null' => [null, null];
+
+ // Empty string
+ yield 'empty' => ['', null];
+ }
+
+ /**
+ * Test that the sort order is correct by comparing sqliteSiValue results.
+ */
+ public function testSortOrder(): void
+ {
+ $parts = ['1uF', '100nF', '10pF', '10uF', '0.1mF', '1F', '10kF', '1MF'];
+ $expected = ['10pF', '100nF', '1uF', '10uF', '0.1mF', '1F', '10kF', '1MF'];
+
+ // Sort using sqliteSiValue
+ usort($parts, static function (string $a, string $b): int {
+ $va = SiValueSort::sqliteSiValue($a);
+ $vb = SiValueSort::sqliteSiValue($b);
+ return $va <=> $vb;
+ });
+
+ $this->assertSame($expected, $parts);
+ }
+
+ /**
+ * Test that NULL values sort last (after all numeric values).
+ */
+ public function testNullSortsLast(): void
+ {
+ $parts = ['Connector', '100nF', 'LED red', '10pF'];
+
+ usort($parts, static function (string $a, string $b): int {
+ $va = SiValueSort::sqliteSiValue($a);
+ $vb = SiValueSort::sqliteSiValue($b);
+
+ // NULL sorts last
+ if ($va === null && $vb === null) {
+ return 0;
+ }
+ if ($va === null) {
+ return 1;
+ }
+ if ($vb === null) {
+ return -1;
+ }
+
+ return $va <=> $vb;
+ });
+
+ $this->assertSame('10pF', $parts[0]);
+ $this->assertSame('100nF', $parts[1]);
+ // Last two should be the non-numeric names
+ $this->assertContains('Connector', array_slice($parts, 2));
+ $this->assertContains('LED red', array_slice($parts, 2));
+ }
+}
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index 9e5ed157..4da88512 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -2780,6 +2780,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can
Name
+
+
+ part.table.si_value
+ SI Value
+
+ part.table.id
From 766665f9e5bb712eaa0ef194dafaf25bd0c5bd85 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Wed, 15 Apr 2026 22:48:47 +0200
Subject: [PATCH 045/108] Use big E for si value formatting output
---
src/DataTables/PartsDataTable.php | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php
index ca2abd45..b34eef9d 100644
--- a/src/DataTables/PartsDataTable.php
+++ b/src/DataTables/PartsDataTable.php
@@ -124,7 +124,8 @@ final class PartsDataTable implements DataTableTypeInterface
'render' => function ($value, Part $context): string {
$siValue = SiValueSort::sqliteSiValue($context->getName());
if ($siValue !== null) {
- return htmlspecialchars(sprintf('%g', $siValue));
+ //Output it as scientific number with a big E
+ return htmlspecialchars(sprintf('%G', $siValue));
}
return '';
},
From 1c3dfa26bb807e2a28b4a84c511cc4afa90d1f45 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Wed, 15 Apr 2026 23:06:40 +0200
Subject: [PATCH 046/108] Updated dependencies
---
composer.lock | 68 +++++++++++++++++++++--------------------
yarn.lock | 85 ++++++++++++++++++++++++++-------------------------
2 files changed, 78 insertions(+), 75 deletions(-)
diff --git a/composer.lock b/composer.lock
index 8e509fbf..217a3e49 100644
--- a/composer.lock
+++ b/composer.lock
@@ -13083,7 +13083,7 @@
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.34.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@@ -13142,7 +13142,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0"
},
"funding": [
{
@@ -13166,7 +13166,7 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.34.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
@@ -13224,7 +13224,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.34.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0"
},
"funding": [
{
@@ -13248,7 +13248,7 @@
},
{
"name": "symfony/polyfill-intl-icu",
- "version": "v1.34.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-icu.git",
@@ -13312,7 +13312,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.34.0"
+ "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.36.0"
},
"funding": [
{
@@ -13336,7 +13336,7 @@
},
{
"name": "symfony/polyfill-intl-idn",
- "version": "v1.34.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
@@ -13399,7 +13399,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.34.0"
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.36.0"
},
"funding": [
{
@@ -13423,7 +13423,7 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.34.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -13484,7 +13484,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0"
},
"funding": [
{
@@ -13508,7 +13508,7 @@
},
{
"name": "symfony/polyfill-php83",
- "version": "v1.34.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
@@ -13564,7 +13564,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php83/tree/v1.34.0"
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.36.0"
},
"funding": [
{
@@ -13588,7 +13588,7 @@
},
{
"name": "symfony/polyfill-php84",
- "version": "v1.34.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php84.git",
@@ -13644,7 +13644,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php84/tree/v1.34.0"
+ "source": "https://github.com/symfony/polyfill-php84/tree/v1.36.0"
},
"funding": [
{
@@ -13668,7 +13668,7 @@
},
{
"name": "symfony/polyfill-php85",
- "version": "v1.34.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php85.git",
@@ -13724,7 +13724,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php85/tree/v1.34.0"
+ "source": "https://github.com/symfony/polyfill-php85/tree/v1.36.0"
},
"funding": [
{
@@ -13748,7 +13748,7 @@
},
{
"name": "symfony/polyfill-uuid",
- "version": "v1.34.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-uuid.git",
@@ -13807,7 +13807,7 @@
"uuid"
],
"support": {
- "source": "https://github.com/symfony/polyfill-uuid/tree/v1.34.0"
+ "source": "https://github.com/symfony/polyfill-uuid/tree/v1.36.0"
},
"funding": [
{
@@ -18469,11 +18469,11 @@
},
{
"name": "phpstan/phpstan",
- "version": "2.1.47",
+ "version": "2.1.48",
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/79015445d8bd79e62b29140f12e5bfced1dcca65",
- "reference": "79015445d8bd79e62b29140f12e5bfced1dcca65",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/231397213efb7c0a066ee024b5c3c87f2d3adfa0",
+ "reference": "231397213efb7c0a066ee024b5c3c87f2d3adfa0",
"shasum": ""
},
"require": {
@@ -18518,7 +18518,7 @@
"type": "github"
}
],
- "time": "2026-04-13T15:49:08+00:00"
+ "time": "2026-04-15T20:24:19+00:00"
},
{
"name": "phpstan/phpstan-doctrine",
@@ -19244,12 +19244,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
- "reference": "b0b156ed9d5d2eb313c33f92af3dbc886ba4688a"
+ "reference": "bb550b5adb0d4d74c4f6857c6b3b3638c022e90b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/b0b156ed9d5d2eb313c33f92af3dbc886ba4688a",
- "reference": "b0b156ed9d5d2eb313c33f92af3dbc886ba4688a",
+ "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/bb550b5adb0d4d74c4f6857c6b3b3638c022e90b",
+ "reference": "bb550b5adb0d4d74c4f6857c6b3b3638c022e90b",
"shasum": ""
},
"conflict": {
@@ -19379,7 +19379,7 @@
"codingms/modules": "<4.3.11|>=5,<5.7.4|>=6,<6.4.2|>=7,<7.5.5",
"commerceteam/commerce": ">=0.9.6,<0.9.9",
"components/jquery": ">=1.0.3,<3.5",
- "composer/composer": "<1.10.27|>=2,<2.2.26|>=2.3,<2.9.3",
+ "composer/composer": "<2.2.27|>=2.3,<2.9.6",
"concrete5/concrete5": "<9.4.8",
"concrete5/core": "<8.5.8|>=9,<9.1",
"contao-components/mediaelement": ">=2.14.2,<2.21.1",
@@ -19396,7 +19396,7 @@
"cpsit/typo3-mailqueue": "<0.4.5|>=0.5,<0.5.2",
"craftcms/aws-s3": ">=2.0.2,<=2.2.4",
"craftcms/azure-blob": ">=2.0.0.0-beta1,<=2.1",
- "craftcms/cms": "<=4.17.7|>=5,<=5.9.13",
+ "craftcms/cms": "<=4.17.8|>=5,<5.9.15",
"craftcms/commerce": ">=4,<4.11|>=5,<5.6",
"craftcms/composer": ">=4.0.0.0-RC1-dev,<=4.10|>=5.0.0.0-RC1-dev,<=5.5.1",
"craftcms/craft": ">=3.5,<=4.16.17|>=5.0.0.0-RC1-dev,<=5.8.21",
@@ -19664,7 +19664,7 @@
"kelvinmo/simplexrd": "<3.1.1",
"kevinpapst/kimai2": "<1.16.7",
"khodakhah/nodcms": "<=3",
- "kimai/kimai": "<=2.50",
+ "kimai/kimai": "<2.53",
"kitodo/presentation": "<3.2.3|>=3.3,<3.3.4",
"klaviyo/magento2-extension": ">=1,<3",
"knplabs/knp-snappy": "<=1.4.2",
@@ -19805,8 +19805,8 @@
"october/backend": "<1.1.2",
"october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1",
"october/october": "<3.7.5",
- "october/rain": "<1.0.472|>=1.1,<1.1.2",
- "october/system": "<=3.7.12|>=4,<=4.0.11",
+ "october/rain": "<=3.7.13|>=4,<=4.1.9",
+ "october/system": "<=3.7.13|>=4,<=4.1.9",
"oliverklee/phpunit": "<3.5.15",
"omeka/omeka-s": "<4.0.3",
"onelogin/php-saml": "<2.21.1|>=3,<3.8.1|>=4,<4.3.1",
@@ -19885,7 +19885,7 @@
"pixelfed/pixelfed": "<0.12.5",
"plotly/plotly.js": "<2.25.2",
"pocketmine/bedrock-protocol": "<8.0.2",
- "pocketmine/pocketmine-mp": "<5.41.1",
+ "pocketmine/pocketmine-mp": "<5.42.1",
"pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1",
"pressbooks/pressbooks": "<5.18",
"prestashop/autoupgrade": ">=4,<4.10.1",
@@ -19939,6 +19939,7 @@
"rudloff/rtmpdump-bin": "<=2.3.1",
"s-cart/core": "<=9.0.5",
"s-cart/s-cart": "<6.9",
+ "s9y/serendipity": "<2.6",
"sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1",
"sabre/dav": ">=1.6,<1.7.11|>=1.8,<1.8.9",
"saloonphp/saloon": "<4",
@@ -20168,6 +20169,7 @@
"webcoast/deferred-image-processing": "<1.0.2",
"webklex/laravel-imap": "<5.3",
"webklex/php-imap": "<5.3",
+ "webonyx/graphql-php": "<=15.31.4",
"webpa/webpa": "<3.1.2",
"webreinvent/vaahcms": "<=2.3.1",
"wikibase/wikibase": "<=1.39.3",
@@ -20187,7 +20189,7 @@
"wpcloud/wp-stateless": "<3.2",
"wpglobus/wpglobus": "<=1.9.6",
"wpmetabox/meta-box": "<5.11.2",
- "wwbn/avideo": "<=26",
+ "wwbn/avideo": "<=29",
"xataface/xataface": "<3",
"xpressengine/xpressengine": "<3.0.15",
"yab/quarx": "<2.4.5",
@@ -20287,7 +20289,7 @@
"type": "tidelift"
}
],
- "time": "2026-04-13T18:30:45+00:00"
+ "time": "2026-04-15T20:21:07+00:00"
},
{
"name": "sebastian/cli-parser",
diff --git a/yarn.lock b/yarn.lock
index 158ab745..f17966b2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2174,9 +2174,9 @@
integrity sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==
"@simple-git/argv-parser@^1.1.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@simple-git/argv-parser/-/argv-parser-1.1.0.tgz#6680aed3fa68f131ca0d7efa90e52b5b23ca3183"
- integrity sha512-sUKOu2lb5vGIWADNNLpscyj07DAeQZU3KLbnE2Tj53tW6BbDQKMly2CCfnR4oYzqtRELCPWfwaPg+Q0T8qfKBg==
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz#275b839c6eeb5030872c73b1ea839a416885da9d"
+ integrity sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==
dependencies:
"@simple-git/args-pathspec" "^1.0.3"
@@ -2732,9 +2732,9 @@ base64-js@^1.1.2, base64-js@^1.3.0:
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.10.12:
- version "2.10.18"
- resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz#565745085ba7743af7d4072707ad132db3a5a42f"
- integrity sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==
+ version "2.10.19"
+ resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz#7697721c22f94f66195d0c34299b1a91e3299493"
+ integrity sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==
big.js@^5.2.2:
version "5.2.2"
@@ -2799,6 +2799,13 @@ browser-stdout@^1.3.1:
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
+browserify-zlib@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+ integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==
+ dependencies:
+ pako "~1.0.5"
+
browserslist@^4.0.0, browserslist@^4.24.0, browserslist@^4.28.1, browserslist@^4.28.2:
version "4.28.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.2.tgz#f50b65362ef48974ca9f50b3680566d786b811d2"
@@ -2862,9 +2869,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001782:
- version "1.0.30001787"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz#fd25c5e42e2d35df5c75eddda00d15d9c0c68f81"
- integrity sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==
+ version "1.0.30001788"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz#31e97d1bfec332b3f2d7eea7781460c97629b3bf"
+ integrity sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==
ccount@^2.0.0:
version "2.0.1"
@@ -3488,9 +3495,9 @@ domhandler@^5.0.2, domhandler@^5.0.3:
domelementtype "^2.3.0"
dompurify@^3.0.3:
- version "3.3.3"
- resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.3.tgz#680cae8af3e61320ddf3666a3bc843f7b291b2b6"
- integrity sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.0.tgz#b1fc33ebdadb373241621e0a30e4ad81573dfd0b"
+ integrity sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==
optionalDependencies:
"@types/trusted-types" "^2.0.7"
@@ -3518,9 +3525,9 @@ eastasianwidth@^0.2.0:
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
electron-to-chromium@^1.5.328:
- version "1.5.335"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz#0b957cea44ef86795c227c616d16b4803d119daa"
- integrity sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==
+ version "1.5.337"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.337.tgz#73051b9160d3960eea398d73323184cbdd6914de"
+ integrity sha512-15gKW9mRUNP9RdzhedJNypFUxtYWSXohFz2nTLzM272xbRXHws68kNDzyATG3qej+vUj/7Sn9hf5XTDh0XK6/w==
emoji-regex@^8.0.0:
version "8.0.0"
@@ -3854,9 +3861,9 @@ get-stream@^6.0.0:
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
get-tsconfig@^4.10.1:
- version "4.13.7"
- resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.13.7.tgz#b9d8b199b06033ceeea1a93df7ea5765415089bc"
- integrity sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==
+ version "4.14.0"
+ resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz#985d85c52a9903864280ccc2448d413fbf1efed8"
+ integrity sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==
dependencies:
resolve-pkg-maps "^1.0.0"
@@ -4406,7 +4413,7 @@ json-formatter-js@^2.3.4:
resolved "https://registry.yarnpkg.com/json-formatter-js/-/json-formatter-js-2.5.23.tgz#b7dd0a1da7e6cbea8e76743d7d8dc1238866cc73"
integrity sha512-Cbm8wHXjo/C56aCePP1VuKvjxoMEmL7g7Ckss1oWFFlCsvOEEbye1kTeaNNaqba1Cl6YpIOYAnK65pUQ8mDIUQ==
-json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1:
+json-parse-even-better-errors@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
@@ -5062,17 +5069,10 @@ micromatch@^4.0.0, micromatch@^4.0.8:
braces "^3.0.3"
picomatch "^2.3.1"
-mime-db@1.52.0:
- version "1.52.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
- integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
-
-mime-types@^2.1.27:
- version "2.1.35"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
- integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
- dependencies:
- mime-db "1.52.0"
+mime-db@^1.54.0:
+ version "1.54.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
+ integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
mimic-fn@^2.1.0:
version "2.1.0"
@@ -5421,7 +5421,7 @@ pako@^0.2.5:
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==
-pako@~1.0.2:
+pako@~1.0.2, pako@~1.0.5:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
@@ -5528,9 +5528,11 @@ plural-forms@^0.5.5:
integrity sha512-rJw4xp22izsfJOVqta5Hyvep2lR3xPkFUtj7dyQtpf/FbxUiX7PQCajTn2EHDRylizH5N/Uqqodfdu22I0ju+g==
png-js@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d"
- integrity sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.1.0.tgz#60a135216601f807b88a6d61ac93bd42a32c5ee1"
+ integrity sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==
+ dependencies:
+ browserify-zlib "^0.2.0"
pofile@^1.1.4:
version "1.1.4"
@@ -5832,9 +5834,9 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.0.0, postcss@^8.2.14, postcss@^8.4.12, postcss@^8.4.40:
- version "8.5.9"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.9.tgz#f6ee9e0b94f0f19c97d2f172bfbd7fc71fe1cca4"
- integrity sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==
+ version "8.5.10"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.10.tgz#8992d8c30acf3f12169e7c09514a12fed7e48356"
+ integrity sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==
dependencies:
nanoid "^3.3.11"
picocolors "^1.1.1"
@@ -6938,9 +6940,9 @@ webpack-sources@^3.0.0, webpack-sources@^3.3.4:
integrity sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==
webpack@^5.74.0:
- version "5.106.1"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.106.1.tgz#0a3eeb43a50e4f67fbecd206e1e6fc2c89fc2b6f"
- integrity sha512-EW8af29ak8Oaf4T8k8YsajjrDBDYgnKZ5er6ljWFJsXABfTNowQfvHLftwcepVgdz+IoLSdEAbBiM9DFXoll9w==
+ version "5.106.2"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.106.2.tgz#ca8174b4fd80f055cc5a45fcc5577d6db76c8ac5"
+ integrity sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"
@@ -6958,9 +6960,8 @@ webpack@^5.74.0:
events "^3.2.0"
glob-to-regexp "^0.4.1"
graceful-fs "^4.2.11"
- json-parse-even-better-errors "^2.3.1"
loader-runner "^4.3.1"
- mime-types "^2.1.27"
+ mime-db "^1.54.0"
neo-async "^2.6.2"
schema-utils "^4.3.3"
tapable "^2.3.0"
From ec05f9d8ab1a7951e80cfffdcf08f18fc3c6732b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Wed, 15 Apr 2026 23:27:10 +0200
Subject: [PATCH 047/108] Fixed phpstan issues
---
phpstan.dist.neon | 6 ++++++
src/Doctrine/Functions/SiValueSort.php | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/phpstan.dist.neon b/phpstan.dist.neon
index fe51518d..c7da636f 100644
--- a/phpstan.dist.neon
+++ b/phpstan.dist.neon
@@ -59,6 +59,9 @@ parameters:
- '#expects .*PartParameter, .*AbstractParameter given.#'
- '#Part::getParameters\(\) should return .*AbstractParameter#'
+ # Fix some weird issue with how covariance with collections is solved
+ - '#Method App\\Entity\\Base\\AbstractStructuralDBElement::getParameters\(\) should return Doctrine\\Common\\Collections\\Collection but returns#'
+
# Ignore doctrine type mapping mismatch
- '#Property .* type mapping mismatch: property can contain .* but database expects .*#'
@@ -70,3 +73,6 @@ parameters:
- message: '#Access to an undefined property Brick\\Schema\\Interfaces\\#'
path: src/Services/InfoProviderSystem/Providers/GenericWebProvider.php
+
+ -
+ identifier: nullCoalesce.property
diff --git a/src/Doctrine/Functions/SiValueSort.php b/src/Doctrine/Functions/SiValueSort.php
index 1bba1b9f..c4d16444 100644
--- a/src/Doctrine/Functions/SiValueSort.php
+++ b/src/Doctrine/Functions/SiValueSort.php
@@ -189,7 +189,7 @@ class SiValueSort extends FunctionNode
return $number;
}
- $multiplier = self::SI_MULTIPLIERS[$prefix] ?? 1.0;
+ $multiplier = self::SI_MULTIPLIERS[$prefix] ?? 1.0; //@phpstan-ignore-line - fallback to 1.0 if prefix is not recognized (should not happen due to regex)
return $number * $multiplier;
}
From 6a30b41688448e9866d60d85fd7db817d712cfea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Wed, 15 Apr 2026 23:27:30 +0200
Subject: [PATCH 048/108] Bumped version to 2.10.0
---
VERSION | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/VERSION b/VERSION
index dedcc7d4..10c2c0c3 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.9.1
+2.10.0
From a82d515034e85e7577c7bef1bc715759881d34bb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Wed, 15 Apr 2026 23:38:40 +0200
Subject: [PATCH 049/108] New Crowdin updates (#1325)
* New translations messages.en.xlf (Danish)
* New translations validators.en.xlf (Danish)
* New translations messages.en.xlf (German)
* New translations messages.en.xlf (English)
* New translations messages.en.xlf (French)
* New translations validators.en.xlf (French)
* New translations messages.en.xlf (English)
* New translations messages.en.xlf (English)
* New translations messages.en.xlf (German)
---
translations/messages.da.xlf | 556 +-
translations/messages.de.xlf | 106 +-
translations/messages.fr.xlf | 8697 +++++++++++++++++++++++++++-----
translations/validators.da.xlf | 18 +-
translations/validators.fr.xlf | 20 +-
5 files changed, 8243 insertions(+), 1154 deletions(-)
diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf
index 85faf9c2..a435396c 100644
--- a/translations/messages.da.xlf
+++ b/translations/messages.da.xlf
@@ -642,6 +642,12 @@ Underelementer vil blive flyttet opad.
Gruppe
+
+
+ specifications.eda_visibility.help
+ Eksporter denne parameter som et EDA felt
+
+ specification.create
@@ -2923,6 +2929,42 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt
Bilag
+
+
+ part.table.eda_status
+ EDA
+
+
+
+
+ eda.status.symbol_set
+ KiCad symbolsæt
+
+
+
+
+ eda.status.footprint_set
+ KiCad footprintsæt
+
+
+
+
+ eda.status.reference_set
+ eda. status.reference_set
+
+
+
+
+ eda.status.complete
+ EDA felter udfyldt (symbol, footprint, reference)
+
+
+
+
+ eda.status.partial
+ EDA felter delvist udfyldt
+
+ flash.login_successful
@@ -3265,6 +3307,12 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt
Ikke længere tilgængelig
+
+
+ orderdetails.edit.eda_visibility
+ Synlige i EDA
+
+ orderdetails.edit.supplierpartnr.placeholder
@@ -9508,6 +9556,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
EIGP 114 stregkode (f.eks. Datamatrix-kode fra Digikey og Mouser dele)
+
+
+ scan_dialog.mode.lcsc
+ LCSC.com barcode
+
+ scan_dialog.info_mode
@@ -9520,6 +9574,24 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
Afkodet information
+
+
+ label_scanner.target_found
+ Genstand fundet i database
+
+
+
+
+ label_scanner.scan_result.title
+ Scan-resultat
+
+
+
+
+ label_scanner.no_locations
+ Part er ikke gemt på nogen lokation.
+
+ label_generator.edit_profiles
@@ -9954,6 +10026,18 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
Denne værdi bestemmer dybden af kategoritræet, der er synligt i KiCad. 0 betyder, at kun kategorierne på øverste niveau er synlige. Indstil værdien til > 0 for at vise yderligere niveauer. Indstil værdien til -1 for at vise alle dele af deldatabasen inden for en enkelt kategori i KiCad.
+
+
+ settings.misc.kicad_eda.datasheet_link
+ Databladsfelt linker til PDF
+
+
+
+
+ settings.misc.kicad_eda.datasheet_link.help
+ Når det er aktiveret, vil dataarkfeltet i KiCad linke til den faktiske PDF-fil (hvis den findes). Når det er deaktiveret, vil det i stedet linke til Part-DB-siden. Linket til Part-DB-siden er altid tilgængeligt som et separat felt "Part-DB URL".
+
+ settings.behavior.sidebar
@@ -10296,6 +10380,24 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
Vis billedoverlejringen med detaljer om vedhæftet fil, når du holder musen over billedgalleriet med dele.
+
+
+ settings.behavior.keybindings
+ Tastaturgenveje
+
+
+
+
+ settings.behavior.keybindings.enable_special_characters
+ Aktivér tastaturgenveje for specialtegn
+
+
+
+
+ settings.behavior.keybindings.enable_special_characters.help
+ Aktivér genvejstasten Alt+ for at indsætte specialtegn (græske bogstaver, matematiske symboler osv.) i tekstfelter. Deaktiver dette, hvis genvejene er i konflikt med dit tastaturlayout eller systemgenveje.
+
+ perm.config.change_system_settings
@@ -10920,6 +11022,84 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
Masseimport af datakilder
+
+
+ part_list.action.group.eda
+ EDA / KiCad
+
+
+
+
+ part_list.action.batch_edit_eda
+ Batchredigering af EDA-felter
+
+
+
+
+ batch_eda.title
+ Batchredigering af EDA-felter
+
+
+
+
+ batch_eda.description
+ Rediger EDA/KiCad-felter for %count% valgte dele. Markér feltet "Anvend" ud for hvert felt, du vil ændre.
+
+
+
+
+ batch_eda.show_parts
+ Vis valgte dele
+
+
+
+
+ batch_eda.apply_hint
+ Kun felter, hvor afkrydsningsfeltet "Anvend" er markeret, ændres. Felter, der ikke er markeret, ændres ikke.
+
+
+
+
+ batch_eda.apply
+ Anvend
+
+
+
+
+ batch_eda.field
+ Felt
+
+
+
+
+ batch_eda.value
+ Værdi
+
+
+
+
+ batch_eda.submit
+ Anvend på udvalgte dele
+
+
+
+
+ batch_eda.cancel
+ Annullér
+
+
+
+
+ batch_eda.success
+ EDA felter er nu opdateret
+
+
+
+
+ batch_eda.no_parts_selected
+ Ingen dele blev valgt til batchredigering.
+
+ info_providers.bulk_import.step1.spn_recommendation
@@ -12233,7 +12413,7 @@ Buerklin API-godkendelsesserver: 10 anmodninger/minut pr. IP-adresse
update_manager.progress.downgrade_title
- Downgrade fremskridt
+ Downgrade fremskridtPart-DB er blevet nedgraderet! Du skal muligvis opdatere siden for at se den nye version.
@@ -12320,6 +12500,102 @@ Buerklin API-godkendelsesserver: 10 anmodninger/minut pr. IP-adresse
Gendannelse af sikkerhedskopi er deaktiveret af serverkonfigurationen.
+
+
+ update_manager.backup.create
+ Opret sikkerhedskopi
+
+
+
+
+ update_manager.backup.create.confirm
+ Vil du lave en fuld sikkerhedskopi nu? Det kan tage et stykke tid.
+
+
+
+
+ update_manager.backup.created
+ Sikkerhedskopi er oprettet.
+
+
+
+
+ update_manager.backup.delete.confirm
+ Er du sikker på at du vil slette denne backup?
+
+
+
+
+ update_manager.backup.deleted
+ Sikkerhedskopi er slettet.
+
+
+
+
+ update_manager.backup.delete_error
+ Sikkerhedskopi kunne ikke udføres.
+
+
+
+
+ update_manager.log.delete.confirm
+ Er du sikker på at du vil slette denne log?
+
+
+
+
+ update_manager.log.deleted
+ Log slettet.
+
+
+
+
+ update_manager.log.delete_error
+ Kunne ikke slette loggen.
+
+
+
+
+ update_manager.view_log
+ Vis log.
+
+
+
+
+ update_manager.delete
+ Slet
+
+
+
+
+ update_manager.backup.download
+ Download sikkerhedskopi
+
+
+
+
+ update_manager.backup.download.password_label
+ Bekræft password for at downloade
+
+
+
+
+ update_manager.backup.download.security_warning
+ Sikkerhedskopier indeholder følsomme data, herunder password-hashes og hemmeligheder. Bekræft venligst dit password for at fortsætte med download.
+
+
+
+
+ update_manager.backup.download.invalid_password
+ Ugyldigt password. Download af sikkerhedskopi er afvist.
+
+
+
+
+ update_manager.backup.docker_warning
+ Docker-installation registreret. Sikkerhedskopier gemmes i var/backups/, som ikke er en persistent enhed. Brug downloadknappen til at gemme sikkerhedskopier eksternt, eller montér var/backups/ som en enhed i din docker-compose.yml.
+
+ settings.ips.conrad
@@ -12410,5 +12686,281 @@ Buerklin API-godkendelsesserver: 10 anmodninger/minut pr. IP-adresse
Opdatér til
+
+
+ part.gtin
+ GTIN / EAN
+
+
+
+
+ info_providers.capabilities.gtin
+ GTIN / EAN
+
+
+
+
+ part.table.gtin
+ GTIN
+
+
+
+
+ scan_dialog.mode.gtin
+ GTIN / EAN barcode
+
+
+
+
+ attachment_type.edit.allowed_targets
+ Anvend kun til
+
+
+
+
+ attachment_type.edit.allowed_targets.help
+ Gør kun denne bilagstype tilgængelig for bestemte elementklasser. Lad feltet stå tomt for at vise denne bilagstype for alle elementklasser.
+
+
+
+
+ orderdetails.edit.prices_includes_vat
+ Pris inklusiv moms.
+
+
+
+
+ prices.incl_vat
+ Inkl. moms
+
+
+
+
+ prices.excl_vat
+ Ekskl. moms
+
+
+
+
+ settings.system.localization.prices_include_tax_by_default
+ Priserne er som standard inklusive moms
+
+
+
+
+ settings.system.localization.prices_include_tax_by_default.description
+ Standardværdien for nyoprettede købsoplysninger, uanset om priserne inkluderer moms eller ej.
+
+
+
+
+ part_lot.edit.last_stocktake_at
+ Seneste optælling
+
+
+
+
+ perm.parts_stock.stocktake
+ Lageropgørelse
+
+
+
+
+ part.info.stocktake_modal.title
+ Lagerbeholdning
+
+
+
+
+ part.info.stocktake_modal.expected_amount
+ Forventet mængde
+
+
+
+
+ part.info.stocktake_modal.actual_amount
+ Aktuel mængde
+
+
+
+
+ log.part_stock_changed.stock_take
+ Lagerbeholdning
+
+
+
+
+ log.element_edited.changed_fields.last_stocktake_at
+ Sidste lagerbeholdning
+
+
+
+
+ part.table.eda_reference
+ EDA reference
+
+
+
+
+ part.table.eda_value
+ EDA-værdi
+
+
+
+
+ settings.misc.kicad_eda.default_parameter_visibility
+ Standard EDA-synlighed for parametre
+
+
+
+
+ settings.misc.kicad_eda.default_parameter_visibility.help
+ EDA-synlighed for alle [Part]-parametre, som ikke har en eksplicit synlighedsindstilling. Når den er aktiveret, vil alle parametre som standard være synlige i EDA-softwaren.
+
+
+
+
+ settings.misc.kicad_eda.default_orderdetails_visibility
+ Standard EDA-synlighed for købsoplysninger
+
+
+
+
+ settings.misc.kicad_eda.default_orderdetails_visibility.help
+ EDA-synlighed for alle købsoplysninger, som ikke har en eksplicit synlighedsindstilling. Når den er aktiveret, vil alle købsoplysninger som standard være synlige i EDA-softwaren.
+
+
+
+
+ label_scanner.open
+ Vis detaljer
+
+
+
+
+ label_scanner.db_part_found
+ Database [part] fundet for barcode
+
+
+
+
+ label_scanner.part_can_be_created
+ [Part] kan oprettes
+
+
+
+
+ label_scanner.part_can_be_created.help
+ Der blev ikke fundet nogen matchende [part] i databasen, men du kan oprette en ny [part] baseret på denne stregkode.
+
+
+
+
+ label_scanner.part_create_btn
+ Opret [part] fra barcode
+
+
+
+
+ parts.create_from_scan.title
+ Opret [part] ud fra labelscanning
+
+
+
+
+ scan_dialog.mode.amazon
+ Amazon barcode
+
+
+
+
+ settings.ips.canopy
+ Canopy
+
+
+
+
+ settings.ips.canopy.alwaysGetDetails
+ Hent altid detaljer
+
+
+
+
+ settings.ips.canopy.alwaysGetDetails.help
+ Når dette er valgt, hentes flere detaljer fra canopy, når en del oprettes. Dette forårsager en yderligere API-anmodning, men giver produktpunkter og kategorioplysninger.
+
+
+
+
+ attachment.sandbox.warning
+ ADVARSEL: Du ser en brugeruploadet vedhæftet fil. Dette er indhold, der ikke er tillid til. Vær forsigtig.
+
+
+
+
+ attachment.sandbox.back_to_partdb
+ Tilbage til Part-DB
+
+
+
+
+ settings.system.attachments.showHTMLAttachments
+ Vis uploadede HTML-filvedhæftninger (sandboxed)
+
+
+
+
+ settings.system.attachments.showHTMLAttachments.help
+ ⚠️ Når det er aktiveret, kan brugeruploadede HTML-vedhæftninger ses direkte i browseren. Mange potentielt skadelige funktioner er begrænsede, men dette er stadig en potentiel sikkerhedsrisiko og bør kun aktiveres, hvis du har tillid til de brugere, der kan uploade filer.
+
+
+
+
+ attachment.sandbox.title
+ HTML [Vedhæftning]
+
+
+
+
+ attachment.sandbox.as_plain_text
+ Vis som alm. tekst
+
+
+
+
+ modal.cancel
+ Annuller
+
+
+
+
+ update_manager.web_updates_allowed
+ Web-opdateringer tilladt
+
+
+
+
+ update_manager.backup_restore_allowed
+ Indlæsning af sikkerhedskopi (backup) tilladt
+
+
+
+
+ update_manager.backup_download_allowed
+ Download af sikkerhedskopi tilladt
+
+
+
+
+ part.create_from_info_provider.lot_filled_from_barcode
+ [Part_lot] oprettet fra stregkode: Kontroller venligst, om dataene er korrekte og ønskede.
+
+
+
+
+ project.bom_import.field_mapping.error.check_delimiter
+ Felttilknytningsfejl: Kontroller, om du har valgt den rigtige tegn-afgrænser!
+
+
-
\ No newline at end of file
+
diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf
index 680d4b7f..7e070ff2 100644
--- a/translations/messages.de.xlf
+++ b/translations/messages.de.xlf
@@ -2779,6 +2779,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr
Name
+
+
+ part.table.si_value
+ SI-Wert
+
+ part.table.id
@@ -7211,6 +7217,18 @@ Element 1 -> Element 1.2
Unterprojekte
+
+
+ project.info.total_build_price
+ Gesamterstellpreis
+
+
+
+
+ project.info.per_unit_price
+ pro Einheit
+
+ project.info.bom_add_parts
@@ -7236,9 +7254,9 @@ Element 1 -> Element 1.2
-
+ project.bom.ext_price
- Extended Price
+ Gesamtpreis
@@ -10034,6 +10052,90 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
Wenn aktiviert, verlinkt das Datenblatt-Feld in KiCad auf die tatsächliche PDF-Datei (sofern gefunden). Wenn deaktiviert, führt es stattdessen zur Part-DB-Seite. Der Link zur Part-DB-Seite ist immer als separates "Part-DB URL"-Feld verfügbar.
+
+
+ settings.misc.kicad_eda.editor.title
+ KiCad Autovervollständigungslisten
+
+
+
+
+ settings.misc.kicad_eda.editor.link
+ Autovervollständigungseinstellungen
+
+
+
+
+ settings.misc.kicad_eda.editor.description
+ Konfigurieren Sie, ob KiCad Autovervollständigung die automatisch generierten Standardlisten oder Ihre benutzerdefinierten Überschreibungsdateien verwendet. Die benutzerdefinierten Dateien sind hier bearbeitbar, während die Standarddateien nur lesbar zur Referenz angezeigt werden.
+
+
+
+
+ settings.misc.kicad_eda.editor.footprints
+ Footprint-Liste
+
+
+
+
+ settings.misc.kicad_eda.editor.footprints.help
+ Ein Eintrag pro Zeile. Wird als Autovervollständigungsvorschlag für KiCad-Footprintfelder verwendet.
+
+
+
+
+ settings.misc.kicad_eda.editor.symbols
+ Symbolliste
+
+
+
+
+ settings.misc.kicad_eda.editor.symbols.help
+ Ein Eintrag pro Zeile. Wird als Autovervollständigungsvorschlag für KiCad-Symbolfelder verwendet.
+
+
+
+
+ settings.misc.kicad_eda.use_custom_list
+ Benutzerdefinierte Autovervollständigungslisten verwenden
+
+
+
+
+ settings.misc.kicad_eda.use_custom_list.help
+ Wenn aktiviert, verwendet die KiCad Autovervollständigung public/kicad/footprints_custom.txt und public/kicad/symbols_custom.txt anstelle der automatisch generierten Standarddateien.
+
+
+
+
+ settings.misc.kicad_eda.editor.custom_footprints
+ Benutzerdefinierte Footprint-Liste
+
+
+
+
+ settings.misc.kicad_eda.editor.custom_symbols
+ Benutzerdefinierte Symbolliste
+
+
+
+
+ settings.misc.kicad_eda.editor.default_footprints
+ Standard Footprint-Liste
+
+
+
+
+ settings.misc.kicad_eda.editor.default_symbols
+ Standardsymboliste
+
+
+
+
+ settings.misc.kicad_eda.editor.default_files_help
+ Automatisch generierte Datei wird nur zur Referenz angezeigt. Änderungen müssen in der benutzerdefinierten Liste vorgenommen werden.
+
+ settings.behavior.sidebar
diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf
index 37e0d27e..49b7ca03 100644
--- a/translations/messages.fr.xlf
+++ b/translations/messages.fr.xlf
@@ -1,13 +1,13 @@
-
+
-
+ attachment_type.caption
- Types pour fichiers joints
+ Type de fichiers pour la pièce jointe
-
+ new
@@ -16,7 +16,7 @@
Modifier le type de pièce jointe
-
+ new
@@ -25,25 +25,25 @@
Nouveau type de pièce jointe
-
+ category.labelpCatégories
-
+ admin.optionsOptions
-
+ admin.advancedAvancé
-
+ new
@@ -52,7 +52,7 @@
Éditer la catégorie
-
+ new
@@ -61,34 +61,28 @@
Nouvelle catégorie
-
-
- currency.caption
- Devise
-
-
-
+ currency.iso_code.captionCode ISO
-
+ currency.symbol.captionSymbole de la devise
-
+ newcurrency.edit
- Editer la devise
+ Éditer la devise
-
+ new
@@ -97,43 +91,61 @@
Nouvelle devise
-
+
+
+ new
+
+
+ project.edit
+ Éditer le projet
+
+
+
+
+ new
+
+
+ project.new
+ Nouveau projet
+
+
+ search.placeholderRecherche
-
+ expandAllAgrandir tout
-
+ reduceAllRéduire tout
-
+ part.info.timetravel_hint
- C'est ainsi que le composant apparaissait avant le %timestamp%. <i>Veuillez noter que cette fonctionnalité est expérimentale, donc les infos ne sont peut-être pas correctes. </i>
+ C'est ainsi que le composant apparaissait avant le %timestamp%. <i>Veuillez noter que cette fonctionnalité est expérimentale, les informations ne sont peut-être pas correctes. </i>
-
+ standard.labelPropriétés
-
+ infos.labelInformations
-
+ new
@@ -142,82 +154,82 @@
Historique
-
+ export.labelExporter
-
+ import_export.label
- Importer exporter
+ Importer / exporter
-
+ mass_creation.label
- Création multiple
+ Création en masse
-
+ admin.commonCommun
-
+ admin.attachmentsFichiers joints
-
+ admin.parametersParamètres
-
+ export_all.labelExporter tous les éléments
-
+ mass_creation.help
- Chaque ligne sera interprétée comme le nom d'un élément qui sera créé.
+ Chaque ligne sera interprétée comme le nom d'un élément qui sera créé. Vous pouvez créer des structures imbriquées par indentations.
-
+ edit.captionÉditer l'élément "%name"
-
+ new.captionNouvel élément
-
+ footprint.labelpEmpreintes
-
+ newfootprint.edit
- Editer l'empreinte
+ Éditer l'empreinte
-
+ new
@@ -226,28 +238,22 @@
Nouvelle empreinte
-
-
- group.edit.caption
- Groupes
-
-
-
+ user.edit.permissionsPermissions
-
+ newgroup.edit
- Editer le groupe
+ Éditer le groupe
-
+ new
@@ -256,34 +262,28 @@
Nouveau groupe
-
-
- label_profile.caption
- Profil des étiquettes
-
-
-
+ label_profile.advancedAvancé
-
+ label_profile.commentCommentaire
-
+ newlabel_profile.edit
- Editer profil d'étiquette
+ Éditer profil d'étiquette
-
+ new
@@ -292,13 +292,7 @@
Nouveau profil d'étiquette
-
-
- manufacturer.caption
- Fabricants
-
-
-
+ new
@@ -307,7 +301,7 @@
Modifiez le fabricant
-
+ new
@@ -316,25 +310,13 @@
Nouveau fabricant
-
-
- measurement_unit.caption
- Unité de mesure
-
-
-
-
- part_custom_state.caption
- État personnalisé du composant
-
-
-
+ storelocation.labelpEmplacement de stockage
-
+ new
@@ -343,7 +325,7 @@
Modifier l'emplacement de stockage
-
+ new
@@ -352,7 +334,7 @@
Nouvel emplacement de stockage
-
+ new
@@ -361,7 +343,7 @@
Modifier le fournisseur
-
+ new
@@ -370,67 +352,61 @@
Nouveau fournisseur
-
-
- user.edit.caption
- Utilisateurs
-
-
-
+ user.edit.configurationConfiguration
-
+ user.edit.passwordMot de passe
-
+ user.edit.tfa.captionAuthentification à deux facteurs
-
+ user.edit.tfa.google_activeApplication d'authentification active
-
+ tfa_backup.remaining_tokensNombre de codes de secours restant
-
+ tfa_backup.generation_dateDate de génération des codes de secours
-
+ user.edit.tfa.disabledMéthode désactivée
-
+ user.edit.tfa.u2f_keys_countClés de sécurité actives
-
+ user.edit.tfa.disable_tfa_title
- Voulez vous vraiment poursuivre ?
+ Voulez-vous vraiment poursuivre ?
-
+ user.edit.tfa.disable_tfa_messageCela désactivera <b> toutes les méthodes d'authentification à deux facteurs de l'utilisateur</b> et supprimera <b>les codes de secours</b>!
@@ -439,13 +415,13 @@ L'utilisateur devra configurer à nouveau toutes les méthodes d'authentificatio
<b>Ne faites ceci qu'en étant sûr de l'identité de l'utilisateur (ayant besoin d'aide),autrement le compte pourrai être compromis!</b>
-
+ user.edit.tfa.disable_tfa.btnDésactiver toutes les méthodes d'authentification à deux facteurs
-
+ new
@@ -454,7 +430,7 @@ L'utilisateur devra configurer à nouveau toutes les méthodes d'authentificatio
Modifier l'utilisateur
-
+ new
@@ -463,75 +439,75 @@ L'utilisateur devra configurer à nouveau toutes les méthodes d'authentificatio
Nouvel utilisateur
-
+ attachment.deleteSupprimer
-
+
- attachment.external
- Externe
+ attachment.external_only
+ Pièce jointe externe uniquement
-
+ attachment.preview.altMiniature du fichier joint
-
+
- attachment.view
- Afficher
+ attachment.view_local
+ Vue locale de la pièce jointe
-
+ attachment.file_not_foundFichier introuvable
-
+ attachment.secureFichier joint privé
-
+ attachment.createAjouter un fichier joint
-
+ part_lot.edit.delete.confirm
- Voulez vous vraiment supprimer ce stock ? Cette action ne pourra pas être annulée!
+ Voulez-vous vraiment supprimer ce stock ? Cette action ne pourra pas être annulée !
-
+ entity.delete.confirm_title
- Voulez vous vraiment supprimer %name%?
+ Voulez-vous vraiment supprimer %name% ?
-
+ entity.delete.message
- Cette action ne pourra pas être annulée!
+ Cette action ne pourra pas être annulée !
<br>
Les sous éléments seront déplacés vers le haut.
-
+ entity.deleteSupprimer l'élément
-
+ new
@@ -540,308 +516,313 @@ Les sous éléments seront déplacés vers le haut.
Éditer le commentaire
-
+ entity.delete.recursiveSuppression récursive (tous les sous éléments)
-
+ entity.duplicateDupliquer l’élément
-
+ export.formatFormat de fichier
-
+ export.levelNiveau de verbosité
-
+ export.level.simpleSimple
-
+ export.level.extendedÉtendu
-
+ export.level.fullComplet
-
+ export.include_childrenExporter également les sous éléments
-
+ export.btnExporter
-
+ id.labelID
-
+ createdAtCréé le
-
+ lastModifiedDernière modification
-
+ entity.info.parts_countNombre de composants avec cet élément
-
+ specifications.propertyParamètre
-
+ specifications.symbolSymbole
-
+ specifications.value_minMin.
-
+ specifications.value_typ
- Typ.
+ Type.
-
+ specifications.value_maxMax.
-
+ specifications.unitUnité
-
+ specifications.textTexte
-
+ specifications.groupGroupe
-
+
+
+ specifications.eda_visibility.help
+ Exporter en tant que paramètre EDA
+
+
+ specification.createNouveau paramètre
-
+ parameter.delete.confirmSouhaitez-vous vraiment supprimer ce paramètre ?
-
+ attachment.list.titleListe des fichiers joints
-
+ part_list.loading.captionChargement
-
+ part_list.loading.messageCela peut prendre un moment.Si ce message ne disparaît pas, essayez de recharger la page.
-
+ vendor.base.javascript_hint
- Activez Javascipt pour profiter de toutes les fonctionnalités!
+ Activez JavaScript pour profiter de toutes les fonctionnalités !
-
+ sidebar.big.toggle
- Afficher/Cacher le panneau latéral
-Show/Hide sidebar
+ Afficher / Cacher le panneau latéral
-
+ loading.caption
- Chargement:
+ Chargement :
-
+ loading.messageCela peut prendre un moment.Si ce message ne disparaît pas, essayez de recharger la page.
-
+ loading.barChargement...
-
+ back_to_topRetour en haut de page
-
+ permission.edit.permissionPermissions
-
+ permission.edit.valueValeur
-
+ permission.legend.title
- Explication des états:
+ Explication des états :
-
+ permission.legend.disallowInterdire
-
+ permission.legend.allowAutoriser
-
+ permission.legend.inheritHériter du groupe (parent)
-
+ bool.trueVrai
-
+ bool.falseFaux
-
+ YesOui
-
+ NoNon
-
+ specifications.valueValeur
-
+ version.captionVersion
-
+ homepage.license
- Information de license
+ Information de licence
-
+ homepage.github.captionPage du projet
-
+ homepage.github.text
- Retrouvez les téléchargements, report de bugs, to-do-list etc. sur <a href="%href%" class="link-external" target="_blank">la page du projet GitHub</a>
+ Retrouvez les téléchargements, report de bugs, to-do-list, etc. sur <a href="%href%" class="link-external" target="_blank">la page du projet GitHub</a>
-
+ homepage.help.captionAide
-
+ homepage.help.textDe l'aide et des conseils sont disponibles sur le Wiki de la <a href="%href%" class="link-external" target="_blank">page GitHub</a>
-
+ homepage.forum.captionForum
-
+ new
@@ -850,97 +831,97 @@ Show/Hide sidebar
Activité récente
-
+ label_generator.titleGénérateur d'étiquettes
-
+ label_generator.commonCommun
-
+ label_generator.advancedAvancé
-
+ label_generator.profilesProfils
-
+ label_generator.selected_profileProfil actuellement sélectionné
-
+ label_generator.edit_profileModifier le profil
-
+ label_generator.load_profileCharger le profil
-
+ label_generator.downloadTélécharger
-
+ label_generator.label_btnGénérer une étiquette
-
+ label_generator.label_emptyNouvelle étiquette vide
-
+ label_scanner.titleLecteur d'étiquettes
-
+ label_scanner.no_cam_found.titleAucune webcam trouvée
-
+ label_scanner.no_cam_found.textVous devez disposer d'une webcam et donner l'autorisation d'utiliser la fonction de scanner. Vous pouvez entrer le code à barres manuellement ci-dessous.
-
+ label_scanner.source_selectSélectionnez une source
-
+ log.list.titleJournal système
-
+ new
@@ -949,7 +930,7 @@ Show/Hide sidebar
Annuler le changement / revenir à une date antérieure ?
-
+ new
@@ -958,337 +939,337 @@ Show/Hide sidebar
Voulez-vous annuler la modification donnée / réinitialiser l'élément à une date donnée ?
-
+ mail.footer.email_sent_byCet email a été envoyé automatiquement par
-
+ mail.footer.dont_replyNe répondez pas à cet email.
-
+ email.hi %name%Bonjour %name%
-
+ email.pw_reset.messageQuelqu’un (surement vous) a demandé une réinitialisation de votre mot de passe.Si ce n'est pas le cas, ignorez simplement cet email.
-
+ email.pw_reset.buttonCliquez ici pour réinitialiser votre mot de passe
-
+ email.pw_reset.fallbackSi cela ne fonctionne pas pour vous, allez à <a href="%url%">%url%</a> et entrez les informations suivantes
-
+ email.pw_reset.usernameNom d'utilisateur
-
+ email.pw_reset.tokenJeton
-
+ email.pw_reset.valid_unit %date%Le jeton de réinitialisation sera valable jusqu'au <i>%date%</i>.
-
+ orderdetail.deleteSupprimer
-
+ pricedetails.edit.min_qtyQuantité minimale de commande
-
+ pricedetails.edit.pricePrix
-
+ pricedetails.edit.price_qtyPour la quantité
-
+ pricedetail.createAjouter prix
-
+ part.edit.titleÉditer le composant
-
+ part.edit.card_titleÉditer le composant
-
+ part.edit.tab.commonGénéral
-
+ part.edit.tab.manufacturerFabricant
-
+ part.edit.tab.advancedAvancé
-
+ part.edit.tab.advanced.ipn.commonSectionHeader
- Suggestions sans incrément de partie
+ Suggestions sans incrémentation de composant
-
+ part.edit.tab.advanced.ipn.partIncrementHeader
- Propositions avec incréments numériques de parties
+ Suggestions avec incrément numérique du composant
-
+ part.edit.tab.advanced.ipn.prefix.description.current-increment
- Spécification IPN actuelle pour la pièce
+ Caractéristiques IPN actuelles du composant
-
+ part.edit.tab.advanced.ipn.prefix.description.increment
- Prochaine spécification IPN possible basée sur une description identique de la pièce
+ Prochaine caractéristique IPN possible basé sur la description d'un composant identique
-
+ 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%"
+ Préfixe IPN ou catégorie vide, en spécifier une dans la catégorie "%name%"
-
+ part.edit.tab.advanced.ipn.prefix.direct_category
- Préfixe IPN de la catégorie directe
+ Préfixe IPN ou 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
+ Préfixe IPN de catégorie directe et incrément spécifique au composant
-
+ part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment
- Préfixes IPN avec un ordre hiérarchique des catégories des préfixes parents
+ Préfixes IPN avec ordre hiérarchique de catégorie du préfixe parent
-
+ 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
+ Préfixes IPN avec ordre hiérarchique de catégorie du préfixe parent et 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
+ Créer d'abord une pièce et l'assigner à une catégorie : pour les catégories existantes et leur propre préfixe IPN, l'IPN de la pièce peut-être suggérer automatiquement
-
+ part.edit.tab.part_lotsStocks
-
+ part.edit.tab.attachmentsFichiers joints
-
+ part.edit.tab.orderdetailsInformations pour la commande
-
+ part.edit.tab.specificationsCaractéristiques
-
+ part.edit.tab.commentCommentaire
-
+ part.new.card_titleCréer un nouveau composant
-
+ part_lot.deleteSupprimer
-
+ part_lot.createCréer un inventaire
-
+ orderdetail.createAjouter un fournisseur
-
+ pricedetails.edit.delete.confirmVoulez-vous vraiment supprimer ce prix ? Cela ne peut pas être défait !
-
+ orderdetails.edit.delete.confirmVoulez-vous vraiment supprimer ce fournisseur ? Cela ne peut pas être défait !
-
+ part.info.titleInformations détaillées pour
-
+ part.part_lots.labelStocks
-
+ comment.labelCommentaire
-
+ part.info.specificationsCaractéristiques
-
+ attachment.labelpFichiers joints
-
+ vendor.partinfo.shopping_infosInformations de commande
-
+ vendor.partinfo.historyHistorique
-
+ tools.labelOutils
-
+ extended_info.labelInformations complémentaires
-
+ attachment.nameNom
-
+ attachment.attachment_typeType de fichier joint
-
+ attachment.file_nameNom du fichier
-
+ attachment.file_sizeTaille du fichier
-
+ attachment.previewAperçu de l'image
-
+
- attachment.download
- Téléchargement
+ attachment.download_local
+ Télécharger la pièce jointe locale
-
+ new
@@ -1297,13 +1278,13 @@ Show/Hide sidebar
Utilisateur qui a créé ce composant
-
+ UnknownInconnu
-
+ new
@@ -1312,7 +1293,7 @@ Show/Hide sidebar
Accès refusé
-
+ new
@@ -1321,31 +1302,31 @@ Show/Hide sidebar
Utilisateur qui a édité ce composant en dernier
-
+ part.isFavoriteFavoris
-
+ part.minOrderAmountQuantité minimale de commande
-
+ manufacturer.labelFabricant
-
+ name.labelNom
-
+ new
@@ -1354,439 +1335,439 @@ Show/Hide sidebar
Retour à la version actuelle
-
+ description.labelDescription
-
+ category.labelCatégorie
-
+ instock.labelEn stock
-
+ mininstock.labelStock minimum
-
+ footprint.labelEmpreinte
-
+ part.avg_price.labelPrix moyen
-
+ part.supplier.nameNom
-
+ part.supplier.partnr
- Lien/Code cmd.
+ Lien/Code Fournisseur
-
+ part.order.minamountNombre minimum
-
+ part.order.pricePrix
-
+ part.order.single_pricePrix unitaire
-
+ part_lots.descriptionDescription
-
+ part_lots.storage_locationEmplacement de stockage
-
+ part_lots.amountQuantité
-
+ part_lots.location_unknownEmplacement de stockage inconnu
-
+ part_lots.instock_unknownQuantité inconnue
-
+ part_lots.expiration_dateDate d'expiration
-
+ part_lots.is_expiredExpiré
-
+ part_lots.need_refillDoit être rempli à nouveau
-
+ part.info.prev_pictureImage précédente
-
+ part.info.next_pictureImage suivante
-
+ part.mass.tooltipPoids
-
+ part.needs_review.badgeRévision nécessaire
-
+ part.favorite.badgeFavoris
-
+ part.obsolete.badgeN'est plus disponible
-
+ parameters.extracted_from_descriptionAutomatiquement extrait de la description
-
+ parameters.auto_extracted_from_commentAutomatiquement extrait du commentaire
-
+ part.edit.btnÉditer
-
+ part.clone.btnDuplication
-
+ part.create.btnCréer un nouveau composant
-
+ part.delete.confirm_titleVoulez-vous vraiment supprimer ce composant ?
-
+ part.delete.messageLe composant et toutes les informations associées (stocks, fichiers joints, etc.) sont supprimés. Cela ne pourra pas être annulé.
-
+ part.deleteSupprimer le composant
-
+ parts_list.all.titleTous les composants
-
+ parts_list.category.titleComposants avec catégorie
-
+ parts_list.footprint.titleComposants avec empreinte
-
+ parts_list.manufacturer.titleComposants avec fabricant
-
+ parts_list.search.titleRecherche de composants
-
+ parts_list.storelocation.titleComposants avec lieu de stockage
-
+ parts_list.supplier.titleComposants avec fournisseur
-
+ parts_list.tags.titleComposants avec tag
-
+ entity.info.common.tabGénéral
-
+ entity.info.statistics.tabStatistiques
-
+ entity.info.attachments.tabPièces jointes
-
+ entity.info.parameters.tabCaractéristiques
-
+ entity.info.nameNom
-
+ entity.info.parentParent
-
+ entity.edit.btnÉditer
-
+ entity.info.children_countNombre de sous-éléments
-
+ tfa.check.titleAuthentification à deux facteurs requise
-
+ tfa.code.trusted_pcIl s'agit d'un ordinateur de confiance (si cette fonction est activée, aucune autre requête à deux facteurs n'est effectuée sur cet ordinateur)
-
+