From ec80115d0af76f0f9e111a7669e99e743a91c691 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Sun, 21 Jun 2026 23:10:44 +0200
Subject: [PATCH 01/24] Renamed AI response schema from errornous "clock" to
"part_detail"
---
src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php b/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php
index a61e7465..3c2cbf64 100644
--- a/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php
+++ b/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php
@@ -42,7 +42,7 @@ final class DTOJsonSchemaConverter
public function getJSONSchema(): array
{
return [
- 'name' => 'clock',
+ 'name' => 'part_detail',
'strict' => true,
'schema' => [
'type' => 'object',
From 3e725dd2ec0fa28c0c6be06001333a8e90a054c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Mon, 22 Jun 2026 22:06:30 +0200
Subject: [PATCH 02/24] Increased timeout for local AI inferences, and made AI
timeout configurable per provider
Fixes issue #1396
---
config/packages/ai_lm_studio_platform.yaml | 1 +
config/packages/ai_ollama_platform.yaml | 1 +
config/packages/ai_open_router_platform.yaml | 1 +
config/services.yaml | 22 +++++++++++++++++++
.../Providers/AIWebProvider.php | 4 ++++
src/Settings/AISettings/AISettings.php | 2 ++
src/Settings/AISettings/LMStudioSettings.php | 11 +++++++++-
src/Settings/AISettings/OllamaSettings.php | 10 +++++++++
.../AISettings/OpenRouterSettings.php | 10 +++++++++
translations/messages.en.xlf | 12 ++++++++++
10 files changed, 73 insertions(+), 1 deletion(-)
diff --git a/config/packages/ai_lm_studio_platform.yaml b/config/packages/ai_lm_studio_platform.yaml
index 0e4287e0..1d832763 100644
--- a/config/packages/ai_lm_studio_platform.yaml
+++ b/config/packages/ai_lm_studio_platform.yaml
@@ -2,3 +2,4 @@ ai:
platform:
lmstudio:
host_url: '%env(string:settings:ai_lmstudio:hostURL)%'
+ http_client: 'app.http_client.ai_lmstudio'
diff --git a/config/packages/ai_ollama_platform.yaml b/config/packages/ai_ollama_platform.yaml
index df551d4a..67ebe190 100644
--- a/config/packages/ai_ollama_platform.yaml
+++ b/config/packages/ai_ollama_platform.yaml
@@ -3,3 +3,4 @@ ai:
ollama:
endpoint: '%env(string:settings:ai_ollama:endpoint)%'
api_key: '%env(string:settings:ai_ollama:apiKey)%'
+ http_client: 'app.http_client.ai_ollama'
diff --git a/config/packages/ai_open_router_platform.yaml b/config/packages/ai_open_router_platform.yaml
index d34de592..53eb20b9 100644
--- a/config/packages/ai_open_router_platform.yaml
+++ b/config/packages/ai_open_router_platform.yaml
@@ -2,3 +2,4 @@ ai:
platform:
openrouter:
api_key: '%env(string:settings:ai_openrouter:apiKey)%'
+ http_client: 'app.http_client.ai_openrouter'
diff --git a/config/services.yaml b/config/services.yaml
index 5021c577..aa476bbb 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -52,6 +52,28 @@ services:
alias: 'doctrine.migrations.dependency_factory'
+ ####################################################################################################################
+ # AI provider HTTP clients (with configurable timeouts)
+ ####################################################################################################################
+
+ app.http_client.ai_ollama:
+ class: Symfony\Contracts\HttpClient\HttpClientInterface
+ factory: ['@http_client', 'withOptions']
+ arguments:
+ - { timeout: '%env(int:settings:ai_ollama:timeout)%' }
+
+ app.http_client.ai_lmstudio:
+ class: Symfony\Contracts\HttpClient\HttpClientInterface
+ factory: ['@http_client', 'withOptions']
+ arguments:
+ - { timeout: '%env(int:settings:ai_lmstudio:timeout)%' }
+
+ app.http_client.ai_openrouter:
+ class: Symfony\Contracts\HttpClient\HttpClientInterface
+ factory: ['@http_client', 'withOptions']
+ arguments:
+ - { timeout: '%env(int:settings:ai_openrouter:timeout)%' }
+
####################################################################################################################
# Email
####################################################################################################################
diff --git a/src/Services/InfoProviderSystem/Providers/AIWebProvider.php b/src/Services/InfoProviderSystem/Providers/AIWebProvider.php
index 6539e69b..be91041e 100644
--- a/src/Services/InfoProviderSystem/Providers/AIWebProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/AIWebProvider.php
@@ -282,6 +282,10 @@ final class AIWebProvider implements InfoProviderInterface
try {
$aiPlatform = $this->AIPlatformRegistry->getPlatform($this->settings->platform ?? throw new \RuntimeException('No AI platform selected') );
+ // AI inference can take much longer than PHP's default max_execution_time (typically 30s).
+ // The HTTP client timeout already enforces the configured limit; disable PHP's constraint here.
+ set_time_limit(0);
+
//'openai/gpt-5-mini'
$result = $aiPlatform->invoke($this->settings->model ?? throw new \RuntimeException('No model selected'), $input, [
'response_format' => [
diff --git a/src/Settings/AISettings/AISettings.php b/src/Settings/AISettings/AISettings.php
index 9f145c7f..659577b6 100644
--- a/src/Settings/AISettings/AISettings.php
+++ b/src/Settings/AISettings/AISettings.php
@@ -35,6 +35,8 @@ class AISettings
{
use SettingsTrait;
+ public const TIMEOUT_LIMIT = 600;
+
#[EmbeddedSettings]
public ?OpenRouterSettings $openRouter = null;
diff --git a/src/Settings/AISettings/LMStudioSettings.php b/src/Settings/AISettings/LMStudioSettings.php
index 2bdad06e..d92ce97e 100644
--- a/src/Settings/AISettings/LMStudioSettings.php
+++ b/src/Settings/AISettings/LMStudioSettings.php
@@ -23,16 +23,17 @@ declare(strict_types=1);
namespace App\Settings\AISettings;
-use App\Form\Type\APIKeyType;
use App\Services\AI\AIPlatformSettingsInterface;
use App\Settings\SettingsIcon;
use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
+use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Translation\StaticMessage;
use Symfony\Component\Translation\TranslatableMessage as TM;
+use Symfony\Component\Validator\Constraints as Assert;
#[Settings(name: 'ai_lmstudio', label: new TM("settings.ai.lmstudio"))]
#[SettingsIcon("fa-robot")]
@@ -46,6 +47,14 @@ class LMStudioSettings implements AIPlatformSettingsInterface
envVar: "AI_LMSTUDIO_HOSTURL", envVarMode: EnvVarMode::OVERWRITE)]
public ?string $hostURL = null;
+ #[SettingsParameter(label: new TM("settings.ai.timeout"),
+ description: new TM("settings.ai.timeout.help"),
+ formType: NumberType::class,
+ formOptions: ["scale" => 0, "attr" => ["min" => 1]],
+ )]
+ #[Assert\Range(min: 1, max: AISettings::TIMEOUT_LIMIT)]
+ public int $timeout = 180;
+
public function isAIPlatformEnabled(): bool
{
return $this->hostURL !== null && $this->hostURL !== "";
diff --git a/src/Settings/AISettings/OllamaSettings.php b/src/Settings/AISettings/OllamaSettings.php
index dd17d5e2..7ca0e5a0 100644
--- a/src/Settings/AISettings/OllamaSettings.php
+++ b/src/Settings/AISettings/OllamaSettings.php
@@ -30,9 +30,11 @@ use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
+use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Translation\StaticMessage;
use Symfony\Component\Translation\TranslatableMessage as TM;
+use Symfony\Component\Validator\Constraints as Assert;
#[Settings(name: 'ai_ollama', label: new TM("settings.ai.ollama"))]
#[SettingsIcon("fa-robot")]
@@ -51,6 +53,14 @@ class OllamaSettings implements AIPlatformSettingsInterface
envVar: "AI_OLLAMA_API_KEY", envVarMode: EnvVarMode::OVERWRITE)]
public ?string $apiKey = null;
+ #[SettingsParameter(label: new TM("settings.ai.timeout"),
+ description: new TM("settings.ai.timeout.help"),
+ formType: NumberType::class,
+ formOptions: ["scale" => 0, "attr" => ["min" => 1]]
+ )]
+ #[Assert\Range(min: 1, max: AISettings::TIMEOUT_LIMIT)]
+ public int $timeout = 180;
+
public function isAIPlatformEnabled(): bool
{
return $this->endpoint !== null && $this->endpoint !== "";
diff --git a/src/Settings/AISettings/OpenRouterSettings.php b/src/Settings/AISettings/OpenRouterSettings.php
index e083513a..16665554 100644
--- a/src/Settings/AISettings/OpenRouterSettings.php
+++ b/src/Settings/AISettings/OpenRouterSettings.php
@@ -30,7 +30,9 @@ use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
+use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Translation\TranslatableMessage as TM;
+use Symfony\Component\Validator\Constraints as Assert;
#[Settings(name: 'ai_openrouter', label: new TM("settings.ai.openrouter"), description: "settings.ai.openrouter.help")]
#[SettingsIcon("fa-robot")]
@@ -43,6 +45,14 @@ class OpenRouterSettings implements AIPlatformSettingsInterface
formOptions: ["help_html" => true], envVar: "AI_OPENROUTER_KEY", envVarMode: EnvVarMode::OVERWRITE)]
public ?string $apiKey = null;
+ #[SettingsParameter(label: new TM("settings.ai.timeout"),
+ description: new TM("settings.ai.timeout.help"),
+ formType: NumberType::class,
+ formOptions: ["scale" => 0, "attr" => ["min" => 1]],
+ envVar: "int:AI_OPENROUTER_TIMEOUT", envVarMode: EnvVarMode::OVERWRITE)]
+ #[Assert\Range(min: 1, max: AISettings::TIMEOUT_LIMIT)]
+ public int $timeout = 90;
+
public function isAIPlatformEnabled(): bool
{
return $this->apiKey !== null && $this->apiKey !== "";
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index af33ee97..d828d3ee 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -13637,6 +13637,18 @@ Buerklin-API Authentication server:
API Key
+
+
+ settings.ai.timeout
+ Timeout
+
+
+
+
+ settings.ai.timeout.help
+ Maximum time in seconds to wait for a response. Local AI inference might take multiple minutes, cloud inference is normally faster.
+
+
browser_plugin.recent_pages.title
From 5e18ae28740122ba765babc7a1953addf18ba59f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Thu, 25 Jun 2026 00:08:57 +0200
Subject: [PATCH 03/24] Fixed problem with changing last_stocktake date, when
editing part
The database has UTC values, we need to set that in the form. A form extension was added to ensure that this does not happen again. Also an issue was fixed that the seconds were cut off. This fixes issue #1390
---
.../DateTimeModelTimezoneExtension.php | 79 +++++++++++++++++++
src/Form/Part/PartLotType.php | 2 +
2 files changed, 81 insertions(+)
create mode 100644 src/Form/Extension/DateTimeModelTimezoneExtension.php
diff --git a/src/Form/Extension/DateTimeModelTimezoneExtension.php b/src/Form/Extension/DateTimeModelTimezoneExtension.php
new file mode 100644
index 00000000..3c4818ea
--- /dev/null
+++ b/src/Form/Extension/DateTimeModelTimezoneExtension.php
@@ -0,0 +1,79 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\Form\Extension;
+
+use Symfony\Component\Form\AbstractTypeExtension;
+use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
+use Symfony\Component\Form\Extension\Core\Type\DateType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\FormEvents;
+
+/**
+ * Catches timezone mismatches between a DateTimeInterface model value and the effective
+ * model_timezone configured on the field.
+ *
+ * Doctrine's UTCDateTimeImmutableType always returns UTC DateTimeImmutable objects, so any
+ * date/datetime field that omits `model_timezone: 'UTC'` will silently corrupt stored values
+ * (the transformer treats the UTC instant as if it were in the user's local timezone).
+ * This extension throws a \LogicException early so the mistake is caught at development time.
+ */
+class DateTimeModelTimezoneExtension extends AbstractTypeExtension
+{
+ public static function getExtendedTypes(): iterable
+ {
+ return [DateTimeType::class, DateType::class];
+ }
+
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void {
+ $data = $event->getData();
+
+ if (!$data instanceof \DateTimeInterface) {
+ return;
+ }
+
+ // Resolve the effective model timezone: explicit option or the PHP default set at build time.
+ // This mirrors what BaseDateTimeTransformer does in its constructor.
+ $modelTimezone = $options['model_timezone'] ?? date_default_timezone_get();
+
+ $dataOffset = $data->getTimezone()->getOffset($data);
+ $modelOffset = (new \DateTimeZone($modelTimezone))->getOffset($data);
+
+ if ($dataOffset !== $modelOffset) {
+ throw new \LogicException(sprintf(
+ 'Form field "%s" received a %s with timezone "%s" (UTC offset %+d s), '
+ . 'but the effective model_timezone is "%s" (UTC offset %+d s). '
+ . 'Set the "model_timezone" option to match the timezone of your data source.',
+ $event->getForm()->getName(),
+ get_debug_type($data),
+ $data->getTimezone()->getName(),
+ $dataOffset,
+ $modelTimezone,
+ $modelOffset
+ ));
+ }
+ });
+ }
+}
diff --git a/src/Form/Part/PartLotType.php b/src/Form/Part/PartLotType.php
index fc330bb1..ef49c57e 100644
--- a/src/Form/Part/PartLotType.php
+++ b/src/Form/Part/PartLotType.php
@@ -115,8 +115,10 @@ class PartLotType extends AbstractType
$builder->add('last_stocktake_at', DateTimeType::class, [
'label' => 'part_lot.edit.last_stocktake_at',
'widget' => 'single_text',
+ 'model_timezone' => 'UTC', // The database stores the datetime in UTC, so we need to set the model timezone to UTC
'disabled' => !$this->security->isGranted('@parts_stock.stocktake'),
'required' => false,
+ 'with_seconds' => true,
]);
}
From 9d4dabbd20088761b5dead8b66816a9a63082015 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Thu, 25 Jun 2026 10:54:13 +0200
Subject: [PATCH 04/24] Removed useless setAccessible() calls
They are noop since 8.1 and we only support 8.2+
---
src/Repository/DBElementRepository.php | 1 -
.../PartKeeprImporter/PKImportHelperTrait.php | 1 -
tests/Entity/Attachments/AttachmentTest.php | 1 -
.../Providers/BuerklinProviderTest.php | 10 ++--------
.../InfoProviderSystem/Providers/LCSCProviderTest.php | 7 -------
5 files changed, 2 insertions(+), 18 deletions(-)
diff --git a/src/Repository/DBElementRepository.php b/src/Repository/DBElementRepository.php
index f737d91d..6e13aff7 100644
--- a/src/Repository/DBElementRepository.php
+++ b/src/Repository/DBElementRepository.php
@@ -158,7 +158,6 @@ class DBElementRepository extends EntityRepository
{
$reflection = new ReflectionClass($element::class);
$property = $reflection->getProperty($field);
- $property->setAccessible(true);
$property->setValue($element, $new_value);
}
}
diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php
index 08b1c301..cbfb4ee0 100644
--- a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php
+++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php
@@ -233,7 +233,6 @@ trait PKImportHelperTrait
$reflectionClass = new \ReflectionClass($entity);
$property = $reflectionClass->getProperty('addedDate');
- $property->setAccessible(true);
$property->setValue($entity, $date);
}
diff --git a/tests/Entity/Attachments/AttachmentTest.php b/tests/Entity/Attachments/AttachmentTest.php
index 9e912b97..bef2df16 100644
--- a/tests/Entity/Attachments/AttachmentTest.php
+++ b/tests/Entity/Attachments/AttachmentTest.php
@@ -276,7 +276,6 @@ final class AttachmentTest extends TestCase
{
$reflection = new ReflectionClass($object);
$reflection_property = $reflection->getProperty($property);
- $reflection_property->setAccessible(true);
$reflection_property->setValue($object, $value);
}
diff --git a/tests/Services/InfoProviderSystem/Providers/BuerklinProviderTest.php b/tests/Services/InfoProviderSystem/Providers/BuerklinProviderTest.php
index ef446c9a..d466f048 100644
--- a/tests/Services/InfoProviderSystem/Providers/BuerklinProviderTest.php
+++ b/tests/Services/InfoProviderSystem/Providers/BuerklinProviderTest.php
@@ -77,7 +77,6 @@ final class BuerklinProviderTest extends TestCase
public function testAttributesToParametersParsesUnitsAndValues(): void
{
$method = new \ReflectionMethod(BuerklinProvider::class, 'attributesToParameters');
- $method->setAccessible(true);
$features = [
[
@@ -127,7 +126,6 @@ final class BuerklinProviderTest extends TestCase
public function testComplianceParameters(): void
{
$method = new \ReflectionMethod(BuerklinProvider::class, 'complianceToParameters');
- $method->setAccessible(true);
$product = [
'labelRoHS' => 'Yes',
@@ -158,7 +156,6 @@ final class BuerklinProviderTest extends TestCase
public function testImageSelectionPrefersZoomAndDeduplicates(): void
{
$method = new \ReflectionMethod(BuerklinProvider::class, 'getProductImages');
- $method->setAccessible(true);
$images = [
['format' => 'product', 'url' => '/img/a.webp'],
@@ -176,7 +173,6 @@ final class BuerklinProviderTest extends TestCase
public function testFootprintExtraction(): void
{
$method = new \ReflectionMethod(BuerklinProvider::class, 'getPartDetail');
- $method->setAccessible(true);
$product = [
'code' => 'TEST1',
@@ -212,7 +208,6 @@ final class BuerklinProviderTest extends TestCase
];
$method = new \ReflectionMethod(BuerklinProvider::class, 'pricesToVendorInfo');
- $method->setAccessible(true);
$vendorInfo = $method->invoke($this->provider, 'SKU1', 'https://x', $detailPrice);
@@ -260,7 +255,6 @@ final class BuerklinProviderTest extends TestCase
);
$method = new \ReflectionMethod(BuerklinProvider::class, 'convertPartDetailToSearchResult');
- $method->setAccessible(true);
$dto = $method->invoke($this->provider, $detail);
@@ -273,13 +267,13 @@ final class BuerklinProviderTest extends TestCase
{
$this->assertSame(['buerklin.com'], $this->provider->getHandledDomains());
}
-
+
#[DataProvider('buerklinIdFromUrlProvider')]
public function testGetIDFromURLExtractsId(string $url, ?string $expected): void
{
$this->assertSame($expected, $this->provider->getIDFromURL($url));
}
-
+
public static function buerklinIdFromUrlProvider(): \Iterator
{
yield 'de long path' => [
diff --git a/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php b/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php
index dc19de6b..9ec3abe1 100644
--- a/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php
+++ b/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php
@@ -367,7 +367,6 @@ final class LCSCProviderTest extends TestCase
{
$reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('sanitizeField');
- $method->setAccessible(true);
$this->assertNull($method->invokeArgs($this->provider, [null]));
$this->assertEquals('Clean text', $method->invokeArgs($this->provider, ['Clean text']));
@@ -378,7 +377,6 @@ final class LCSCProviderTest extends TestCase
{
$reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('getUsedCurrency');
- $method->setAccessible(true);
$this->assertEquals('USD', $method->invokeArgs($this->provider, ['US$']));
$this->assertEquals('USD', $method->invokeArgs($this->provider, ['$']));
@@ -391,7 +389,6 @@ final class LCSCProviderTest extends TestCase
{
$reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('getProductShortURL');
- $method->setAccessible(true);
$result = $method->invokeArgs($this->provider, ['C123456']);
$this->assertEquals('https://www.lcsc.com/product-detail/C123456.html', $result);
@@ -401,7 +398,6 @@ final class LCSCProviderTest extends TestCase
{
$reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('getProductDatasheets');
- $method->setAccessible(true);
$result = $method->invokeArgs($this->provider, [null]);
$this->assertIsArray($result);
@@ -417,7 +413,6 @@ final class LCSCProviderTest extends TestCase
{
$reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('getProductImages');
- $method->setAccessible(true);
$result = $method->invokeArgs($this->provider, [null]);
$this->assertIsArray($result);
@@ -434,7 +429,6 @@ final class LCSCProviderTest extends TestCase
{
$reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('attributesToParameters');
- $method->setAccessible(true);
$attributes = [
['paramNameEn' => 'Resistance', 'paramValueEn' => '1kΩ'],
@@ -454,7 +448,6 @@ final class LCSCProviderTest extends TestCase
{
$reflection = new \ReflectionClass($this->provider);
$method = $reflection->getMethod('pricesToVendorInfo');
- $method->setAccessible(true);
$prices = [
['ladder' => 1, 'productPrice' => '0.10', 'currencySymbol' => 'US$'],
From fe0809230b86f8f495af2eb0039baa6bfd69dc12 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Thu, 25 Jun 2026 11:59:37 +0200
Subject: [PATCH 05/24] Fixed sqlite deprecation on PHP8.5
---
.../SQLiteRegexExtensionMiddlewareDriver.php | 32 +++++++++++++------
1 file changed, 23 insertions(+), 9 deletions(-)
diff --git a/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php
index aa6108c9..fa3230e4 100644
--- a/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php
+++ b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php
@@ -27,6 +27,7 @@ use App\Doctrine\Functions\SiValueSort;
use App\Exceptions\InvalidRegexException;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
+use Pdo\Sqlite;
/**
* This middleware is used to add the regexp operator to the SQLite platform.
@@ -44,17 +45,30 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
if ($params['driver'] === 'pdo_sqlite') {
$native_connection = $connection->getNativeConnection();
- //Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation
if($native_connection instanceof \PDO) {
- $native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC);
- $native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC);
- $native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC);
- //Create a new collation for natural sorting
- $native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
+ //Use the new PDO::createFunction and PDO::createCollation methods if available (PHP 8.4+)
+ if (is_a($native_connection, \PDO\Sqlite::class)) { #TODO: Remove this check when PHP 8.4 is the minimum requirement
+ $native_connection->createFunction('REGEXP', self::regexp(...), 2, Sqlite::DETERMINISTIC);
+ $native_connection->createFunction('FIELD', self::field(...), -1, Sqlite::DETERMINISTIC);
+ $native_connection->createFunction('FIELD2', self::field2(...), 2, Sqlite::DETERMINISTIC);
- //Create a function for SI prefix value sorting
- $native_connection->sqliteCreateFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, \PDO::SQLITE_DETERMINISTIC);
+ //Create a new collation for natural sorting
+ $native_connection->createCollation('NATURAL_CMP', strnatcmp(...));
+
+ //Create a function for SI prefix value sorting
+ $native_connection->createFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, Sqlite::DETERMINISTIC);
+ } else {
+ $native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC);
+ $native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC);
+ $native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC);
+
+ //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);
+ }
}
}
@@ -118,4 +132,4 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
return $index + 1;
}
-}
\ No newline at end of file
+}
From b8fc5d4aceb387f5d30c75328889b2ed81e5bdeb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Thu, 25 Jun 2026 12:07:54 +0200
Subject: [PATCH 06/24] Warn if ProjectBuild passed lot if null
Fixed caused deprecations for it
---
src/Helpers/Projects/ProjectBuildRequest.php | 20 +++++++++++++------
.../Projects/ProjectBuildRequestTest.php | 7 ++++++-
2 files changed, 20 insertions(+), 7 deletions(-)
diff --git a/src/Helpers/Projects/ProjectBuildRequest.php b/src/Helpers/Projects/ProjectBuildRequest.php
index 430d37b5..d2dfe139 100644
--- a/src/Helpers/Projects/ProjectBuildRequest.php
+++ b/src/Helpers/Projects/ProjectBuildRequest.php
@@ -84,8 +84,10 @@ final class ProjectBuildRequest
$remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry);
foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) {
//If the lot has instock use it for the build
- $this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount());
- $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]);
+ $id = $lot->getID() ?? throw new \RuntimeException("Part lot needs to have an ID!");
+
+ $this->withdraw_amounts[$id] = min($remaining_amount, $lot->getAmount());
+ $remaining_amount -= max(0, $this->withdraw_amounts[$id]);
}
}
}
@@ -176,6 +178,10 @@ final class ProjectBuildRequest
{
$lot_id = $lot instanceof PartLot ? $lot->getID() : $lot;
+ if ($lot_id === null) {
+ throw new \InvalidArgumentException('The given lot must have an ID!');
+ }
+
if (! array_key_exists($lot_id, $this->withdraw_amounts)) {
throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!');
}
@@ -192,10 +198,12 @@ final class ProjectBuildRequest
{
if ($lot instanceof PartLot) {
$lot_id = $lot->getID();
- } elseif (is_int($lot)) {
- $lot_id = $lot;
} else {
- throw new \InvalidArgumentException('The given lot must be an instance of PartLot or an ID of a PartLot!');
+ $lot_id = $lot;
+ }
+
+ if ($lot_id === null) {
+ throw new \InvalidArgumentException('The given lot must have an ID!');
}
$this->withdraw_amounts[$lot_id] = $amount;
@@ -296,7 +304,7 @@ final class ProjectBuildRequest
* @param bool $dont_check_quantity
* @return $this
*/
- public function setDontCheckQuantity(bool $dont_check_quantity): ProjectBuildRequest
+ public function setDontCheckQuantity(bool $dont_check_quantity): self
{
$this->dont_check_quantity = $dont_check_quantity;
return $this;
diff --git a/tests/Helpers/Projects/ProjectBuildRequestTest.php b/tests/Helpers/Projects/ProjectBuildRequestTest.php
index c1fd1498..3014f762 100644
--- a/tests/Helpers/Projects/ProjectBuildRequestTest.php
+++ b/tests/Helpers/Projects/ProjectBuildRequestTest.php
@@ -82,7 +82,12 @@ final class ProjectBuildRequestTest extends TestCase
$part2->setName('Part 2');
$part2->setPartUnit($float_unit);
- $this->lot2 = new PartLot();
+ $this->lot2 = new class extends PartLot {
+ public function getID(): ?int
+ {
+ return 3;
+ }
+ };;
$part2->addPartLot($this->lot2);
$this->lot2->setAmount(2.5);
$this->lot2->setDescription('Lot 2');
From eb7da91c4494b7ccda533b602af3fda93ba02075 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Thu, 25 Jun 2026 12:09:06 +0200
Subject: [PATCH 07/24] Fixed phpstan issue
---
.../Middleware/SQLiteRegexExtensionMiddlewareDriver.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php
index fa3230e4..f721f986 100644
--- a/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php
+++ b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php
@@ -48,7 +48,7 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
if($native_connection instanceof \PDO) {
//Use the new PDO::createFunction and PDO::createCollation methods if available (PHP 8.4+)
- if (is_a($native_connection, \PDO\Sqlite::class)) { #TODO: Remove this check when PHP 8.4 is the minimum requirement
+ if (is_a($native_connection, Sqlite::class)) { #TODO: Remove this check when PHP 8.4 is the minimum requirement
$native_connection->createFunction('REGEXP', self::regexp(...), 2, Sqlite::DETERMINISTIC);
$native_connection->createFunction('FIELD', self::field(...), -1, Sqlite::DETERMINISTIC);
$native_connection->createFunction('FIELD2', self::field2(...), 2, Sqlite::DETERMINISTIC);
From 9c919a9be9b263f0c476eebccf579eab1949364d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Thu, 25 Jun 2026 12:12:34 +0200
Subject: [PATCH 08/24] Moved from phpunit annotation to attribute
---
tests/Doctrine/Functions/SiValueSortTest.php | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/tests/Doctrine/Functions/SiValueSortTest.php b/tests/Doctrine/Functions/SiValueSortTest.php
index dbdd9d28..4ef8a397 100644
--- a/tests/Doctrine/Functions/SiValueSortTest.php
+++ b/tests/Doctrine/Functions/SiValueSortTest.php
@@ -26,6 +26,7 @@ use App\Doctrine\Functions\SiValueSort;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
+use PHPUnit\Framework\Attributes\DataProvider;
final class SiValueSortTest extends AbstractDoctrineFunctionTestCase
{
@@ -71,9 +72,7 @@ final class SiValueSortTest extends AbstractDoctrineFunctionTestCase
$this->assertSame('SI_VALUE(part_name)', $sql);
}
- /**
- * @dataProvider sqliteSiValueProvider
- */
+ #[DataProvider('sqliteSiValueProvider')]
public function testSqliteSiValue(?string $input, ?float $expected): void
{
$result = SiValueSort::sqliteSiValue($input);
From 23e22b19e21e52c35cc7d1f3b74cc8957c4a9d55 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Thu, 25 Jun 2026 12:27:37 +0200
Subject: [PATCH 09/24] Removed usesless easy_log_handler file
---
config/packages/dev/easy_log_handler.yaml | 16 ----------------
1 file changed, 16 deletions(-)
delete mode 100644 config/packages/dev/easy_log_handler.yaml
diff --git a/config/packages/dev/easy_log_handler.yaml b/config/packages/dev/easy_log_handler.yaml
deleted file mode 100644
index 27bfc608..00000000
--- a/config/packages/dev/easy_log_handler.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-services:
- EasyCorp\EasyLog\EasyLogHandler:
- public: false
- arguments: ['%kernel.logs_dir%/%kernel.environment%.log']
-
-#// FIXME: How to add this configuration automatically without messing up with the monolog configuration?
-#monolog:
-# handlers:
-# buffered:
-# type: buffer
-# handler: easylog
-# channels: ['!event']
-# level: debug
-# easylog:
-# type: service
-# id: EasyCorp\EasyLog\EasyLogHandler
From b3895c1e91287d23a7984750f539242901f6e412 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Thu, 25 Jun 2026 12:33:52 +0200
Subject: [PATCH 10/24] Fixed depprecation of fluent config builders
---
config/packages/doctrine.php | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/config/packages/doctrine.php b/config/packages/doctrine.php
index e5be011f..f62cdcfb 100644
--- a/config/packages/doctrine.php
+++ b/config/packages/doctrine.php
@@ -20,16 +20,16 @@
declare(strict_types=1);
-use Symfony\Config\DoctrineConfig;
-
/**
- * This class extends the default doctrine ORM configuration to enable native lazy objects on PHP 8.4+.
+ * This file enables native lazy objects on PHP 8.4+.
* We have to do this in a PHP file, because the yaml file does not support conditionals on PHP version.
+ *
+ * TODO: Remove this file when we drop support for PHP < 8.4
*/
-return static function(DoctrineConfig $doctrine) {
- //On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies.
- if (PHP_VERSION_ID >= 80400) {
- $doctrine->orm()->enableNativeLazyObjects(true);
- }
-};
+// On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies.
+if (PHP_VERSION_ID >= 80400) {
+ return ['doctrine' => ['orm' => ['enable_native_lazy_objects' => true]]];
+}
+
+return [];
From cd87b59c15b201079b6245d5a2214d5d95f40ac9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Thu, 25 Jun 2026 12:37:22 +0200
Subject: [PATCH 11/24] Fixed small deprecations
---
src/Entity/ProjectSystem/Project.php | 2 +-
src/Form/Type/AttachmentTypeType.php | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Entity/ProjectSystem/Project.php b/src/Entity/ProjectSystem/Project.php
index a103d694..f4a3e9de 100644
--- a/src/Entity/ProjectSystem/Project.php
+++ b/src/Entity/ProjectSystem/Project.php
@@ -117,7 +117,7 @@ class Project extends AbstractStructuralDBElement
/**
* @var string|null The current status of the project
*/
- #[Assert\Choice(['draft', 'planning', 'in_production', 'finished', 'archived'])]
+ #[Assert\Choice(choices: ['draft', 'planning', 'in_production', 'finished', 'archived'])]
#[Groups(['extended', 'full', 'project:read', 'project:write', 'import'])]
#[ORM\Column(type: Types::STRING, length: 64, nullable: true)]
protected ?string $status = null;
diff --git a/src/Form/Type/AttachmentTypeType.php b/src/Form/Type/AttachmentTypeType.php
index 099ed282..95b5a254 100644
--- a/src/Form/Type/AttachmentTypeType.php
+++ b/src/Form/Type/AttachmentTypeType.php
@@ -38,7 +38,7 @@ class AttachmentTypeType extends AbstractType
return StructuralEntityType::class;
}
- public function configureOptions(OptionsResolver $resolver)
+ public function configureOptions(OptionsResolver $resolver): void
{
$resolver->define('attachment_filter_class')->allowedTypes('null', 'string')->default(null);
From 188444b30f92cdd3990d5c72b53673e28a1f3cbc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Thu, 25 Jun 2026 12:44:06 +0200
Subject: [PATCH 12/24] Fixed deprecations
---
.../details/_extra_collection_element_deleted.html.twig | 4 ++--
templates/parts/info/_picture.html.twig | 4 ++--
templates/parts/info/show_part_info.html.twig | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/templates/log_system/details/_extra_collection_element_deleted.html.twig b/templates/log_system/details/_extra_collection_element_deleted.html.twig
index 221fae95..1c2ce2f3 100644
--- a/templates/log_system/details/_extra_collection_element_deleted.html.twig
+++ b/templates/log_system/details/_extra_collection_element_deleted.html.twig
@@ -4,7 +4,7 @@
{% trans %}log.collection_deleted.deleted{% endtrans %}:
- {{ entity_type_label(entry.deletedElementClass) }} #{{ entry.deletedElementID }}
+ {{ type_label(entry.deletedElementClass) }} #{{ entry.deletedElementID }}
{% if entry.oldName is not empty %}
({{ entry.oldName }})
{% endif %}
@@ -12,4 +12,4 @@
{% trans %}log.collection_deleted.on_collection{% endtrans %}:
{{ log_helper.translate_field(entry.collectionName) }}
-
\ No newline at end of file
+
diff --git a/templates/parts/info/_picture.html.twig b/templates/parts/info/_picture.html.twig
index e6aa74b3..db6c59ca 100644
--- a/templates/parts/info/_picture.html.twig
+++ b/templates/parts/info/_picture.html.twig
@@ -19,7 +19,7 @@
{{ pic.name }}
{% if pic.filename %}({{ pic.filename }}) {% endif %}
-
{{ entity_type_label(pic.element) }}
+
{{ type_label(pic.element) }}
{% endif %}
@@ -41,4 +41,4 @@
{% else %}
-{% endif %}
\ No newline at end of file
+{% endif %}
diff --git a/templates/parts/info/show_part_info.html.twig b/templates/parts/info/show_part_info.html.twig
index 96b5e209..b36ab047 100644
--- a/templates/parts/info/show_part_info.html.twig
+++ b/templates/parts/info/show_part_info.html.twig
@@ -22,7 +22,7 @@
({{ timeTravel | format_datetime('short') }})
{% endif %}
{% if part.projectBuildPart %}
- ({{ entity_type_label(part.builtProject) }}: {{ part.builtProject.name }})
+ ({{ type_label(part.builtProject) }}: {{ part.builtProject.name }})
{% endif %}
From 0eba7121aa83d24bafa34e93c6dc388e550cf958 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20B=C3=B6hmer?=
Date: Thu, 25 Jun 2026 20:16:44 +0200
Subject: [PATCH 13/24] Merged subchildren operations on APIPlatform into one
APIResource, as havinng multiple is deprecated
---
src/Entity/Attachments/AttachmentType.php | 17 ++++++-----------
src/Entity/Parts/Category.php | 19 ++++++-------------
src/Entity/Parts/Footprint.php | 19 ++++++-------------
src/Entity/Parts/Manufacturer.php | 19 ++++++-------------
src/Entity/Parts/MeasurementUnit.php | 19 ++++++-------------
src/Entity/Parts/StorageLocation.php | 19 ++++++-------------
src/Entity/Parts/Supplier.php | 17 ++++++-----------
src/Entity/PriceInformations/Currency.php | 19 ++++++-------------
src/Entity/PriceInformations/Orderdetail.php | 20 +++++++-------------
src/Entity/ProjectSystem/Project.php | 19 ++++++-------------
src/Entity/ProjectSystem/ProjectBOMEntry.php | 19 ++++++-------------
11 files changed, 67 insertions(+), 139 deletions(-)
diff --git a/src/Entity/Attachments/AttachmentType.php b/src/Entity/Attachments/AttachmentType.php
index 7a314ffe..03bb8031 100644
--- a/src/Entity/Attachments/AttachmentType.php
+++ b/src/Entity/Attachments/AttachmentType.php
@@ -65,21 +65,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
+ new GetCollection(
+ uriTemplate: '/attachment_types/{id}/children.{_format}',
+ uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: AttachmentType::class)],
+ openapi: new Operation(summary: 'Retrieves the children elements of an attachment type.'),
+ security: 'is_granted("@attachment_types.read")'
+ ),
],
normalizationContext: ['groups' => ['attachment_type:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['attachment_type:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)]
-#[ApiResource(
- uriTemplate: '/attachment_types/{id}/children.{_format}',
- operations: [
- new GetCollection(openapi: new Operation(summary: 'Retrieves the children elements of an attachment type.'),
- security: 'is_granted("@attachment_types.read")')
- ],
- uriVariables: [
- 'id' => new Link(fromProperty: 'children', fromClass: AttachmentType::class)
- ],
- normalizationContext: ['groups' => ['attachment_type:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
-)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
diff --git a/src/Entity/Parts/Category.php b/src/Entity/Parts/Category.php
index 22f8a3e4..80fcc43c 100644
--- a/src/Entity/Parts/Category.php
+++ b/src/Entity/Parts/Category.php
@@ -68,23 +68,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
+ new GetCollection(
+ uriTemplate: '/categories/{id}/children.{_format}',
+ uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Category::class)],
+ openapi: new Operation(summary: 'Retrieves the children elements of a category.'),
+ security: 'is_granted("@categories.read")'
+ ),
],
normalizationContext: ['groups' => ['category:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['category:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)]
-#[ApiResource(
- uriTemplate: '/categories/{id}/children.{_format}',
- operations: [
- new GetCollection(
- openapi: new Operation(summary: 'Retrieves the children elements of a category.'),
- security: 'is_granted("@categories.read")'
- )
- ],
- uriVariables: [
- 'id' => new Link(fromProperty: 'children', fromClass: Category::class)
- ],
- normalizationContext: ['groups' => ['category:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
-)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
diff --git a/src/Entity/Parts/Footprint.php b/src/Entity/Parts/Footprint.php
index 3d8be686..2027a310 100644
--- a/src/Entity/Parts/Footprint.php
+++ b/src/Entity/Parts/Footprint.php
@@ -67,23 +67,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
+ new GetCollection(
+ uriTemplate: '/footprints/{id}/children.{_format}',
+ uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Footprint::class)],
+ openapi: new Operation(summary: 'Retrieves the children elements of a footprint.'),
+ security: 'is_granted("@footprints.read")'
+ ),
],
normalizationContext: ['groups' => ['footprint:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['footprint:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)]
-#[ApiResource(
- uriTemplate: '/footprints/{id}/children.{_format}',
- operations: [
- new GetCollection(
- openapi: new Operation(summary: 'Retrieves the children elements of a footprint.'),
- security: 'is_granted("@footprints.read")'
- )
- ],
- uriVariables: [
- 'id' => new Link(fromProperty: 'children', fromClass: Footprint::class)
- ],
- normalizationContext: ['groups' => ['footprint:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
-)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
diff --git a/src/Entity/Parts/Manufacturer.php b/src/Entity/Parts/Manufacturer.php
index 0edf8232..76526c31 100644
--- a/src/Entity/Parts/Manufacturer.php
+++ b/src/Entity/Parts/Manufacturer.php
@@ -66,23 +66,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
+ new GetCollection(
+ uriTemplate: '/manufacturers/{id}/children.{_format}',
+ uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class)],
+ openapi: new Operation(summary: 'Retrieves the children elements of a manufacturer.'),
+ security: 'is_granted("@manufacturers.read")'
+ ),
],
normalizationContext: ['groups' => ['manufacturer:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['manufacturer:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)]
-#[ApiResource(
- uriTemplate: '/manufacturers/{id}/children.{_format}',
- operations: [
- new GetCollection(
- openapi: new Operation(summary: 'Retrieves the children elements of a manufacturer.'),
- security: 'is_granted("@manufacturers.read")'
- )
- ],
- uriVariables: [
- 'id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class)
- ],
- normalizationContext: ['groups' => ['manufacturer:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
-)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
diff --git a/src/Entity/Parts/MeasurementUnit.php b/src/Entity/Parts/MeasurementUnit.php
index 6dd0b9f2..e775b65b 100644
--- a/src/Entity/Parts/MeasurementUnit.php
+++ b/src/Entity/Parts/MeasurementUnit.php
@@ -71,23 +71,16 @@ use Symfony\Component\Validator\Constraints\Length;
new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
+ new GetCollection(
+ uriTemplate: '/measurement_units/{id}/children.{_format}',
+ uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: MeasurementUnit::class)],
+ openapi: new Operation(summary: 'Retrieves the children elements of a MeasurementUnit.'),
+ security: 'is_granted("@measurement_units.read")'
+ ),
],
normalizationContext: ['groups' => ['measurement_unit:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['measurement_unit:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)]
-#[ApiResource(
- uriTemplate: '/measurement_units/{id}/children.{_format}',
- operations: [
- new GetCollection(
- openapi: new Operation(summary: 'Retrieves the children elements of a MeasurementUnit.'),
- security: 'is_granted("@measurement_units.read")'
- )
- ],
- uriVariables: [
- 'id' => new Link(fromProperty: 'children', fromClass: MeasurementUnit::class)
- ],
- normalizationContext: ['groups' => ['measurement_unit:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
-)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "unit"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
diff --git a/src/Entity/Parts/StorageLocation.php b/src/Entity/Parts/StorageLocation.php
index 7ba400d9..9571da6e 100644
--- a/src/Entity/Parts/StorageLocation.php
+++ b/src/Entity/Parts/StorageLocation.php
@@ -67,23 +67,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
+ new GetCollection(
+ uriTemplate: '/storage_locations/{id}/children.{_format}',
+ uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: StorageLocation::class)],
+ openapi: new Operation(summary: 'Retrieves the children elements of a storage location.'),
+ security: 'is_granted("@storelocations.read")'
+ ),
],
normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['location:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)]
-#[ApiResource(
- uriTemplate: '/storage_locations/{id}/children.{_format}',
- operations: [
- new GetCollection(
- openapi: new Operation(summary: 'Retrieves the children elements of a storage location.'),
- security: 'is_granted("@storelocations.read")'
- )
- ],
- uriVariables: [
- 'id' => new Link(fromProperty: 'children', fromClass: StorageLocation::class)
- ],
- normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
-)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
diff --git a/src/Entity/Parts/Supplier.php b/src/Entity/Parts/Supplier.php
index 2c004e9e..75cf62d1 100644
--- a/src/Entity/Parts/Supplier.php
+++ b/src/Entity/Parts/Supplier.php
@@ -71,21 +71,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
+ new GetCollection(
+ uriTemplate: '/suppliers/{id}/children.{_format}',
+ uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Supplier::class)],
+ openapi: new Operation(summary: 'Retrieves the children elements of a supplier.'),
+ security: 'is_granted("@manufacturers.read")'
+ ),
],
normalizationContext: ['groups' => ['supplier:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['supplier:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)]
-#[ApiResource(
- uriTemplate: '/suppliers/{id}/children.{_format}',
- operations: [new GetCollection(
- openapi: new Operation(summary: 'Retrieves the children elements of a supplier.'),
- security: 'is_granted("@manufacturers.read")'
- )],
- uriVariables: [
- 'id' => new Link(fromProperty: 'children', fromClass: Supplier::class)
- ],
- normalizationContext: ['groups' => ['supplier:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
-)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
diff --git a/src/Entity/PriceInformations/Currency.php b/src/Entity/PriceInformations/Currency.php
index 4a811aa0..507ec810 100644
--- a/src/Entity/PriceInformations/Currency.php
+++ b/src/Entity/PriceInformations/Currency.php
@@ -71,23 +71,16 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
+ new GetCollection(
+ uriTemplate: '/currencies/{id}/children.{_format}',
+ uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Currency::class)],
+ openapi: new Operation(summary: 'Retrieves the children elements of a currency.'),
+ security: 'is_granted("@currencies.read")'
+ ),
],
normalizationContext: ['groups' => ['currency:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['currency:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)]
-#[ApiResource(
- uriTemplate: '/currencies/{id}/children.{_format}',
- operations: [
- new GetCollection(
- openapi: new Operation(summary: 'Retrieves the children elements of a currency.'),
- security: 'is_granted("@currencies.read")'
- )
- ],
- uriVariables: [
- 'id' => new Link(fromProperty: 'children', fromClass: Currency::class)
- ],
- normalizationContext: ['groups' => ['currency:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
-)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "iso_code"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
diff --git a/src/Entity/PriceInformations/Orderdetail.php b/src/Entity/PriceInformations/Orderdetail.php
index 56428e3a..8b5c9f7b 100644
--- a/src/Entity/PriceInformations/Orderdetail.php
+++ b/src/Entity/PriceInformations/Orderdetail.php
@@ -71,23 +71,17 @@ use Symfony\Component\Validator\Constraints\Length;
new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
+ new GetCollection(
+ uriTemplate: '/parts/{id}/orderdetails.{_format}',
+ uriVariables: ['id' => new Link(toProperty: 'part', fromClass: Part::class)],
+ normalizationContext: ['groups' => ['orderdetail:read', 'pricedetail:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
+ openapi: new Operation(summary: 'Retrieves the orderdetails of a part.'),
+ security: 'is_granted("@parts.read")'
+ ),
],
normalizationContext: ['groups' => ['orderdetail:read', 'orderdetail:read:standalone', 'api:basic:read', 'pricedetail:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['orderdetail:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
)]
-#[ApiResource(
- uriTemplate: '/parts/{id}/orderdetails.{_format}',
- operations: [
- new GetCollection(
- openapi: new Operation(summary: 'Retrieves the orderdetails of a part.'),
- security: 'is_granted("@parts.read")'
- )
- ],
- uriVariables: [
- 'id' => new Link(toProperty: 'part', fromClass: Part::class)
- ],
- normalizationContext: ['groups' => ['orderdetail:read', 'pricedetail:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
-)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["supplierpartnr", "supplier_product_url"])]
diff --git a/src/Entity/ProjectSystem/Project.php b/src/Entity/ProjectSystem/Project.php
index f4a3e9de..34e77051 100644
--- a/src/Entity/ProjectSystem/Project.php
+++ b/src/Entity/ProjectSystem/Project.php
@@ -66,23 +66,16 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
+ new GetCollection(
+ uriTemplate: '/projects/{id}/children.{_format}',
+ uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Project::class)],
+ openapi: new Operation(summary: 'Retrieves the children elements of a project.'),
+ security: 'is_granted("@projects.read")'
+ ),
],
normalizationContext: ['groups' => ['project:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['project:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)]
-#[ApiResource(
- uriTemplate: '/projects/{id}/children.{_format}',
- operations: [
- new GetCollection(
- openapi: new Operation(summary: 'Retrieves the children elements of a project.'),
- security: 'is_granted("@projects.read")'
- )
- ],
- uriVariables: [
- 'id' => new Link(fromProperty: 'children', fromClass: Project::class)
- ],
- normalizationContext: ['groups' => ['project:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
-)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])]
diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php
index 2a7862ec..c016d741 100644
--- a/src/Entity/ProjectSystem/ProjectBOMEntry.php
+++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php
@@ -63,23 +63,16 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
new Post(uriTemplate: '/project_bom_entries.{_format}', securityPostDenormalize: 'is_granted("create", object)',),
new Patch(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("edit", object)',),
new Delete(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("delete", object)',),
+ new GetCollection(
+ uriTemplate: '/projects/{id}/bom.{_format}',
+ uriVariables: ['id' => new Link(fromProperty: 'bom_entries', fromClass: Project::class)],
+ openapi: new Operation(summary: 'Retrieves the BOM entries of the given project.'),
+ security: 'is_granted("@projects.read")'
+ ),
],
normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['bom_entry:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
)]
-#[ApiResource(
- uriTemplate: '/projects/{id}/bom.{_format}',
- operations: [
- new GetCollection(
- openapi: new Operation(summary: 'Retrieves the BOM entries of the given project.'),
- security: 'is_granted("@projects.read")'
- )
- ],
- uriVariables: [
- 'id' => new Link(fromProperty: 'bom_entries', fromClass: Project::class)
- ],
- normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
-)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])]
#[ApiFilter(RangeFilter::class, properties: ['quantity'])]
From 2503e63bb24865cb6615bf14fb2bf0b31d4db435 Mon Sep 17 00:00:00 2001
From: 0x915
Date: Fri, 26 Jun 2026 02:41:55 +0800
Subject: [PATCH 14/24] Perform a full translation of Simplified Chinese.
(#1426)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Perform a full translation of Simplified Chinese.
Based on the latest *.en.xlf file.
Translated automatically using DeepSinkV4 based on the source code,
then manually reviewed and corrected line by line.
* Normalized formatting
---------
Co-authored-by: Jan Böhmer
---
translations/frontend.zh.xlf | 12 +-
translations/messages.zh.xlf | 8986 +++++++++++++++++++++++---------
translations/security.zh.xlf | 6 +-
translations/validators.zh.xlf | 80 +-
4 files changed, 6592 insertions(+), 2492 deletions(-)
diff --git a/translations/frontend.zh.xlf b/translations/frontend.zh.xlf
index 7425b1e8..bdb053d6 100644
--- a/translations/frontend.zh.xlf
+++ b/translations/frontend.zh.xlf
@@ -10,7 +10,7 @@
part.labelp
- 部件
+ 物料
@@ -34,7 +34,7 @@
user.password_strength.medium
- 中
+ 中等
@@ -52,9 +52,15 @@
search.submit
- GO!
+ 搜索
+
+
+ user.password_strength.crack_time
+ 预计破解时间:%time%
+
+
dialog.btn.ok
diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf
index fde08cfe..7083c3ff 100644
--- a/translations/messages.zh.xlf
+++ b/translations/messages.zh.xlf
@@ -1,49 +1,49 @@
-
+
-
+
attachment_type.caption
- 附件类型
+ 附件的文件类型
-
+
new
attachment_type.edit
- 编辑文件类型
+ 编辑附件类型
-
+
new
attachment_type.new
- 新建类型
+ 新建附件类型
-
+
category.labelp
类别
-
+
admin.options
选项
-
+
admin.advanced
高级
-
+
new
@@ -52,7 +52,7 @@
编辑类别
-
+
new
@@ -61,25 +61,19 @@
新建类别
-
-
- currency.caption
- 货币
-
-
-
+
currency.iso_code.caption
- 货币ISO代码
+ ISO代码
-
+
currency.symbol.caption
货币符号
-
+
new
@@ -88,7 +82,7 @@
编辑货币
-
+
new
@@ -97,7 +91,7 @@
新建货币
-
+
new
@@ -106,7 +100,7 @@
编辑项目
-
+
new
@@ -115,118 +109,118 @@
新建项目
-
+
search.placeholder
搜索
-
+
expandAll
全部展开
-
+
reduceAll
全部收起
-
-
+
+
part.info.timetravel_hint
- 在 %timestamp% 之前,部件是这样显示的。 <i>请注意,这是试验性功能,信息可能不正确。</i>
+ 这是物料在 %timestamp% 之前的数据。<i>请注意,此功能是实验性的,信息可能不正确。</i>
-
+
standard.label
属性
-
+
infos.label
信息
-
+
new
-
+
history.label
历史
-
+
export.label
导出
-
+
import_export.label
导入/导出
-
+
mass_creation.label
- 大量创建
+ 批量创建
-
+
admin.common
- 基本
+ 通用
-
+
admin.attachments
附件
-
+
admin.parameters
参数
-
+
export_all.label
导出所有元素
-
+
mass_creation.help
- 每一行将被解析为新建元素的名称。可以通过缩进创建嵌套元素。
+ 每行将被解释为要创建的元素的名称。可以通过缩进创建嵌套结构。
-
-
+
+
edit.caption
- 编辑元素 "%name"
+ 编辑元素%name%
-
-
+
+
new.caption
- 新建元素
+ 新元素
-
+
footprint.labelp
封装
-
+
new
@@ -235,7 +229,7 @@
编辑封装
-
+
new
@@ -244,79 +238,61 @@
新建封装
-
-
- group.edit.caption
- 组
-
-
-
+
user.edit.permissions
权限
-
+
new
group.edit
- 编辑组
+ 编辑用户组
-
+
new
group.new
- 新建组
+ 新建用户组
-
-
- label_profile.caption
- 标签配置
-
-
-
+
label_profile.advanced
高级
-
+
label_profile.comment
- 注释
+ 备注
-
+
new
label_profile.edit
- 编辑标签配置
+ 编辑标签模板
-
+
new
label_profile.new
- 新建标签配置
+ 新建标签模板
-
-
- manufacturer.caption
- 制造商
-
-
-
+
new
@@ -325,7 +301,7 @@
编辑制造商
-
+
new
@@ -334,43 +310,31 @@
新建制造商
-
-
- measurement_unit.caption
- 计量单位
-
-
-
-
- part_custom_state.caption
- 部件的自定义状态
-
-
-
+
storelocation.labelp
- 储存位置
+ 存储位置
-
+
new
storelocation.edit
- 编辑存储位置
+ 编辑储存位置
-
+
new
storelocation.new
- 新建存储位置
+ 新建储存位置
-
+
new
@@ -379,7 +343,7 @@
编辑供应商
-
+
new
@@ -388,82 +352,73 @@
新建供应商
-
-
- user.edit.caption
- Users
-
-
-
+
user.edit.configuration
配置
-
+
user.edit.password
密码
-
+
user.edit.tfa.caption
- 2FA身份验证
+ 双因素认证
-
+
user.edit.tfa.google_active
- 身份验证器应用处于活动状态
+ 身份验证器应用已激活
-
+
tfa_backup.remaining_tokens
- 剩余备份代码计数
+ 剩余备份代码数量
-
+
tfa_backup.generation_date
备份代码的生成日期
-
+
user.edit.tfa.disabled
方法未启用
-
+
user.edit.tfa.u2f_keys_count
- 主动安全密钥
+ 活跃的安全密钥
-
+
user.edit.tfa.disable_tfa_title
- 确定继续?
+ 确定要继续?
-
+
user.edit.tfa.disable_tfa_message
- 这将禁用 <b>账号的所有活动2FA身份验证方法</b> 并删除 <b>备份代码</b>!
-<br>
-该账号将必须重新设置所有2FA身份验证方法并获取新的备份代码。 <br><br>
-<b>应在完全确定 申请者身份 时才执行此操作,否则该账号可能会受到攻击。</b>
+ 这将<b>禁用用户所有活动的双因素认证方法</b>并删除<b>备份代码</b>!<br>用户将需要重新设置所有双因素认证方法并打印新的备份代码!<br><br><b>只有在绝对确定用户身份(寻求帮助)的情况下才执行此操作,否则账号可能被攻击者入侵!</b>
-
+
user.edit.tfa.disable_tfa.btn
- 禁用所有2FA身份验证方法
+ 禁用所有双因素认证方法
-
+
new
@@ -472,7 +427,7 @@
编辑用户
-
+
new
@@ -481,1347 +436,1363 @@
新建用户
-
+
attachment.delete
删除
-
+
- attachment.external
- 外部
+ attachment.external_only
+ 仅外部
-
+
attachment.preview.alt
附件缩略图
-
+
- attachment.view
- 查看
+ attachment.view_local
+ 查看本地副本
-
+
attachment.file_not_found
- 文件未找到
+ 未找到文件
-
+
attachment.secure
私有附件
-
+
attachment.create
- 添加附件
+ 增加附件
-
+
part_lot.edit.delete.confirm
- 确认删除 该库存 ? 该操作不能被撤消
+ 确定要删除此库存?此操作无法撤消!
-
+
entity.delete.confirm_title
- 确定删除 %name%?
+ 确定要删除%name%?
-
+
entity.delete.message
- 该操作不能被撤销
-<br>
-子元素将向上移动。
+ 此操作无法撤消!<br>子元素将向上移动</br>。
-
+
entity.delete
- Delete element
+ 删除元素
-
+
new
-
+
edit.log_comment
- 更改评论
+ 更改备注
-
+
entity.delete.recursive
- 递归删除(所有子元素)
+ 递归删除(所有子元素)
-
+
entity.duplicate
- 重复元素
+ 复制此元素并新建
-
+
export.format
文件格式
-
+
export.level
详细程度
-
+
export.level.simple
简单
-
+
export.level.extended
扩展
-
+
export.level.full
- 完全
+ 完整
-
+
export.include_children
- 在导出中包含子元素
+ 导出时包含子元素
-
+
export.btn
导出
-
+
id.label
ID
-
+
createdAt
- 创建于
+ 创建时间
-
+
lastModified
- 上次修改
+ 最后修改时间
-
+
entity.info.parts_count
- 具有该元素的部件数量
+ 具有此元素的物料数量
-
+
specifications.property
参数
-
+
specifications.symbol
符号
-
+
specifications.value_min
- 最小.
+ 最小
-
+
specifications.value_typ
- 标称.
+ 标称
-
+
specifications.value_max
- 最大.
+ 最大
-
+
specifications.unit
单位
-
+
specifications.text
文本
-
+
specifications.group
- Group
+ 用户组
-
+
+
+ specifications.eda_visibility.help
+ 将此参数导出为EDA字段
+
+
+
specification.create
- 新建参数
+ 新参数
-
+
parameter.delete.confirm
- 确实删除该参数?
+ 确定要删除此参数?
-
+
attachment.list.title
附件列表
-
+
part_list.loading.caption
- 加载中...
+ 加载中
-
+
part_list.loading.message
- 这可能需要一些时间。如果此消息没有消失,请尝试重新加载页面。
+ 这可能需要等待一些时间。如果此消息未消失请尝试重新加载页面。
-
+
vendor.base.javascript_hint
- 需要激活 Javascript 才能使用所有功能
+ 请启用Javascript以使用所有功能!
-
+
sidebar.big.toggle
- 显示/隐藏 侧边栏
+ 显示/隐藏侧边栏
-
+
loading.caption
- 加载中:
+ 加载中:
-
+
loading.message
- 这可能需要一段时间。如果此消息长时间存在,请尝试重新加载页面。
+ 这可能需要等待一段时间。如果此消息长时间存在请尝试重新加载页面。
-
+
loading.bar
加载中...
-
+
back_to_top
返回页面顶部
-
+
permission.edit.permission
权限
-
+
permission.edit.value
- 配置
+ 值
-
+
permission.legend.title
状态说明
-
+
permission.legend.disallow
禁止
-
+
permission.legend.allow
- 允许
+ 已允许
-
+
permission.legend.inherit
- 继承
+ 从父级用户组继承
-
+
bool.true
- TRUE
+ true
-
+
bool.false
- FALSE
+ false
-
+
Yes
YES
-
+
No
NO
-
+
specifications.value
值
-
+
version.caption
版本
-
+
homepage.license
- 许可信息
+ 许可证信息
-
+
homepage.github.caption
项目页面
-
-
+
+
homepage.github.text
- 源代码、下载、错误报告、待办事项列表等可以在 <a href="%href%" class="link-external" target="_blank">项目仓库</a> 上找到。
+ 源代码、下载、错误报告、待办列表等可在 <a href="%href%"class="link-external"target="_blank">GitHub项目页面</a> 上找到
-
+
homepage.help.caption
帮助
-
+
homepage.help.text
- 帮助和提示可以在 <a href="%href%" class="link-external" target="_blank">文档</a>中找到。
+ 帮助和技巧可在 Wiki 和 <a href="%href%"class="link-external"target="_blank">GitHub页面</a> 中找到
-
+
homepage.forum.caption
论坛
-
+
new
homepage.last_activity
- 最近的事件
+ 最近活跃事件
-
+
label_generator.title
标签生成器
-
-
+
+
label_generator.common
- 基本
+ 通用
-
+
label_generator.advanced
高级
-
+
label_generator.profiles
- 标签配置
+ 模板
-
+
label_generator.selected_profile
- 当前选择的配置
+ 当前选中的模板
-
+
label_generator.edit_profile
- 编辑配置
+ 编辑模板
-
+
label_generator.load_profile
- 载入配置
+ 加载模板
-
+
label_generator.download
下载
-
+
label_generator.label_btn
生成标签
-
+
label_generator.label_empty
- 新建空标签
+ 新的空白标签
-
+
label_scanner.title
标签扫描器
-
+
label_scanner.no_cam_found.title
- 未找到摄像头
+ 未找到网络摄像头
-
+
label_scanner.no_cam_found.text
- 您需要一个摄像头并授予权限。或在下面手动输入条形码。
+ 需要网络摄像头并授权。也可以在下方手动输入条码。
-
+
label_scanner.source_select
选择源
-
-
+
+
log.list.title
系统日志
-
+
new
-
+
log.undo.confirm_title
- 确定撤消更改/恢复到时间戳?
+ 确实要撤消更改/恢复到时间戳?
-
+
new
-
+
log.undo.confirm_message
- 确定撤消给定的更改/将元素重置到给定的时间戳?
+ 确定要撤消该更改/将元素重置到该时间戳?
-
+
mail.footer.email_sent_by
- 这封电子邮件是自动发送的,由
+ 此邮件由以下系统自动发送
-
+
mail.footer.dont_reply
- 不要回复此电子邮件。
+ 请勿回复此邮件。
-
+
email.hi %name%
你好 %name%
-
+
email.pw_reset.message
- 有人请求重置您的密码。 如果此请求不是您提出的,请忽略此邮件。
+ 重置密码请求。如果对此请求不知情,请忽略此邮件。
-
+
email.pw_reset.button
- 单击此处重置密码
+ 点击重置密码
-
+
email.pw_reset.fallback
- 如果这对您不起作用,请转到 <a href="%url%">%url%</a> 并输入以下信息
+ 如果按钮不起作用,请访问<a href="%url%">%url%</a>并输入以下信息
-
+
email.pw_reset.username
用户名
-
+
email.pw_reset.token
- Token
+ 令牌
-
+
email.pw_reset.valid_unit %date%
- 重置令牌将在 <i>%date%</i> 之前有效。
+ 重置令牌有效期至 <i>%date%</i> 。
-
+
orderdetail.delete
- Delete
+ 删除
-
+
pricedetails.edit.min_qty
- 最低折扣数量
+ 最小折扣数量
-
+
pricedetails.edit.price
价格
-
+
pricedetails.edit.price_qty
数量
-
+
pricedetail.create
- 添加价格
+ 增加价格
-
+
part.edit.title
- 编辑部件
+ 编辑物料
-
+
part.edit.card_title
- 编辑部件
+ 编辑物料
-
+
+
+ form.dirty_form.unsaved_changes.title
+ 未保存的更改
+
+
+
+
+ form.dirty_form.unsaved_changes.message
+ 有未保存的更改,如果离开此页面更改将丢弃。确定要继续?
+
+
+
part.edit.tab.common
- 基础
+ 通用
-
+
part.edit.tab.manufacturer
制造商
-
+
part.edit.tab.advanced
高级
-
+
part.edit.tab.advanced.ipn.commonSectionHeader
- Sugestie bez zwiększenia części
+ 不包含物料增量的建议
-
+
part.edit.tab.advanced.ipn.partIncrementHeader
- 包含部件数值增量的建议
+ 包含数字物料增量的建议
-
+
part.edit.tab.advanced.ipn.prefix.description.current-increment
- 部件的当前IPN规格
+ 物料的当前IPN规范
-
+
part.edit.tab.advanced.ipn.prefix.description.increment
- 基于相同部件描述的下一个可能的IPN规格
+ 基于相同物料描述的下一个可能的IPN规范
-
+
part.edit.tab.advanced.ipn.prefix_empty.direct_category
- 直接类别的 IPN 前缀为空,请在类别“%name%”中指定。
+ 直接类别的IPN前缀为空,请在类别%name%中指定
-
+
part.edit.tab.advanced.ipn.prefix.direct_category
直接类别的IPN前缀
-
+
part.edit.tab.advanced.ipn.prefix.direct_category.increment
- 直接类别的IPN前缀和部件特定的增量
+ 直接类别的IPN前缀和物料特定增量
-
+
part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment
- 具有父级前缀层级类别顺序的IPN前缀
+ 具有父前缀层次结构的IPN前缀
-
+
part.edit.tab.advanced.ipn.prefix.hierarchical.increment
- 具有父级前缀层级类别顺序和组件特定增量的IPN前缀
+ 具有父前缀层次结构和物料特定增量的IPN前缀
-
+
part.edit.tab.advanced.ipn.prefix.not_saved
- 请先创建组件并将其分配到类别:基于现有类别及其专属的IPN前缀,可以自动建议组件的IPN
+ 请先创建物料并分配类别:利用现有的类别及其各自的IPN前缀,系统会自动为该物料建议IPN。
-
+
part.edit.tab.part_lots
库存
-
+
part.edit.tab.attachments
附件
-
+
part.edit.tab.orderdetails
采购信息
-
+
part.edit.tab.specifications
参数
-
+
part.edit.tab.comment
- 注释
+ 备注
-
+
part.new.card_title
- 创建新部件
+ 创建新物料
-
+
part_lot.delete
删除
-
+
part_lot.create
增加库存
-
+
orderdetail.create
- 添加经销商
+ 增加经销商
-
+
pricedetails.edit.delete.confirm
- 确实删除此价格? 该操作不能被撤消
+ 确定要删除此价格?此操作无法撤消。
-
+
orderdetails.edit.delete.confirm
- 确实要删除此经销商信息? 该操作不能被撤消
+ 确定要删除此经销商信息?此操作无法撤消!
-
+
part.info.title
- 部件的详细信息
+ 物料详细信息
-
+
part.part_lots.label
库存
-
+
comment.label
- 注释
+ 备注
-
+
part.info.specifications
参数
-
+
attachment.labelp
附件
-
+
vendor.partinfo.shopping_infos
- 采购信息
+ 购物信息
-
+
vendor.partinfo.history
历史
-
+
tools.label
工具
-
+
extended_info.label
扩展信息
-
+
attachment.name
名称
-
+
attachment.attachment_type
附件类型
-
+
attachment.file_name
文件名
-
+
attachment.file_size
文件大小
-
+
attachment.preview
预览图片
-
+
- attachment.download
- 下载
+ attachment.download_local
+ 下载本地副本
-
+
new
-
+
user.creating_user
- 创建此部件的用户
+ 创建此物料的用户
-
+
Unknown
未知
-
+
new
-
+
accessDenied
- 拒绝访问
+ 访问被拒绝
-
+
new
-
+
user.last_editing_user
- 最后编辑此部件的用户
+ 最后编辑此物料的用户
-
+
part.isFavorite
- Favorite
+ 收藏
-
+
part.minOrderAmount
- 最低订购量
+ 最小订购数量
-
+
manufacturer.label
制造商
-
+
name.label
名称
-
+
new
-
+
part.back_to_info
- 返回当前版本
+ 返回信息页面
-
+
description.label
描述
-
+
category.label
类别
-
+
instock.label
在库
-
+
mininstock.label
- 最低库存
+ 最小库存量
-
+
footprint.label
封装
-
+
part.avg_price.label
平均价格
-
+
part.supplier.name
名称
-
+
part.supplier.partnr
- 合作伙伴.
+ 物料编号
-
+
part.order.minamount
- 最低数量
+ 最小数量
-
+
part.order.price
价格
-
+
part.order.single_price
单价
-
+
part_lots.description
描述
-
+
part_lots.storage_location
存储位置
-
+
part_lots.amount
数量
-
+
part_lots.location_unknown
存储位置未知
-
+
part_lots.instock_unknown
数量未知
-
+
part_lots.expiration_date
- 到期时间
+ 过期日期
-
+
part_lots.is_expired
- 已到期
+ 已过期
-
+
part_lots.need_refill
- 需要补充
+ 需要补货
-
+
part.info.prev_picture
上一张图片
-
+
part.info.next_picture
下一张图片
-
+
part.mass.tooltip
- 重量
+ 质量
-
+
part.needs_review.badge
- 需要审查
+ 需要检查
-
+
part.favorite.badge
收藏
-
+
part.obsolete.badge
- 不再可用
+ 已停产
-
+
parameters.extracted_from_description
从描述中自动提取
-
+
parameters.auto_extracted_from_comment
- 从注释中自动提取
+ 从备注中自动提取
-
+
part.edit.btn
- 编辑部件
+ 编辑物料
-
+
part.clone.btn
- 克隆部件
+ 克隆物料
-
+
part.create.btn
- 新建部件
+ 创建新物料
-
+
part.delete.confirm_title
- 确定删除该部件?
+ 确定要删除此物料?
-
+
part.delete.message
- 此部件与它的任何相关信息(如附件、价格信息等)将被删除。 该操作不能被撤消
+ 此物料及所有关联信息(如附件、价格信息等)将被删除。此操作无法撤消!
-
+
part.delete
- 删除部件
+ 删除物料
-
+
parts_list.all.title
- 所有部件
+ 所有物料
-
+
parts_list.category.title
- 部件(根据类别)
+ 属于类别的物料
-
+
parts_list.footprint.title
- 部件(根据封装)
+ 使用封装的物料
-
+
parts_list.manufacturer.title
- 部件(根据制造商)
+ 属于制造商的物料
-
+
parts_list.search.title
- 搜索部件
+ 搜索物料
-
+
parts_list.storelocation.title
- 部件(根据存储位置)
+ 位于存储位置的物料
-
+
parts_list.supplier.title
- 部件(根据供应商)
+ 属于供应商的物料
-
+
parts_list.tags.title
- 部件(根据标签)
+ 拥有标签的物料
-
+
entity.info.common.tab
- 基本
+ 通用
-
+
entity.info.statistics.tab
- 统计数据
+ 统计
-
+
entity.info.attachments.tab
附件
-
+
entity.info.parameters.tab
参数
-
+
entity.info.name
名称
-
+
entity.info.parent
- 父元素
+ 父级
-
+
entity.edit.btn
编辑
-
+
entity.info.children_count
- 子元素计数
+ 子元素数量
-
+
tfa.check.title
- 需要2FA身份验证
+ 需要双因素认证
-
+
tfa.code.trusted_pc
- 这是受信任的计算机(如果启用此功能,则不会在此计算机上执行进一步的2FA查询)
+ 这是一台受信任的计算机(如果启用,将不会在此计算机上执行进一步的双因素查询)
-
+
login.btn
登录
-
+
user.logout
- 注销
+ 登出
-
+
tfa.check.code.label
身份验证器应用代码
-
+
tfa.check.code.help
- 输入身份验证器应用中的6位代码,如果身份验证器不可用,请输入备用代码之一。
+ 输入身份验证器应用中的6位代码,或如果身份验证器不可用,输入备份代码。
-
+
login.title
登录
-
+
login.card_title
登录
-
+
login.username.label
用户名
-
+
login.username.placeholder
用户名
-
+
login.password.label
密码
-
+
login.password.placeholder
密码
-
+
login.rememberme
- 记住我(不应在公共计算机上使用)
+ 保持登录(不应在共享计算机上使用)
-
+
pw_reset.password_forget
- 忘记用户名/密码?
+ 忘记用户名/密码?
-
+
pw_reset.new_pw.header.title
设置新密码
-
+
pw_reset.request.header.title
- 要求新的密码
+ 请求新密码
-
+
tfa_u2f.http_warning
- 您正在使用不安全的 HTTP 方法访问此页面,因此 U2F 很可能无法工作(错误请求错误消息)。 如果您想使用安全密钥,请要求管理员设置安全 HTTPS 方法。
+ 正在使用不安全的HTTP方法访问此页面,因此U2F很可能无法工作(错误的请求错误消息)。如果要使用安全密钥,请让管理员设置HTTPS方法。
-
+
r_u2f_two_factor.pressbutton
- 请插入您的安全密钥并按下其按钮
+ 请插入安全密钥并按下按钮!
-
+
tfa_u2f.add_key.title
- 添加安全密钥
+ 增加安全密钥
-
+