diff --git a/VERSION b/VERSION
index 10c2c0c3..dedcc7d4 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.10.0
+2.9.1
diff --git a/composer.lock b/composer.lock
index 217a3e49..8e509fbf 100644
--- a/composer.lock
+++ b/composer.lock
@@ -13083,7 +13083,7 @@
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.36.0",
+ "version": "v1.34.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.36.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0"
},
"funding": [
{
@@ -13166,7 +13166,7 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.36.0",
+ "version": "v1.34.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.36.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.34.0"
},
"funding": [
{
@@ -13248,7 +13248,7 @@
},
{
"name": "symfony/polyfill-intl-icu",
- "version": "v1.36.0",
+ "version": "v1.34.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.36.0"
+ "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.34.0"
},
"funding": [
{
@@ -13336,7 +13336,7 @@
},
{
"name": "symfony/polyfill-intl-idn",
- "version": "v1.36.0",
+ "version": "v1.34.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.36.0"
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.34.0"
},
"funding": [
{
@@ -13423,7 +13423,7 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.36.0",
+ "version": "v1.34.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.36.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0"
},
"funding": [
{
@@ -13508,7 +13508,7 @@
},
{
"name": "symfony/polyfill-php83",
- "version": "v1.36.0",
+ "version": "v1.34.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.36.0"
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.34.0"
},
"funding": [
{
@@ -13588,7 +13588,7 @@
},
{
"name": "symfony/polyfill-php84",
- "version": "v1.36.0",
+ "version": "v1.34.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.36.0"
+ "source": "https://github.com/symfony/polyfill-php84/tree/v1.34.0"
},
"funding": [
{
@@ -13668,7 +13668,7 @@
},
{
"name": "symfony/polyfill-php85",
- "version": "v1.36.0",
+ "version": "v1.34.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.36.0"
+ "source": "https://github.com/symfony/polyfill-php85/tree/v1.34.0"
},
"funding": [
{
@@ -13748,7 +13748,7 @@
},
{
"name": "symfony/polyfill-uuid",
- "version": "v1.36.0",
+ "version": "v1.34.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.36.0"
+ "source": "https://github.com/symfony/polyfill-uuid/tree/v1.34.0"
},
"funding": [
{
@@ -18469,11 +18469,11 @@
},
{
"name": "phpstan/phpstan",
- "version": "2.1.48",
+ "version": "2.1.47",
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/231397213efb7c0a066ee024b5c3c87f2d3adfa0",
- "reference": "231397213efb7c0a066ee024b5c3c87f2d3adfa0",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/79015445d8bd79e62b29140f12e5bfced1dcca65",
+ "reference": "79015445d8bd79e62b29140f12e5bfced1dcca65",
"shasum": ""
},
"require": {
@@ -18518,7 +18518,7 @@
"type": "github"
}
],
- "time": "2026-04-15T20:24:19+00:00"
+ "time": "2026-04-13T15:49:08+00:00"
},
{
"name": "phpstan/phpstan-doctrine",
@@ -19244,12 +19244,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
- "reference": "bb550b5adb0d4d74c4f6857c6b3b3638c022e90b"
+ "reference": "b0b156ed9d5d2eb313c33f92af3dbc886ba4688a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/bb550b5adb0d4d74c4f6857c6b3b3638c022e90b",
- "reference": "bb550b5adb0d4d74c4f6857c6b3b3638c022e90b",
+ "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/b0b156ed9d5d2eb313c33f92af3dbc886ba4688a",
+ "reference": "b0b156ed9d5d2eb313c33f92af3dbc886ba4688a",
"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": "<2.2.27|>=2.3,<2.9.6",
+ "composer/composer": "<1.10.27|>=2,<2.2.26|>=2.3,<2.9.3",
"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.8|>=5,<5.9.15",
+ "craftcms/cms": "<=4.17.7|>=5,<=5.9.13",
"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.53",
+ "kimai/kimai": "<=2.50",
"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": "<=3.7.13|>=4,<=4.1.9",
- "october/system": "<=3.7.13|>=4,<=4.1.9",
+ "october/rain": "<1.0.472|>=1.1,<1.1.2",
+ "october/system": "<=3.7.12|>=4,<=4.0.11",
"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.42.1",
+ "pocketmine/pocketmine-mp": "<5.41.1",
"pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1",
"pressbooks/pressbooks": "<5.18",
"prestashop/autoupgrade": ">=4,<4.10.1",
@@ -19939,7 +19939,6 @@
"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",
@@ -20169,7 +20168,6 @@
"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",
@@ -20189,7 +20187,7 @@
"wpcloud/wp-stateless": "<3.2",
"wpglobus/wpglobus": "<=1.9.6",
"wpmetabox/meta-box": "<5.11.2",
- "wwbn/avideo": "<=29",
+ "wwbn/avideo": "<=26",
"xataface/xataface": "<3",
"xpressengine/xpressengine": "<3.0.15",
"yab/quarx": "<2.4.5",
@@ -20289,7 +20287,7 @@
"type": "tidelift"
}
],
- "time": "2026-04-15T20:21:07+00:00"
+ "time": "2026-04-13T18:30:45+00:00"
},
{
"name": "sebastian/cli-parser",
diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml
index 164ac717..5261c295 100644
--- a/config/packages/doctrine.yaml
+++ b/config/packages/doctrine.yaml
@@ -56,7 +56,6 @@ 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/phpstan.dist.neon b/phpstan.dist.neon
index c7da636f..fe51518d 100644
--- a/phpstan.dist.neon
+++ b/phpstan.dist.neon
@@ -59,9 +59,6 @@ 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 .*#'
@@ -73,6 +70,3 @@ parameters:
- message: '#Access to an undefined property Brick\\Schema\\Interfaces\\#'
path: src/Services/InfoProviderSystem/Providers/GenericWebProvider.php
-
- -
- identifier: nullCoalesce.property
diff --git a/public/kicad/footprints.txt b/public/kicad/footprints.txt
index 551d7d9c..a34b9406 100644
--- a/public/kicad/footprints.txt
+++ b/public/kicad/footprints.txt
@@ -1,4 +1,4 @@
-# Generated on Mon Apr 13 05:19:27 UTC 2026
+# Generated on Mon Mar 9 04:23:25 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 34e246a5..54fd79d2 100644
--- a/public/kicad/symbols.txt
+++ b/public/kicad/symbols.txt
@@ -1,4 +1,4 @@
-# Generated on Mon Apr 13 05:20:06 UTC 2026
+# Generated on Mon Mar 9 04:24:12 UTC 2026
# This file contains all symbols available in the offical KiCAD library
4xxx:14528
4xxx:14529
@@ -899,7 +899,6 @@ Amplifier_Buffer:BUF634AxD
Amplifier_Buffer:BUF634AxDDA
Amplifier_Buffer:BUF634AxDRB
Amplifier_Buffer:BUF634U
-Amplifier_Buffer:BUF802
Amplifier_Buffer:EL2001CN
Amplifier_Buffer:LH0002H
Amplifier_Buffer:LM6321H
@@ -1668,6 +1667,7 @@ 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,7 +2198,6 @@ Audio:WM8731SEDS
Audio:YM2149
Audio:YM2612
Audio:YM3438
-Auxiliary_Items:Generic_Outline
Auxiliary_Items:Jumper_Shunt
Auxiliary_Items:MountingScrew
Battery_Management:ADP5063
@@ -2255,11 +2254,6 @@ 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
@@ -2769,8 +2763,6 @@ 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
@@ -2909,7 +2901,6 @@ Connector:TestPoint_Alt
Connector:TestPoint_Flag
Connector:TestPoint_Probe
Connector:TestPoint_Small
-Connector:TestPoint_Square
Connector:UEXT_Host
Connector:UEXT_Slave
Connector:USB3_A
@@ -7781,7 +7772,6 @@ 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
@@ -15741,7 +15731,6 @@ Power_Management:RT9742AGJ5F
Power_Management:RT9742ANGJ5F
Power_Management:RT9742BGJ5F
Power_Management:RT9742BNGJ5F
-Power_Management:RT9742SNGV
Power_Management:SN6505ADBV
Power_Management:SN6505BDBV
Power_Management:SN6507DGQ
@@ -18703,7 +18692,6 @@ Regulator_Linear:TPS7A0530PDBZ
Regulator_Linear:TPS7A0531PDBV
Regulator_Linear:TPS7A0533PDBV
Regulator_Linear:TPS7A0533PDBZ
-Regulator_Linear:TPS7A20xxxDBV
Regulator_Linear:TPS7A20xxxDQN
Regulator_Linear:TPS7A3301RGW
Regulator_Linear:TPS7A39
@@ -20313,6 +20301,7 @@ Sensor:BME280
Sensor:BME680
Sensor:CHT11
Sensor:DHT11
+Sensor:INA260
Sensor:LTC2990
Sensor:MAX30102
Sensor:Nuclear-Radiation_Detector
@@ -20599,12 +20588,9 @@ 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
@@ -20886,7 +20872,6 @@ 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
@@ -21806,7 +21791,6 @@ 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
diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php
index 531deb3f..d2c35efd 100644
--- a/src/Controller/ProjectController.php
+++ b/src/Controller/ProjectController.php
@@ -69,13 +69,10 @@ 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/PartsDataTable.php b/src/DataTables/PartsDataTable.php
index b34eef9d..8bb5f6aa 100644
--- a/src/DataTables/PartsDataTable.php
+++ b/src/DataTables/PartsDataTable.php
@@ -38,7 +38,6 @@ 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;
@@ -119,18 +118,6 @@ 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) {
- //Output it as scientific number with a big E
- return htmlspecialchars(sprintf('%G', $siValue));
- }
- return '';
- },
- 'orderField' => 'SI_VALUE_SORT(part.name)',
- ])
->add('id', TextColumn::class, [
'label' => $this->translator->trans('part.table.id'),
])
@@ -497,19 +484,6 @@ 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/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php
index 2d5c4ebc..04d8206b 100644
--- a/src/DataTables/ProjectBomEntriesDataTable.php
+++ b/src/DataTables/ProjectBomEntriesDataTable.php
@@ -29,15 +29,12 @@ use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\MarkdownColumn;
use App\DataTables\Helpers\PartDataTableHelper;
use App\Doctrine\Helpers\FieldHelper;
-use App\Entity\Parts\ManufacturingStatus;
use App\Entity\Parts\Part;
+use App\Entity\Parts\ManufacturingStatus;
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;
@@ -53,9 +50,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
protected EntityURLGenerator $entityURLGenerator,
protected TranslatorInterface $translator,
protected AmountFormatter $amountFormatter,
- protected PartDataTableHelper $partDataTableHelper,
- protected ProjectBuildHelper $projectBuildHelper,
- protected MoneyFormatter $moneyFormatter,
+ protected PartDataTableHelper $partDataTableHelper
) {
}
@@ -207,27 +202,6 @@ 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/Doctrine/Functions/SiValueSort.php b/src/Doctrine/Functions/SiValueSort.php
deleted file mode 100644
index c4d16444..00000000
--- a/src/Doctrine/Functions/SiValueSort.php
+++ /dev/null
@@ -1,196 +0,0 @@
-.
- */
-
-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; //@phpstan-ignore-line - fallback to 1.0 if prefix is not recognized (should not happen due to regex)
-
- return $number * $multiplier;
- }
-}
diff --git a/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php
index aa6108c9..ad572d4c 100644
--- a/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php
+++ b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php
@@ -23,7 +23,6 @@ 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;
@@ -52,9 +51,6 @@ 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/Services/ProjectSystem/ProjectBuildHelper.php b/src/Services/ProjectSystem/ProjectBuildHelper.php
index ee5b8c68..a541c29d 100644
--- a/src/Services/ProjectSystem/ProjectBuildHelper.php
+++ b/src/Services/ProjectSystem/ProjectBuildHelper.php
@@ -25,22 +25,16 @@ 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,
- private PricedetailHelper $pricedetailHelper,
- ) {
+ public function __construct(private PartLotWithdrawAddHelper $withdraw_add_helper)
+ {
}
/**
@@ -174,81 +168,4 @@ 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/src/Settings/BehaviorSettings/PartTableColumns.php b/src/Settings/BehaviorSettings/PartTableColumns.php
index 32f6100b..3b30e0a4 100644
--- a/src/Settings/BehaviorSettings/PartTableColumns.php
+++ b/src/Settings/BehaviorSettings/PartTableColumns.php
@@ -52,8 +52,6 @@ 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/templates/projects/info/_info.html.twig b/templates/projects/info/_info.html.twig
index c3a8e86d..b95be253 100644
--- a/templates/projects/info/_info.html.twig
+++ b/templates/projects/info/_info.html.twig
@@ -55,32 +55,6 @@
- {% 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 %}
@@ -95,9 +69,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/Doctrine/Functions/SiValueSortTest.php b/tests/Doctrine/Functions/SiValueSortTest.php
deleted file mode 100644
index dbdd9d28..00000000
--- a/tests/Doctrine/Functions/SiValueSortTest.php
+++ /dev/null
@@ -1,193 +0,0 @@
-.
- */
-
-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/tests/Services/ProjectSystem/ProjectBuildHelperTest.php b/tests/Services/ProjectSystem/ProjectBuildHelperTest.php
index b80adb2f..fb31b51e 100644
--- a/tests/Services/ProjectSystem/ProjectBuildHelperTest.php
+++ b/tests/Services/ProjectSystem/ProjectBuildHelperTest.php
@@ -26,15 +26,13 @@ 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
{
- protected ProjectBuildHelper $service;
+ /** @var ProjectBuildHelper */
+ protected $service;
protected function setUp(): void
{
@@ -132,180 +130,6 @@ 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 8126c83d..894f6315 100644
--- a/tests/Services/ProjectSystem/ProjectBuildPartHelperTest.php
+++ b/tests/Services/ProjectSystem/ProjectBuildPartHelperTest.php
@@ -28,7 +28,8 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class ProjectBuildPartHelperTest extends WebTestCase
{
- protected ProjectBuildPartHelper $service;
+ /** @var ProjectBuildPartHelper */
+ protected $service;
protected function setUp(): void
{
diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf
index 0f4cf2c8..74ca2a26 100644
--- a/translations/messages.cs.xlf
+++ b/translations/messages.cs.xlf
@@ -7241,12 +7241,6 @@ 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 a435396c..9878a09e 100644
--- a/translations/messages.da.xlf
+++ b/translations/messages.da.xlf
@@ -642,12 +642,6 @@ Underelementer vil blive flyttet opad.
Gruppe
-
-
- specifications.eda_visibility.help
- Eksporter denne parameter som et EDA felt
-
- specification.create
@@ -2929,42 +2923,6 @@ 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
@@ -3307,12 +3265,6 @@ 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
@@ -7232,12 +7184,6 @@ Element 3
Pris
-
-
- project.bom.ext_price
- Extended Price
-
- part.info.withdraw_modal.title.withdraw
@@ -9556,12 +9502,6 @@ 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
@@ -9574,24 +9514,6 @@ 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
@@ -10026,18 +9948,6 @@ 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
@@ -10380,24 +10290,6 @@ 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
@@ -11022,84 +10914,6 @@ 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
@@ -12413,7 +12227,7 @@ Buerklin API-godkendelsesserver: 10 anmodninger/minut pr. IP-adresse
update_manager.progress.downgrade_title
- Downgrade fremskridtPart-DB er blevet nedgraderet! Du skal muligvis opdatere siden for at se den nye version.
+ Downgrade fremskridt
@@ -12500,102 +12314,6 @@ 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
@@ -12686,281 +12404,5 @@ 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 7e070ff2..db595136 100644
--- a/translations/messages.de.xlf
+++ b/translations/messages.de.xlf
@@ -2779,12 +2779,6 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr
Name
-
-
- part.table.si_value
- SI-Wert
-
- part.table.id
@@ -7217,18 +7211,6 @@ Element 1 -> Element 1.2
Unterprojekte
-
-
- project.info.total_build_price
- Gesamterstellpreis
-
-
-
-
- project.info.per_unit_price
- pro Einheit
-
- project.info.bom_add_parts
@@ -7253,12 +7235,6 @@ Element 1 -> Element 1.2
Preis
-
-
- project.bom.ext_price
- Gesamtpreis
-
- part.info.withdraw_modal.title.withdraw
@@ -10052,90 +10028,6 @@ 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.en.xlf b/translations/messages.en.xlf
index 4da88512..176c6650 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -2780,12 +2780,6 @@ 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
@@ -7218,18 +7212,6 @@ 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
@@ -7254,12 +7236,6 @@ 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 c580a491..17b2156b 100644
--- a/translations/messages.es.xlf
+++ b/translations/messages.es.xlf
@@ -7259,12 +7259,6 @@ Elemento 3
Precio
-
-
- project.bom.ext_price
- Extended Price
-
- part.info.withdraw_modal.title.withdraw
diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf
index 49b7ca03..37e0d27e 100644
--- a/translations/messages.fr.xlf
+++ b/translations/messages.fr.xlf
@@ -1,13 +1,13 @@
-
+
-
+ attachment_type.caption
- Type de fichiers pour la pièce jointe
+ Types pour fichiers joints
-
+ 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,28 +61,34 @@
Nouvelle catégorie
-
+
+
+ currency.caption
+ Devise
+
+
+ currency.iso_code.captionCode ISO
-
+ currency.symbol.captionSymbole de la devise
-
+ newcurrency.edit
- Éditer la devise
+ Editer la devise
-
+ new
@@ -91,61 +97,43 @@
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, les informations 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, donc les infos ne sont peut-être pas correctes. </i>
-
+ standard.labelPropriétés
-
+ infos.labelInformations
-
+ new
@@ -154,82 +142,82 @@
Historique
-
+ export.labelExporter
-
+ import_export.label
- Importer / exporter
+ Importer exporter
-
+ mass_creation.label
- Création en masse
+ Création multiple
-
+ 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éé. Vous pouvez créer des structures imbriquées par indentations.
+ Chaque ligne sera interprétée comme le nom d'un élément qui sera créé.
-
+ edit.captionÉditer l'élément "%name"
-
+ new.captionNouvel élément
-
+ footprint.labelpEmpreintes
-
+ newfootprint.edit
- Éditer l'empreinte
+ Editer l'empreinte
-
+ new
@@ -238,22 +226,28 @@
Nouvelle empreinte
-
+
+
+ group.edit.caption
+ Groupes
+
+
+ user.edit.permissionsPermissions
-
+ newgroup.edit
- Éditer le groupe
+ Editer le groupe
-
+ new
@@ -262,28 +256,34 @@
Nouveau groupe
-
+
+
+ label_profile.caption
+ Profil des étiquettes
+
+
+ label_profile.advancedAvancé
-
+ label_profile.commentCommentaire
-
+ newlabel_profile.edit
- Éditer profil d'étiquette
+ Editer profil d'étiquette
-
+ new
@@ -292,7 +292,13 @@
Nouveau profil d'étiquette
-
+
+
+ manufacturer.caption
+ Fabricants
+
+
+ new
@@ -301,7 +307,7 @@
Modifiez le fabricant
-
+ new
@@ -310,13 +316,25 @@
Nouveau fabricant
-
+
+
+ measurement_unit.caption
+ Unité de mesure
+
+
+
+
+ part_custom_state.caption
+ État personnalisé du composant
+
+
+ storelocation.labelpEmplacement de stockage
-
+ new
@@ -325,7 +343,7 @@
Modifier l'emplacement de stockage
-
+ new
@@ -334,7 +352,7 @@
Nouvel emplacement de stockage
-
+ new
@@ -343,7 +361,7 @@
Modifier le fournisseur
-
+ new
@@ -352,61 +370,67 @@
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>!
@@ -415,13 +439,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
@@ -430,7 +454,7 @@ L'utilisateur devra configurer à nouveau toutes les méthodes d'authentificatio
Modifier l'utilisateur
-
+ new
@@ -439,75 +463,75 @@ L'utilisateur devra configurer à nouveau toutes les méthodes d'authentificatio
Nouvel utilisateur
-
+ attachment.deleteSupprimer
-
+
- attachment.external_only
- Pièce jointe externe uniquement
+ attachment.external
+ Externe
-
+ attachment.preview.altMiniature du fichier joint
-
+
- attachment.view_local
- Vue locale de la pièce jointe
+ attachment.view
+ Afficher
-
+ 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
@@ -516,313 +540,308 @@ 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
- Type.
+ Typ.
-
+ 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 JavaScript pour profiter de toutes les fonctionnalités !
+ Activez Javascipt pour profiter de toutes les fonctionnalités!
-
+ sidebar.big.toggle
- Afficher / Cacher le panneau latéral
+ Afficher/Cacher le panneau latéral
+Show/Hide sidebar
-
+ 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 licence
+ Information de license
-
+ 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
@@ -831,97 +850,97 @@ Les sous éléments seront déplacés vers le haut.
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
@@ -930,7 +949,7 @@ Les sous éléments seront déplacés vers le haut.
Annuler le changement / revenir à une date antérieure ?
-
+ new
@@ -939,337 +958,337 @@ Les sous éléments seront déplacés vers le haut.
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émentation de composant
+ Suggestions sans incrément de partie
-
+ part.edit.tab.advanced.ipn.partIncrementHeader
- Suggestions avec incrément numérique du composant
+ Propositions avec incréments numériques de parties
-
+ part.edit.tab.advanced.ipn.prefix.description.current-increment
- Caractéristiques IPN actuelles du composant
+ Spécification IPN actuelle pour la pièce
-
+ part.edit.tab.advanced.ipn.prefix.description.increment
- Prochaine caractéristique IPN possible basé sur la description d'un composant identique
+ Prochaine spécification IPN possible basée sur une description identique de la pièce
-
+ part.edit.tab.advanced.ipn.prefix_empty.direct_category
- Préfixe IPN ou catégorie vide, en spécifier une dans la catégorie "%name%"
+ Le préfixe IPN de la catégorie directe est vide, veuillez le spécifier dans la catégorie "%name%"
-
+ part.edit.tab.advanced.ipn.prefix.direct_category
- Préfixe IPN ou catégorie directe
+ Préfixe IPN de la catégorie directe
-
+ part.edit.tab.advanced.ipn.prefix.direct_category.increment
- Préfixe IPN de catégorie directe et incrément spécifique au composant
+ Préfixe IPN de la catégorie directe et d'un incrément spécifique à la partie
-
+ part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment
- Préfixes IPN avec ordre hiérarchique de catégorie du préfixe parent
+ Préfixes IPN avec un ordre hiérarchique des catégories des préfixes parents
-
+ part.edit.tab.advanced.ipn.prefix.hierarchical.increment
- Préfixes IPN avec ordre hiérarchique de catégorie du préfixe parent et incrément spécifique à la pièce
+ Préfixes IPN avec un ordre hiérarchique des catégories des préfixes parents et un incrément spécifique à la pièce
-
+ part.edit.tab.advanced.ipn.prefix.not_saved
- Cré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
+ Créez d'abord une pièce et assignez-la à une catégorie : avec les catégories existantes et leurs propres préfixes IPN, l'identifiant IPN pour la pièce peut être proposé automatiquement
-
+ part.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_local
- Télécharger la pièce jointe locale
+ attachment.download
+ Téléchargement
-
+ new
@@ -1278,13 +1297,13 @@ Les sous éléments seront déplacés vers le haut.
Utilisateur qui a créé ce composant
-
+ UnknownInconnu
-
+ new
@@ -1293,7 +1312,7 @@ Les sous éléments seront déplacés vers le haut.
Accès refusé
-
+ new
@@ -1302,31 +1321,31 @@ Les sous éléments seront déplacés vers le haut.
Utilisateur qui a édité ce composant en dernier
-
+ part.isFavoriteFavoris
-
+ part.minOrderAmountQuantité minimale de commande
-
+ manufacturer.labelFabricant
-
+ name.labelNom
-
+ new
@@ -1335,439 +1354,439 @@ Les sous éléments seront déplacés vers le haut.
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 Fournisseur
+ Lien/Code cmd.
-
+ 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
-
+