Compare commits

...

8 commits

Author SHA1 Message Date
Jan Böhmer
a82d515034
New Crowdin updates (#1325)
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
* New translations messages.en.xlf (Danish)

* New translations validators.en.xlf (Danish)

* New translations messages.en.xlf (German)

* New translations messages.en.xlf (English)

* New translations messages.en.xlf (French)

* New translations validators.en.xlf (French)

* New translations messages.en.xlf (English)

* New translations messages.en.xlf (English)

* New translations messages.en.xlf (German)
2026-04-15 23:38:40 +02:00
Jan Böhmer
6a30b41688 Bumped version to 2.10.0 2026-04-15 23:27:30 +02:00
Jan Böhmer
ec05f9d8ab Fixed phpstan issues 2026-04-15 23:27:10 +02:00
Jan Böhmer
1c3dfa26bb Updated dependencies 2026-04-15 23:06:40 +02:00
Jan Böhmer
766665f9e5 Use big E for si value formatting output 2026-04-15 22:57:02 +02:00
Wieland Schopohl
29db029d69
Add SI-prefix-aware sorting column for parts tableFeature/si value sort (#1344)
* Add SI-prefix-aware sorting column for the parts table

Adds an optional "Name (SI)" column that parses numeric values with SI
prefixes (p, n, u/µ, m, k/K, M, G, T) from part names and sorts by the
resulting physical value. This is useful for electronic components where
alphabetical sorting produces wrong results — e.g. 100nF, 10pF, 1uF
should sort as 10pF < 100nF < 1uF.

Implementation:
- New SiValueSort DQL function with platform-specific SQL generation
  for PostgreSQL (POSIX regex), MySQL/MariaDB (REGEXP_SUBSTR), and
  SQLite (PHP callback registered via the existing middleware).
- The regex is start-anchored: only names beginning with a number are
  matched. Part numbers like "MCP2515" or "Crystal 20MHz" are ignored.
- When SI sort is active, NATSORT is appended as a secondary sort so
  that non-matching parts fall back to natural string ordering instead
  of appearing in arbitrary order.
- The column is opt-in (not in default columns) and displays the parsed
  float value, or an empty cell for non-matching names.

* Rename SI column from "Name (SI)" to "SI Value"

The column now shows the parsed numeric value rather than the part name,
so the label should reflect that.

* Support comma as decimal separator in SI value parsing

Part names using European decimal notation (e.g. "4,7 kΩ", "2,2uF")
were parsed incorrectly because the regex only recognized dots. Now
commas are normalized to dots before parsing, matching the existing
pattern used elsewhere in the codebase (PartNormalizer, price providers).
2026-04-15 22:56:34 +02:00
Jan Böhmer
146e85f84c
Update KiCad symbols and footprints lists (#1333)
* Update KiCad symbols and footprints lists

* Update KiCad symbols and footprints lists

* Update KiCad symbols and footprints lists

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-15 22:13:54 +02:00
Niklas
c17cf5e83c
Add price columns to project BOM table and build price summary (#1345)
* Add unit price and extended price columns to project BOM table

Adds two optional columns to the project BOM datatable (hidden by
default, toggleable via column visibility):

- **Price**: unit price for the BOM entry in the base currency,
  looked up via PricedetailHelper. For parts whose BOM quantity falls
  below the minimum order amount the minimum order amount is used for
  the price tier lookup so that a price is always returned.
- **Extended Price**: unit price multiplied by the BOM quantity.

Prices are rendered via MoneyFormatter (locale-aware, with currency
symbol). Both columns round up to 2 decimal places to avoid displaying
0.00 for very small prices.

* Add translation key for project.bom.ext_price

Adds the English translation "Extended Price" for the new BOM extended
price column. Other languages are marked needs-translation and will be
picked up by Crowdin.

* Add build price summary to project info tab

Displays the total BOM price for N builds on the project info page,
using the existing price-tier logic from PricedetailHelper. The user
can adjust the number of builds via a small form; the unit price is
also shown when N > 1.

New backend:
- ProjectBuildHelper gains calculateTotalBuildPrice(),
  calculateUnitBuildPrice(), roundedTotalBuildPrice(), and
  roundedUnitBuildPrice() — bulk-order quantities are factored in so
  that price tiers apply correctly across N builds.
- ProjectController::info() now reads ?n= and passes number_of_builds
  to the template.

Template (_info.html.twig):
- Adds price badge (hidden when no pricing data is available).
- Adds number-of-builds form that reloads the info page.

* Add tests for build price calculation in ProjectBuildHelper

Covers calculateTotalBuildPrice(), calculateUnitBuildPrice(),
roundedTotalBuildPrice(), and the private getBomEntryUnitPrice()
helper. Scenarios tested: empty project, no pricing data, non-part BOM
entries with manual prices, part entries with pricedetails, mixed
entries, rounding-up of sub-cent prices, and minimum order amount
floor for price tier lookup.

* Deduplicate BOM entry price logic into ProjectBuildHelper

The private getBomEntryUnitPrice() in ProjectBomEntriesDataTable was
identical to the one in ProjectBuildHelper. Replaced it with a new
public getEntryUnitPrice() on ProjectBuildHelper (returns BigDecimal,
never null) and delegate to it from the DataTable.

This eliminates the duplicate code and brings the DataTable lines under
the existing ProjectBuildHelper test coverage. Added three tests for
getEntryUnitPrice() covering the no-pricing, non-part, and part cases.

* Added type hint to service

---------

Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-04-15 22:13:07 +02:00
31 changed files with 9172 additions and 1245 deletions

View file

@ -1 +1 @@
2.9.1
2.10.0

68
composer.lock generated
View file

@ -13083,7 +13083,7 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.34.0",
"version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@ -13142,7 +13142,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0"
},
"funding": [
{
@ -13166,7 +13166,7 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.34.0",
"version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
@ -13224,7 +13224,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0"
},
"funding": [
{
@ -13248,7 +13248,7 @@
},
{
"name": "symfony/polyfill-intl-icu",
"version": "v1.34.0",
"version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-icu.git",
@ -13312,7 +13312,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.36.0"
},
"funding": [
{
@ -13336,7 +13336,7 @@
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.34.0",
"version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
@ -13399,7 +13399,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.36.0"
},
"funding": [
{
@ -13423,7 +13423,7 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.34.0",
"version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@ -13484,7 +13484,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0"
},
"funding": [
{
@ -13508,7 +13508,7 @@
},
{
"name": "symfony/polyfill-php83",
"version": "v1.34.0",
"version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
@ -13564,7 +13564,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-php83/tree/v1.36.0"
},
"funding": [
{
@ -13588,7 +13588,7 @@
},
{
"name": "symfony/polyfill-php84",
"version": "v1.34.0",
"version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php84.git",
@ -13644,7 +13644,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php84/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-php84/tree/v1.36.0"
},
"funding": [
{
@ -13668,7 +13668,7 @@
},
{
"name": "symfony/polyfill-php85",
"version": "v1.34.0",
"version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php85.git",
@ -13724,7 +13724,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php85/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-php85/tree/v1.36.0"
},
"funding": [
{
@ -13748,7 +13748,7 @@
},
{
"name": "symfony/polyfill-uuid",
"version": "v1.34.0",
"version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-uuid.git",
@ -13807,7 +13807,7 @@
"uuid"
],
"support": {
"source": "https://github.com/symfony/polyfill-uuid/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-uuid/tree/v1.36.0"
},
"funding": [
{
@ -18469,11 +18469,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.47",
"version": "2.1.48",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/79015445d8bd79e62b29140f12e5bfced1dcca65",
"reference": "79015445d8bd79e62b29140f12e5bfced1dcca65",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/231397213efb7c0a066ee024b5c3c87f2d3adfa0",
"reference": "231397213efb7c0a066ee024b5c3c87f2d3adfa0",
"shasum": ""
},
"require": {
@ -18518,7 +18518,7 @@
"type": "github"
}
],
"time": "2026-04-13T15:49:08+00:00"
"time": "2026-04-15T20:24:19+00:00"
},
{
"name": "phpstan/phpstan-doctrine",
@ -19244,12 +19244,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "b0b156ed9d5d2eb313c33f92af3dbc886ba4688a"
"reference": "bb550b5adb0d4d74c4f6857c6b3b3638c022e90b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/b0b156ed9d5d2eb313c33f92af3dbc886ba4688a",
"reference": "b0b156ed9d5d2eb313c33f92af3dbc886ba4688a",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/bb550b5adb0d4d74c4f6857c6b3b3638c022e90b",
"reference": "bb550b5adb0d4d74c4f6857c6b3b3638c022e90b",
"shasum": ""
},
"conflict": {
@ -19379,7 +19379,7 @@
"codingms/modules": "<4.3.11|>=5,<5.7.4|>=6,<6.4.2|>=7,<7.5.5",
"commerceteam/commerce": ">=0.9.6,<0.9.9",
"components/jquery": ">=1.0.3,<3.5",
"composer/composer": "<1.10.27|>=2,<2.2.26|>=2.3,<2.9.3",
"composer/composer": "<2.2.27|>=2.3,<2.9.6",
"concrete5/concrete5": "<9.4.8",
"concrete5/core": "<8.5.8|>=9,<9.1",
"contao-components/mediaelement": ">=2.14.2,<2.21.1",
@ -19396,7 +19396,7 @@
"cpsit/typo3-mailqueue": "<0.4.5|>=0.5,<0.5.2",
"craftcms/aws-s3": ">=2.0.2,<=2.2.4",
"craftcms/azure-blob": ">=2.0.0.0-beta1,<=2.1",
"craftcms/cms": "<=4.17.7|>=5,<=5.9.13",
"craftcms/cms": "<=4.17.8|>=5,<5.9.15",
"craftcms/commerce": ">=4,<4.11|>=5,<5.6",
"craftcms/composer": ">=4.0.0.0-RC1-dev,<=4.10|>=5.0.0.0-RC1-dev,<=5.5.1",
"craftcms/craft": ">=3.5,<=4.16.17|>=5.0.0.0-RC1-dev,<=5.8.21",
@ -19664,7 +19664,7 @@
"kelvinmo/simplexrd": "<3.1.1",
"kevinpapst/kimai2": "<1.16.7",
"khodakhah/nodcms": "<=3",
"kimai/kimai": "<=2.50",
"kimai/kimai": "<2.53",
"kitodo/presentation": "<3.2.3|>=3.3,<3.3.4",
"klaviyo/magento2-extension": ">=1,<3",
"knplabs/knp-snappy": "<=1.4.2",
@ -19805,8 +19805,8 @@
"october/backend": "<1.1.2",
"october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1",
"october/october": "<3.7.5",
"october/rain": "<1.0.472|>=1.1,<1.1.2",
"october/system": "<=3.7.12|>=4,<=4.0.11",
"october/rain": "<=3.7.13|>=4,<=4.1.9",
"october/system": "<=3.7.13|>=4,<=4.1.9",
"oliverklee/phpunit": "<3.5.15",
"omeka/omeka-s": "<4.0.3",
"onelogin/php-saml": "<2.21.1|>=3,<3.8.1|>=4,<4.3.1",
@ -19885,7 +19885,7 @@
"pixelfed/pixelfed": "<0.12.5",
"plotly/plotly.js": "<2.25.2",
"pocketmine/bedrock-protocol": "<8.0.2",
"pocketmine/pocketmine-mp": "<5.41.1",
"pocketmine/pocketmine-mp": "<5.42.1",
"pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1",
"pressbooks/pressbooks": "<5.18",
"prestashop/autoupgrade": ">=4,<4.10.1",
@ -19939,6 +19939,7 @@
"rudloff/rtmpdump-bin": "<=2.3.1",
"s-cart/core": "<=9.0.5",
"s-cart/s-cart": "<6.9",
"s9y/serendipity": "<2.6",
"sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1",
"sabre/dav": ">=1.6,<1.7.11|>=1.8,<1.8.9",
"saloonphp/saloon": "<4",
@ -20168,6 +20169,7 @@
"webcoast/deferred-image-processing": "<1.0.2",
"webklex/laravel-imap": "<5.3",
"webklex/php-imap": "<5.3",
"webonyx/graphql-php": "<=15.31.4",
"webpa/webpa": "<3.1.2",
"webreinvent/vaahcms": "<=2.3.1",
"wikibase/wikibase": "<=1.39.3",
@ -20187,7 +20189,7 @@
"wpcloud/wp-stateless": "<3.2",
"wpglobus/wpglobus": "<=1.9.6",
"wpmetabox/meta-box": "<5.11.2",
"wwbn/avideo": "<=26",
"wwbn/avideo": "<=29",
"xataface/xataface": "<3",
"xpressengine/xpressengine": "<3.0.15",
"yab/quarx": "<2.4.5",
@ -20287,7 +20289,7 @@
"type": "tidelift"
}
],
"time": "2026-04-13T18:30:45+00:00"
"time": "2026-04-15T20:21:07+00:00"
},
{
"name": "sebastian/cli-parser",

View file

@ -56,6 +56,7 @@ doctrine:
natsort: App\Doctrine\Functions\Natsort
array_position: App\Doctrine\Functions\ArrayPosition
ilike: App\Doctrine\Functions\ILike
si_value_sort: App\Doctrine\Functions\SiValueSort
when@test:
doctrine:

View file

@ -59,6 +59,9 @@ parameters:
- '#expects .*PartParameter, .*AbstractParameter given.#'
- '#Part::getParameters\(\) should return .*AbstractParameter#'
# Fix some weird issue with how covariance with collections is solved
- '#Method App\\Entity\\Base\\AbstractStructuralDBElement::getParameters\(\) should return Doctrine\\Common\\Collections\\Collection<int, App\\Entity\\Parameters\\AbstractParameter> but returns#'
# Ignore doctrine type mapping mismatch
- '#Property .* type mapping mismatch: property can contain .* but database expects .*#'
@ -70,3 +73,6 @@ parameters:
- message: '#Access to an undefined property Brick\\Schema\\Interfaces\\#'
path: src/Services/InfoProviderSystem/Providers/GenericWebProvider.php
-
identifier: nullCoalesce.property

View file

@ -1,4 +1,4 @@
# Generated on Mon Mar 9 04:23:25 UTC 2026
# Generated on Mon Apr 13 05:19:27 UTC 2026
# This file contains all footprints available in the offical KiCAD library
Audio_Module:Reverb_BTDR-1H
Audio_Module:Reverb_BTDR-1V

View file

@ -1,4 +1,4 @@
# Generated on Mon Mar 9 04:24:12 UTC 2026
# Generated on Mon Apr 13 05:20:06 UTC 2026
# This file contains all symbols available in the offical KiCAD library
4xxx:14528
4xxx:14529
@ -899,6 +899,7 @@ Amplifier_Buffer:BUF634AxD
Amplifier_Buffer:BUF634AxDDA
Amplifier_Buffer:BUF634AxDRB
Amplifier_Buffer:BUF634U
Amplifier_Buffer:BUF802
Amplifier_Buffer:EL2001CN
Amplifier_Buffer:LH0002H
Amplifier_Buffer:LM6321H
@ -1667,7 +1668,6 @@ Analog_ADC:CA3300
Analog_ADC:HX711
Analog_ADC:ICL7106CPL
Analog_ADC:ICL7107CPL
Analog_ADC:INA234AxYBJ
Analog_ADC:LTC1406CGN
Analog_ADC:LTC1406IGN
Analog_ADC:LTC1594CS
@ -2198,6 +2198,7 @@ Audio:WM8731SEDS
Audio:YM2149
Audio:YM2612
Audio:YM3438
Auxiliary_Items:Generic_Outline
Auxiliary_Items:Jumper_Shunt
Auxiliary_Items:MountingScrew
Battery_Management:ADP5063
@ -2254,6 +2255,11 @@ Battery_Management:BQ76200PW
Battery_Management:BQ76920PW
Battery_Management:BQ76930DBT
Battery_Management:BQ76940DBT
Battery_Management:BQ7695201PFBR
Battery_Management:BQ7695202PFBR
Battery_Management:BQ7695203PFBR
Battery_Management:BQ7695204PFBR
Battery_Management:BQ76952PFBR
Battery_Management:BQ78350DBT
Battery_Management:BQ78350DBT-R1
Battery_Management:CN3063
@ -2763,6 +2769,8 @@ Connector:DIN41612_02x32_AC
Connector:DIN41612_02x32_AE
Connector:DIN41612_02x32_ZB
Connector:DIN41612_03x32_C_Split
Connector:DP_Sink
Connector:DP_Source
Connector:DVI-D_Dual_Link
Connector:DVI-I_Dual_Link
Connector:ExpressCard
@ -2901,6 +2909,7 @@ Connector:TestPoint_Alt
Connector:TestPoint_Flag
Connector:TestPoint_Probe
Connector:TestPoint_Small
Connector:TestPoint_Square
Connector:UEXT_Host
Connector:UEXT_Slave
Connector:USB3_A
@ -7772,6 +7781,7 @@ FPGA_Lattice:ICE40HX1K-TQ144
FPGA_Lattice:ICE40HX4K-BG121
FPGA_Lattice:ICE40HX4K-TQ144
FPGA_Lattice:ICE40HX8K-BG121
FPGA_Lattice:ICE40LP384-SG32
FPGA_Lattice:ICE40UL1K-SWG16
FPGA_Lattice:ICE40UP5K-SG48ITR
FPGA_Lattice:ICE5LP1K-SG48
@ -15731,6 +15741,7 @@ Power_Management:RT9742AGJ5F
Power_Management:RT9742ANGJ5F
Power_Management:RT9742BGJ5F
Power_Management:RT9742BNGJ5F
Power_Management:RT9742SNGV
Power_Management:SN6505ADBV
Power_Management:SN6505BDBV
Power_Management:SN6507DGQ
@ -18692,6 +18703,7 @@ Regulator_Linear:TPS7A0530PDBZ
Regulator_Linear:TPS7A0531PDBV
Regulator_Linear:TPS7A0533PDBV
Regulator_Linear:TPS7A0533PDBZ
Regulator_Linear:TPS7A20xxxDBV
Regulator_Linear:TPS7A20xxxDQN
Regulator_Linear:TPS7A3301RGW
Regulator_Linear:TPS7A39
@ -20301,7 +20313,6 @@ Sensor:BME280
Sensor:BME680
Sensor:CHT11
Sensor:DHT11
Sensor:INA260
Sensor:LTC2990
Sensor:MAX30102
Sensor:Nuclear-Radiation_Detector
@ -20588,9 +20599,12 @@ Sensor_Energy:INA219BxD
Sensor_Energy:INA219BxDCN
Sensor_Energy:INA226
Sensor_Energy:INA228
Sensor_Energy:INA229
Sensor_Energy:INA233
Sensor_Energy:INA234AxYBJ
Sensor_Energy:INA237
Sensor_Energy:INA238
Sensor_Energy:INA260
Sensor_Energy:LTC4151xMS
Sensor_Energy:MCP39F521
Sensor_Energy:PAC1931x-xJ6CX
@ -20872,6 +20886,7 @@ Sensor_Proximity:BPR-105
Sensor_Proximity:BPR-105F
Sensor_Proximity:BPR-205
Sensor_Proximity:CNY70
Sensor_Proximity:FDC1004DGS
Sensor_Proximity:GP2S700HCP
Sensor_Proximity:ITR1201SR10AR
Sensor_Proximity:ITR8307
@ -21791,6 +21806,7 @@ Transistor_BJT:Q_NPN_Darlington_ECBC
Transistor_BJT:Q_NPN_EBC
Transistor_BJT:Q_NPN_ECB
Transistor_BJT:Q_NPN_ECBC
Transistor_BJT:Q_PNP_ACAB
Transistor_BJT:Q_PNP_BCE
Transistor_BJT:Q_PNP_BCEC
Transistor_BJT:Q_PNP_BEC

View file

@ -69,10 +69,13 @@ class ProjectController extends AbstractController
return $table->getResponse();
}
$number_of_builds = max(1, $request->query->getInt('n', 1));
return $this->render('projects/info/info.html.twig', [
'buildHelper' => $buildHelper,
'datatable' => $table,
'project' => $project,
'number_of_builds' => $number_of_builds,
]);
}

View file

@ -38,6 +38,7 @@ use App\DataTables\Filters\PartFilter;
use App\DataTables\Filters\PartSearchFilter;
use App\DataTables\Helpers\ColumnSortHelper;
use App\DataTables\Helpers\PartDataTableHelper;
use App\Doctrine\Functions\SiValueSort;
use App\Doctrine\Helpers\FieldHelper;
use App\Entity\Parts\ManufacturingStatus;
use App\Entity\Parts\Part;
@ -118,6 +119,18 @@ 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'),
])
@ -484,6 +497,19 @@ final class PartsDataTable implements DataTableTypeInterface
//$builder->addGroupBy('_bulkImportJob');
}
//When sorting by SI value, add NATSORT as a secondary sort so that parts without
//an SI-prefixed value fall back to natural string ordering seamlessly.
$orderByParts = $builder->getDQLPart('orderBy');
foreach ($orderByParts as $orderBy) {
foreach ($orderBy->getParts() as $part) {
if (str_contains($part, 'SI_VALUE_SORT')) {
$direction = str_contains($part, 'DESC') ? 'DESC' : 'ASC';
$builder->addOrderBy('NATSORT(part.name)', $direction);
break 2;
}
}
}
return $builder;
}

View file

@ -29,12 +29,15 @@ use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\MarkdownColumn;
use App\DataTables\Helpers\PartDataTableHelper;
use App\Doctrine\Helpers\FieldHelper;
use App\Entity\Parts\Part;
use App\Entity\Parts\ManufacturingStatus;
use App\Entity\Parts\Part;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Services\ElementTypeNameGenerator;
use App\Services\EntityURLGenerator;
use App\Services\Formatters\AmountFormatter;
use App\Services\Formatters\MoneyFormatter;
use App\Services\ProjectSystem\ProjectBuildHelper;
use Brick\Math\RoundingMode;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
@ -50,7 +53,9 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
protected EntityURLGenerator $entityURLGenerator,
protected TranslatorInterface $translator,
protected AmountFormatter $amountFormatter,
protected PartDataTableHelper $partDataTableHelper
protected PartDataTableHelper $partDataTableHelper,
protected ProjectBuildHelper $projectBuildHelper,
protected MoneyFormatter $moneyFormatter,
) {
}
@ -202,6 +207,27 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
return '';
}
])
->add('price', TextColumn::class, [
'label' => 'project.bom.price',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
$price = $this->projectBuildHelper->getEntryUnitPrice($context);
return $this->moneyFormatter->format($price->toScale(2, RoundingMode::UP)->toFloat(), null, 2, true);
},
])
->add('ext_price', TextColumn::class, [
'label' => 'project.bom.ext_price',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
$price = $this->projectBuildHelper->getEntryUnitPrice($context);
return $this->moneyFormatter->format(
$price->multipliedBy($context->getQuantity())->toScale(2, RoundingMode::UP)->toFloat(),
null,
2,
true
);
},
])
->add('addedDate', LocaleDateTimeColumn::class, [
'label' => $this->translator->trans('part.table.addedDate'),

View file

@ -0,0 +1,196 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;
}
}

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Doctrine\Middleware;
use App\Doctrine\Functions\SiValueSort;
use App\Exceptions\InvalidRegexException;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
@ -51,6 +52,9 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
//Create a new collation for natural sorting
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
//Create a function for SI prefix value sorting
$native_connection->sqliteCreateFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, \PDO::SQLITE_DETERMINISTIC);
}
}

View file

@ -25,16 +25,22 @@ namespace App\Services\ProjectSystem;
use App\Entity\Parts\Part;
use App\Entity\ProjectSystem\Project;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Entity\PriceInformations\Currency;
use App\Helpers\Projects\ProjectBuildRequest;
use App\Services\Parts\PartLotWithdrawAddHelper;
use App\Services\Parts\PricedetailHelper;
use Brick\Math\BigDecimal;
use Brick\Math\RoundingMode;
/**
* @see \App\Tests\Services\ProjectSystem\ProjectBuildHelperTest
*/
final readonly class ProjectBuildHelper
{
public function __construct(private PartLotWithdrawAddHelper $withdraw_add_helper)
{
public function __construct(
private PartLotWithdrawAddHelper $withdraw_add_helper,
private PricedetailHelper $pricedetailHelper,
) {
}
/**
@ -168,4 +174,81 @@ final readonly class ProjectBuildHelper
$this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message);
}
}
/**
* Calculates the total price to build the given project N times, taking bulk pricing into account.
* Returns null if no BOM entry has any pricing information.
*/
public function calculateTotalBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
{
$total = BigDecimal::zero();
$has_price = false;
foreach ($project->getBomEntries() as $entry) {
$unit_price = $this->getBomEntryUnitPrice($entry, $number_of_builds, $currency);
if ($unit_price === null) {
continue;
}
$has_price = true;
$total = $total->plus($unit_price->multipliedBy($entry->getQuantity())->multipliedBy($number_of_builds));
}
return $has_price ? $total : null;
}
/**
* Calculates the price to build one unit of the given project when ordering for N builds in total.
* Returns null if no BOM entry has any pricing information.
*/
public function calculateUnitBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
{
$total = $this->calculateTotalBuildPrice($project, $number_of_builds, $currency);
if ($total === null) {
return null;
}
return $total->dividedBy($number_of_builds, 10, RoundingMode::HALF_UP);
}
/**
* Returns the total build price rounded up to 2 decimal places, ready for display.
*/
public function roundedTotalBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
{
return $this->calculateTotalBuildPrice($project, $number_of_builds, $currency)
?->toScale(2, RoundingMode::UP);
}
/**
* Returns the unit build price rounded up to 2 decimal places, ready for display.
*/
public function roundedUnitBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
{
return $this->calculateUnitBuildPrice($project, $number_of_builds, $currency)
?->toScale(2, RoundingMode::UP);
}
/**
* Returns the effective unit price for a single piece of the given BOM entry,
* taking bulk pricing and minimum order amounts into account for N builds.
* Returns BigDecimal::zero() when no pricing data is available.
*/
public function getEntryUnitPrice(ProjectBOMEntry $entry, int $number_of_builds = 1, ?Currency $currency = null): BigDecimal
{
return $this->getBomEntryUnitPrice($entry, $number_of_builds, $currency) ?? BigDecimal::zero();
}
/**
* Returns the effective unit price for a single piece of the given BOM entry,
* taking bulk pricing into account for N builds.
*/
private function getBomEntryUnitPrice(ProjectBOMEntry $entry, int $number_of_builds, ?Currency $currency): ?BigDecimal
{
if ($entry->getPart() instanceof Part) {
$total_qty = $entry->getQuantity() * $number_of_builds;
$min_order = $this->pricedetailHelper->getMinOrderAmount($entry->getPart());
$effective_qty = ($min_order !== null) ? max($total_qty, $min_order) : $total_qty;
return $this->pricedetailHelper->calculateAvgPrice($entry->getPart(), $effective_qty, $currency);
}
return $entry->getPrice();
}
}

View file

@ -52,6 +52,8 @@ enum PartTableColumns : string implements TranslatableInterface
case TAGS = "tags";
case ATTACHMENTS = "attachments";
case SI_VALUE = "si_value";
case EDA_REFERENCE = "eda_reference";
case EDA_VALUE = "eda_value";

View file

@ -55,6 +55,32 @@
</span>
</h6>
</div>
{% 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 %}
<div class="mt-1">
<h6>
<span class="badge badge-primary bg-success">
<i class="fa-solid fa-money-bill-wave fa-fw"></i>
{% 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 %}
<span class="ms-1">
({% trans %}project.info.per_unit_price{% endtrans %}: {{ unit_build_price | format_money(app.user.currency ?? null, 2) }})
</span>
{% endif %}
</span>
</h6>
</div>
{% endif %}
<form method="get" action="{{ path('project_info', {'id': project.id}) }}" class="mt-2">
<div class="input-group input-group-sm">
<span class="input-group-text">{% trans %}project.builds.number_of_builds{% endtrans %}</span>
<input type="number" min="1" class="form-control" name="n" required value="{{ n }}">
<button class="btn btn-outline-secondary" type="submit">{% trans %}project.build.btn_build{% endtrans %}</button>
</div>
</form>
{% if project.children is not empty %}
<div class="mt-1">
<h6>
@ -69,9 +95,9 @@
</div>
{% if project.comment is not empty %}
<p>
<h5>{% trans %}comment.label{% endtrans %}:</h5>
{{ project.comment|format_markdown }}
</p>
<div class="col-12 mt-2">
<h5>{% trans %}comment.label{% endtrans %}:</h5>
{{ project.comment|format_markdown }}
</div>
{% endif %}
</div>
</div>

View file

@ -0,0 +1,193 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<string, array{?string, ?float}>
*/
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));
}
}

View file

@ -26,13 +26,15 @@ use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Entity\ProjectSystem\Project;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
use App\Services\ProjectSystem\ProjectBuildHelper;
use Brick\Math\BigDecimal;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class ProjectBuildHelperTest extends WebTestCase
{
/** @var ProjectBuildHelper */
protected $service;
protected ProjectBuildHelper $service;
protected function setUp(): void
{
@ -130,6 +132,180 @@ final class ProjectBuildHelperTest extends WebTestCase
$project->addBomEntry($bom_entry1);
$this->assertSame('∞', $this->service->getMaximumBuildableCountAsString($project));
}
// --- Build price tests ---
private function makePartWithPrice(float $pricePerPiece, float $minQty = 1.0): Part
{
$part = new Part();
$orderdetail = new Orderdetail();
$pricedetail = (new Pricedetail())
->setMinDiscountQuantity($minQty)
->setPrice(BigDecimal::of((string) $pricePerPiece));
$orderdetail->addPricedetail($pricedetail);
$part->addOrderdetail($orderdetail);
return $part;
}
public function testCalculateTotalBuildPriceEmptyProject(): void
{
$project = new Project();
$this->assertNull($this->service->calculateTotalBuildPrice($project));
}
public function testCalculateTotalBuildPriceNoPricingData(): void
{
$project = new Project();
// Part with no orderdetails — no pricing
$entry = (new ProjectBOMEntry())->setPart(new Part())->setQuantity(2);
$project->addBomEntry($entry);
$this->assertNull($this->service->calculateTotalBuildPrice($project));
}
public function testCalculateTotalBuildPriceNonPartEntry(): void
{
$project = new Project();
$entry = new ProjectBOMEntry();
$entry->setName('Custom wire');
$entry->setQuantity(3);
$entry->setPrice(BigDecimal::of('2.00'));
$project->addBomEntry($entry);
// 3 × 2.00 = 6.00 for 1 build
$result = $this->service->calculateTotalBuildPrice($project, 1);
$this->assertNotNull($result);
$this->assertTrue(BigDecimal::of('6.00')->isEqualTo($result));
}
public function testCalculateTotalBuildPriceNonPartEntryMultipleBuilds(): void
{
$project = new Project();
$entry = new ProjectBOMEntry();
$entry->setName('Custom wire');
$entry->setQuantity(3);
$entry->setPrice(BigDecimal::of('2.00'));
$project->addBomEntry($entry);
// 3 × 2.00 × 5 = 30.00 for 5 builds
$result = $this->service->calculateTotalBuildPrice($project, 5);
$this->assertNotNull($result);
$this->assertTrue(BigDecimal::of('30.00')->isEqualTo($result));
}
public function testCalculateTotalBuildPriceWithPart(): void
{
$project = new Project();
$entry = new ProjectBOMEntry();
$entry->setPart($this->makePartWithPrice(1.50));
$entry->setQuantity(4);
$project->addBomEntry($entry);
// 4 × 1.50 = 6.00 for 1 build
$result = $this->service->calculateTotalBuildPrice($project, 1);
$this->assertNotNull($result);
$this->assertTrue(BigDecimal::of('6.00')->isEqualTo($result));
}
public function testCalculateUnitBuildPriceEqualsTotal(): void
{
$project = new Project();
$entry = new ProjectBOMEntry();
$entry->setName('Screw');
$entry->setQuantity(10);
$entry->setPrice(BigDecimal::of('0.10'));
$project->addBomEntry($entry);
// unit = 10 × 0.10 = 1.00; total for 3 builds = 3.00
$unit = $this->service->calculateUnitBuildPrice($project, 3);
$total = $this->service->calculateTotalBuildPrice($project, 3);
$this->assertNotNull($unit);
$this->assertNotNull($total);
$this->assertTrue($total->isEqualTo($unit->multipliedBy(3)));
}
public function testRoundedTotalBuildPriceRoundsUp(): void
{
$project = new Project();
$entry = new ProjectBOMEntry();
$entry->setName('Tiny part');
$entry->setQuantity(1);
$entry->setPrice(BigDecimal::of('0.001'));
$project->addBomEntry($entry);
// 0.001 rounded up to 2dp = 0.01
$result = $this->service->roundedTotalBuildPrice($project, 1);
$this->assertNotNull($result);
$this->assertTrue(BigDecimal::of('0.01')->isEqualTo($result));
}
public function testCalculateTotalBuildPriceMixedEntries(): void
{
$project = new Project();
// Part entry: 2 × 3.00 = 6.00
$partEntry = new ProjectBOMEntry();
$partEntry->setPart($this->makePartWithPrice(3.00));
$partEntry->setQuantity(2);
$project->addBomEntry($partEntry);
// Non-part entry with price: 5 × 1.00 = 5.00
$nonPartEntry = new ProjectBOMEntry();
$nonPartEntry->setName('Solder');
$nonPartEntry->setQuantity(5);
$nonPartEntry->setPrice(BigDecimal::of('1.00'));
$project->addBomEntry($nonPartEntry);
// Total = 11.00
$result = $this->service->calculateTotalBuildPrice($project, 1);
$this->assertNotNull($result);
$this->assertTrue(BigDecimal::of('11.00')->isEqualTo($result));
}
public function testGetEntryUnitPriceReturnsZeroForNoPricingData(): void
{
$entry = new ProjectBOMEntry();
$entry->setPart(new Part()); // part with no orderdetails
$entry->setQuantity(5);
$result = $this->service->getEntryUnitPrice($entry);
$this->assertTrue(BigDecimal::zero()->isEqualTo($result));
}
public function testGetEntryUnitPriceNonPartEntry(): void
{
$entry = new ProjectBOMEntry();
$entry->setName('Wire');
$entry->setQuantity(2);
$entry->setPrice(BigDecimal::of('1.25'));
$result = $this->service->getEntryUnitPrice($entry);
$this->assertTrue(BigDecimal::of('1.25')->isEqualTo($result));
}
public function testGetEntryUnitPriceWithPart(): void
{
$entry = new ProjectBOMEntry();
$entry->setPart($this->makePartWithPrice(2.00));
$entry->setQuantity(3);
$result = $this->service->getEntryUnitPrice($entry);
$this->assertTrue(BigDecimal::of('2.00')->isEqualTo($result));
}
public function testCalculateTotalBuildPriceRespectsMinOrderAmount(): void
{
$project = new Project();
// Part has a minimum order quantity of 10 at 0.50/piece
$entry = new ProjectBOMEntry();
$entry->setPart($this->makePartWithPrice(0.50, 10.0));
$entry->setQuantity(1); // BOM only needs 1, but MOQ is 10
$project->addBomEntry($entry);
// Price lookup uses qty=10 (MOQ), returns 0.50. Cost = 1 × 0.50 = 0.50
$result = $this->service->calculateTotalBuildPrice($project, 1);
$this->assertNotNull($result);
$this->assertTrue(BigDecimal::of('0.50')->isEqualTo($result));
}
}

View file

@ -28,8 +28,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class ProjectBuildPartHelperTest extends WebTestCase
{
/** @var ProjectBuildPartHelper */
protected $service;
protected ProjectBuildPartHelper $service;
protected function setUp(): void
{

View file

@ -7241,6 +7241,12 @@ Element 3</target>
<target>Cena</target>
</segment>
</unit>
<unit id="bomExPrc" name="project.bom.ext_price">
<segment state="initial">
<source>project.bom.ext_price</source>
<target>Extended Price</target>
</segment>
</unit>
<unit id="8tP3lQI" name="part.info.withdraw_modal.title.withdraw">
<segment state="translated">
<source>part.info.withdraw_modal.title.withdraw</source>

View file

@ -642,6 +642,12 @@ Underelementer vil blive flyttet opad.</target>
<target>Gruppe</target>
</segment>
</unit>
<unit id="8rz303Z" name="specifications.eda_visibility.help">
<segment state="translated">
<source>specifications.eda_visibility.help</source>
<target>Eksporter denne parameter som et EDA felt</target>
</segment>
</unit>
<unit id="XclPxI9" name="specification.create">
<segment state="translated">
<source>specification.create</source>
@ -2923,6 +2929,42 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt
<target>Bilag</target>
</segment>
</unit>
<unit id="f3Dggp6" name="part.table.eda_status">
<segment state="translated">
<source>part.table.eda_status</source>
<target>EDA</target>
</segment>
</unit>
<unit id="Q_myBuD" name="eda.status.symbol_set">
<segment state="translated">
<source>eda.status.symbol_set</source>
<target>KiCad symbolsæt</target>
</segment>
</unit>
<unit id="QGLfvit" name="eda.status.footprint_set">
<segment state="translated">
<source>eda.status.footprint_set</source>
<target>KiCad footprintsæt</target>
</segment>
</unit>
<unit id="hkze9M." name="eda.status.reference_set">
<segment state="translated">
<source>eda.status.reference_set</source>
<target>eda. status.reference_set</target>
</segment>
</unit>
<unit id="OTXbAfL" name="eda.status.complete">
<segment state="translated">
<source>eda.status.complete</source>
<target>EDA felter udfyldt (symbol, footprint, reference)</target>
</segment>
</unit>
<unit id="z9E5RB." name="eda.status.partial">
<segment state="translated">
<source>eda.status.partial</source>
<target>EDA felter delvist udfyldt</target>
</segment>
</unit>
<unit id="bMkafCp" name="flash.login_successful">
<segment state="translated">
<source>flash.login_successful</source>
@ -3265,6 +3307,12 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt
<target>Ikke længere tilgængelig</target>
</segment>
</unit>
<unit id="6H0WQWq" name="orderdetails.edit.eda_visibility">
<segment state="translated">
<source>orderdetails.edit.eda_visibility</source>
<target>Synlige i EDA</target>
</segment>
</unit>
<unit id="ZsO5AKM" name="orderdetails.edit.supplierpartnr.placeholder">
<segment state="translated">
<source>orderdetails.edit.supplierpartnr.placeholder</source>
@ -7184,6 +7232,12 @@ Element 3</target>
<target>Pris</target>
</segment>
</unit>
<unit id="bomExPrc" name="project.bom.ext_price">
<segment state="initial">
<source>project.bom.ext_price</source>
<target>Extended Price</target>
</segment>
</unit>
<unit id="8tP3lQI" name="part.info.withdraw_modal.title.withdraw">
<segment state="translated">
<source>part.info.withdraw_modal.title.withdraw</source>
@ -9502,6 +9556,12 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
<target>EIGP 114 stregkode (f.eks. Datamatrix-kode fra Digikey og Mouser dele)</target>
</segment>
</unit>
<unit id="BnqcKWx" name="scan_dialog.mode.lcsc">
<segment state="translated">
<source>scan_dialog.mode.lcsc</source>
<target>LCSC.com barcode</target>
</segment>
</unit>
<unit id="QSMS_Bd" name="scan_dialog.info_mode">
<segment state="translated">
<source>scan_dialog.info_mode</source>
@ -9514,6 +9574,24 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
<target>Afkodet information</target>
</segment>
</unit>
<unit id="kQnodbA" name="label_scanner.target_found">
<segment state="translated">
<source>label_scanner.target_found</source>
<target>Genstand fundet i database</target>
</segment>
</unit>
<unit id="7Arfw2q" name="label_scanner.scan_result.title">
<segment state="translated">
<source>label_scanner.scan_result.title</source>
<target>Scan-resultat</target>
</segment>
</unit>
<unit id="PTh4EK_" name="label_scanner.no_locations">
<segment state="translated">
<source>label_scanner.no_locations</source>
<target>Part er ikke gemt på nogen lokation.</target>
</segment>
</unit>
<unit id="nmXQWcS" name="label_generator.edit_profiles">
<segment state="translated">
<source>label_generator.edit_profiles</source>
@ -9948,6 +10026,18 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
<target>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 &gt; 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.</target>
</segment>
</unit>
<unit id="X5.rQdO" name="settings.misc.kicad_eda.datasheet_link">
<segment state="translated">
<source>settings.misc.kicad_eda.datasheet_link</source>
<target>Databladsfelt linker til PDF</target>
</segment>
</unit>
<unit id="Fm1QTCs" name="settings.misc.kicad_eda.datasheet_link.help">
<segment state="translated">
<source>settings.misc.kicad_eda.datasheet_link.help</source>
<target>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".</target>
</segment>
</unit>
<unit id="VwvmcWE" name="settings.behavior.sidebar">
<segment state="translated">
<source>settings.behavior.sidebar</source>
@ -10290,6 +10380,24 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
<target>Vis billedoverlejringen med detaljer om vedhæftet fil, når du holder musen over billedgalleriet med dele.</target>
</segment>
</unit>
<unit id="0iYdzdk" name="settings.behavior.keybindings">
<segment state="translated">
<source>settings.behavior.keybindings</source>
<target>Tastaturgenveje</target>
</segment>
</unit>
<unit id="_x13bMa" name="settings.behavior.keybindings.enable_special_characters">
<segment state="translated">
<source>settings.behavior.keybindings.enable_special_characters</source>
<target>Aktivér tastaturgenveje for specialtegn</target>
</segment>
</unit>
<unit id="Af8Zzqr" name="settings.behavior.keybindings.enable_special_characters.help">
<segment state="translated">
<source>settings.behavior.keybindings.enable_special_characters.help</source>
<target>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.</target>
</segment>
</unit>
<unit id="ALfPkeR" name="perm.config.change_system_settings">
<segment state="translated">
<source>perm.config.change_system_settings</source>
@ -10914,6 +11022,84 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
<target>Masseimport af datakilder</target>
</segment>
</unit>
<unit id="VtS1yT7" name="part_list.action.group.eda">
<segment state="translated">
<source>part_list.action.group.eda</source>
<target>EDA / KiCad</target>
</segment>
</unit>
<unit id="swU1Rp2" name="part_list.action.batch_edit_eda">
<segment state="translated">
<source>part_list.action.batch_edit_eda</source>
<target>Batchredigering af EDA-felter</target>
</segment>
</unit>
<unit id="ZaS_Hg5" name="batch_eda.title">
<segment state="translated">
<source>batch_eda.title</source>
<target>Batchredigering af EDA-felter</target>
</segment>
</unit>
<unit id="k2FDo7A" name="batch_eda.description">
<segment state="translated">
<source>batch_eda.description</source>
<target>Rediger EDA/KiCad-felter for %count% valgte dele. Markér feltet "Anvend" ud for hvert felt, du vil ændre.</target>
</segment>
</unit>
<unit id="WVHbic3" name="batch_eda.show_parts">
<segment state="translated">
<source>batch_eda.show_parts</source>
<target>Vis valgte dele</target>
</segment>
</unit>
<unit id="ubQd6G4" name="batch_eda.apply_hint">
<segment state="translated">
<source>batch_eda.apply_hint</source>
<target>Kun felter, hvor afkrydsningsfeltet "Anvend" er markeret, ændres. Felter, der ikke er markeret, ændres ikke.</target>
</segment>
</unit>
<unit id="w.5FGYL" name="batch_eda.apply">
<segment state="translated">
<source>batch_eda.apply</source>
<target>Anvend</target>
</segment>
</unit>
<unit id="9EmHp5C" name="batch_eda.field">
<segment state="translated">
<source>batch_eda.field</source>
<target>Felt</target>
</segment>
</unit>
<unit id="xHaCnEQ" name="batch_eda.value">
<segment state="translated">
<source>batch_eda.value</source>
<target>Værdi</target>
</segment>
</unit>
<unit id="PLqIBvC" name="batch_eda.submit">
<segment state="translated">
<source>batch_eda.submit</source>
<target>Anvend på udvalgte dele</target>
</segment>
</unit>
<unit id="5nO7Fpq" name="batch_eda.cancel">
<segment state="translated">
<source>batch_eda.cancel</source>
<target>Annullér</target>
</segment>
</unit>
<unit id="vhlPBNU" name="batch_eda.success">
<segment state="translated">
<source>batch_eda.success</source>
<target>EDA felter er nu opdateret</target>
</segment>
</unit>
<unit id="2fMo760" name="batch_eda.no_parts_selected">
<segment state="translated">
<source>batch_eda.no_parts_selected</source>
<target>Ingen dele blev valgt til batchredigering.</target>
</segment>
</unit>
<unit id="yzpXFkB" name="info_providers.bulk_import.step1.spn_recommendation">
<segment state="translated">
<source>info_providers.bulk_import.step1.spn_recommendation</source>
@ -12227,7 +12413,7 @@ Buerklin API-godkendelsesserver: 10 anmodninger/minut pr. IP-adresse</target>
<unit id="aSHDhOi" name="update_manager.progress.downgrade_title">
<segment state="translated">
<source>update_manager.progress.downgrade_title</source>
<target>Downgrade fremskridt</target>
<target>Downgrade fremskridtPart-DB er blevet nedgraderet! Du skal muligvis opdatere siden for at se den nye version.</target>
</segment>
</unit>
<unit id="XYR1vvR" name="update_manager.progress.downgrade_completed">
@ -12314,6 +12500,102 @@ Buerklin API-godkendelsesserver: 10 anmodninger/minut pr. IP-adresse</target>
<target>Gendannelse af sikkerhedskopi er deaktiveret af serverkonfigurationen.</target>
</segment>
</unit>
<unit id="oAb35wU" name="update_manager.backup.create">
<segment state="translated">
<source>update_manager.backup.create</source>
<target>Opret sikkerhedskopi</target>
</segment>
</unit>
<unit id="ms26oI0" name="update_manager.backup.create.confirm">
<segment state="translated">
<source>update_manager.backup.create.confirm</source>
<target>Vil du lave en fuld sikkerhedskopi nu? Det kan tage et stykke tid.</target>
</segment>
</unit>
<unit id="H9y0eLa" name="update_manager.backup.created">
<segment state="translated">
<source>update_manager.backup.created</source>
<target>Sikkerhedskopi er oprettet.</target>
</segment>
</unit>
<unit id="bMhXPVB" name="update_manager.backup.delete.confirm">
<segment state="translated">
<source>update_manager.backup.delete.confirm</source>
<target>Er du sikker på at du vil slette denne backup?</target>
</segment>
</unit>
<unit id="8tw67c_" name="update_manager.backup.deleted">
<segment state="translated">
<source>update_manager.backup.deleted</source>
<target>Sikkerhedskopi er slettet.</target>
</segment>
</unit>
<unit id="BzBBuqk" name="update_manager.backup.delete_error">
<segment state="translated">
<source>update_manager.backup.delete_error</source>
<target>Sikkerhedskopi kunne ikke udføres.</target>
</segment>
</unit>
<unit id="2olmcSs" name="update_manager.log.delete.confirm">
<segment state="translated">
<source>update_manager.log.delete.confirm</source>
<target>Er du sikker på at du vil slette denne log?</target>
</segment>
</unit>
<unit id=".ZrVHpp" name="update_manager.log.deleted">
<segment state="translated">
<source>update_manager.log.deleted</source>
<target>Log slettet.</target>
</segment>
</unit>
<unit id="P2JI5Yw" name="update_manager.log.delete_error">
<segment state="translated">
<source>update_manager.log.delete_error</source>
<target>Kunne ikke slette loggen.</target>
</segment>
</unit>
<unit id="Yos9FWk" name="update_manager.view_log">
<segment state="translated">
<source>update_manager.view_log</source>
<target>Vis log.</target>
</segment>
</unit>
<unit id="B9uA2va" name="update_manager.delete">
<segment state="translated">
<source>update_manager.delete</source>
<target>Slet</target>
</segment>
</unit>
<unit id="ZtgvnXB" name="update_manager.backup.download">
<segment state="translated">
<source>update_manager.backup.download</source>
<target>Download sikkerhedskopi</target>
</segment>
</unit>
<unit id="wxtmrnP" name="update_manager.backup.download.password_label">
<segment state="translated">
<source>update_manager.backup.download.password_label</source>
<target>Bekræft password for at downloade</target>
</segment>
</unit>
<unit id="MIlTTgL" name="update_manager.backup.download.security_warning">
<segment state="translated">
<source>update_manager.backup.download.security_warning</source>
<target>Sikkerhedskopier indeholder følsomme data, herunder password-hashes og hemmeligheder. Bekræft venligst dit password for at fortsætte med download.</target>
</segment>
</unit>
<unit id="kZPHBRt" name="update_manager.backup.download.invalid_password">
<segment state="translated">
<source>update_manager.backup.download.invalid_password</source>
<target>Ugyldigt password. Download af sikkerhedskopi er afvist.</target>
</segment>
</unit>
<unit id="AZOjnE0" name="update_manager.backup.docker_warning">
<segment state="translated">
<source>update_manager.backup.docker_warning</source>
<target>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.</target>
</segment>
</unit>
<unit id="kHKChQB" name="settings.ips.conrad">
<segment state="translated">
<source>settings.ips.conrad</source>
@ -12404,5 +12686,281 @@ Buerklin API-godkendelsesserver: 10 anmodninger/minut pr. IP-adresse</target>
<target>Opdatér til</target>
</segment>
</unit>
<unit id="XPhnMxn" name="part.gtin">
<segment state="translated">
<source>part.gtin</source>
<target>GTIN / EAN</target>
</segment>
</unit>
<unit id="TyykD7B" name="info_providers.capabilities.gtin">
<segment state="translated">
<source>info_providers.capabilities.gtin</source>
<target>GTIN / EAN</target>
</segment>
</unit>
<unit id="JBGly8p" name="part.table.gtin">
<segment state="translated">
<source>part.table.gtin</source>
<target>GTIN</target>
</segment>
</unit>
<unit id="0qHQof." name="scan_dialog.mode.gtin">
<segment state="translated">
<source>scan_dialog.mode.gtin</source>
<target>GTIN / EAN barcode</target>
</segment>
</unit>
<unit id="cmchX59" name="attachment_type.edit.allowed_targets">
<segment state="translated">
<source>attachment_type.edit.allowed_targets</source>
<target>Anvend kun til</target>
</segment>
</unit>
<unit id="t5R8p1l" name="attachment_type.edit.allowed_targets.help">
<segment state="translated">
<source>attachment_type.edit.allowed_targets.help</source>
<target>Gør kun denne bilagstype tilgængelig for bestemte elementklasser. Lad feltet stå tomt for at vise denne bilagstype for alle elementklasser.</target>
</segment>
</unit>
<unit id="LvlEUjC" name="orderdetails.edit.prices_includes_vat">
<segment state="translated">
<source>orderdetails.edit.prices_includes_vat</source>
<target>Pris inklusiv moms.</target>
</segment>
</unit>
<unit id="GUsVh5T" name="prices.incl_vat">
<segment state="translated">
<source>prices.incl_vat</source>
<target>Inkl. moms</target>
</segment>
</unit>
<unit id="3ipwaVQ" name="prices.excl_vat">
<segment state="translated">
<source>prices.excl_vat</source>
<target>Ekskl. moms</target>
</segment>
</unit>
<unit id="WDJ7EeF" name="settings.system.localization.prices_include_tax_by_default">
<segment state="translated">
<source>settings.system.localization.prices_include_tax_by_default</source>
<target>Priserne er som standard inklusive moms</target>
</segment>
</unit>
<unit id="01oGY_r" name="settings.system.localization.prices_include_tax_by_default.description">
<segment state="translated">
<source>settings.system.localization.prices_include_tax_by_default.description</source>
<target>Standardværdien for nyoprettede købsoplysninger, uanset om priserne inkluderer moms eller ej.</target>
</segment>
</unit>
<unit id="heWSnAH" name="part_lot.edit.last_stocktake_at">
<segment state="translated">
<source>part_lot.edit.last_stocktake_at</source>
<target>Seneste optælling</target>
</segment>
</unit>
<unit id=".LP93kG" name="perm.parts_stock.stocktake">
<segment state="translated">
<source>perm.parts_stock.stocktake</source>
<target>Lageropgørelse</target>
</segment>
</unit>
<unit id="Vnhrb5R" name="part.info.stocktake_modal.title">
<segment state="translated">
<source>part.info.stocktake_modal.title</source>
<target>Lagerbeholdning</target>
</segment>
</unit>
<unit id="WqOG7RK" name="part.info.stocktake_modal.expected_amount">
<segment state="translated">
<source>part.info.stocktake_modal.expected_amount</source>
<target>Forventet mængde</target>
</segment>
</unit>
<unit id="E7IbVN6" name="part.info.stocktake_modal.actual_amount">
<segment state="translated">
<source>part.info.stocktake_modal.actual_amount</source>
<target>Aktuel mængde</target>
</segment>
</unit>
<unit id="4GwSma7" name="log.part_stock_changed.stock_take">
<segment state="translated">
<source>log.part_stock_changed.stock_take</source>
<target>Lagerbeholdning</target>
</segment>
</unit>
<unit id="aRQPMW7" name="log.element_edited.changed_fields.last_stocktake_at">
<segment state="translated">
<source>log.element_edited.changed_fields.last_stocktake_at</source>
<target>Sidste lagerbeholdning</target>
</segment>
</unit>
<unit id="GNWhoTW" name="part.table.eda_reference">
<segment state="translated">
<source>part.table.eda_reference</source>
<target>EDA reference</target>
</segment>
</unit>
<unit id="tW4yCbf" name="part.table.eda_value">
<segment state="translated">
<source>part.table.eda_value</source>
<target>EDA-værdi</target>
</segment>
</unit>
<unit id="s1pgReC" name="settings.misc.kicad_eda.default_parameter_visibility">
<segment state="translated">
<source>settings.misc.kicad_eda.default_parameter_visibility</source>
<target>Standard EDA-synlighed for parametre</target>
</segment>
</unit>
<unit id="Z78QunV" name="settings.misc.kicad_eda.default_parameter_visibility.help">
<segment state="translated">
<source>settings.misc.kicad_eda.default_parameter_visibility.help</source>
<target>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.</target>
</segment>
</unit>
<unit id="J6pYnaC" name="settings.misc.kicad_eda.default_orderdetails_visibility">
<segment state="translated">
<source>settings.misc.kicad_eda.default_orderdetails_visibility</source>
<target>Standard EDA-synlighed for købsoplysninger</target>
</segment>
</unit>
<unit id="Hiye4C." name="settings.misc.kicad_eda.default_orderdetails_visibility.help">
<segment state="translated">
<source>settings.misc.kicad_eda.default_orderdetails_visibility.help</source>
<target>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.</target>
</segment>
</unit>
<unit id="aEgd0if" name="label_scanner.open">
<segment state="translated">
<source>label_scanner.open</source>
<target>Vis detaljer</target>
</segment>
</unit>
<unit id="vw_0Qws" name="label_scanner.db_part_found">
<segment state="translated">
<source>label_scanner.db_part_found</source>
<target>Database [part] fundet for barcode</target>
</segment>
</unit>
<unit id="zntajcd" name="label_scanner.part_can_be_created">
<segment state="translated">
<source>label_scanner.part_can_be_created</source>
<target>[Part] kan oprettes</target>
</segment>
</unit>
<unit id="cLTbd9w" name="label_scanner.part_can_be_created.help">
<segment state="translated">
<source>label_scanner.part_can_be_created.help</source>
<target>Der blev ikke fundet nogen matchende [part] i databasen, men du kan oprette en ny [part] baseret på denne stregkode.</target>
</segment>
</unit>
<unit id="FfHA3Yf" name="label_scanner.part_create_btn">
<segment state="translated">
<source>label_scanner.part_create_btn</source>
<target>Opret [part] fra barcode</target>
</segment>
</unit>
<unit id="xH258F." name="parts.create_from_scan.title">
<segment state="translated">
<source>parts.create_from_scan.title</source>
<target>Opret [part] ud fra labelscanning</target>
</segment>
</unit>
<unit id="8WZYwRJ" name="scan_dialog.mode.amazon">
<segment state="translated">
<source>scan_dialog.mode.amazon</source>
<target>Amazon barcode</target>
</segment>
</unit>
<unit id="BQWuR_G" name="settings.ips.canopy">
<segment state="translated">
<source>settings.ips.canopy</source>
<target>Canopy</target>
</segment>
</unit>
<unit id="44BfYzy" name="settings.ips.canopy.alwaysGetDetails">
<segment state="translated">
<source>settings.ips.canopy.alwaysGetDetails</source>
<target>Hent altid detaljer</target>
</segment>
</unit>
<unit id="so_ms3t" name="settings.ips.canopy.alwaysGetDetails.help">
<segment state="translated">
<source>settings.ips.canopy.alwaysGetDetails.help</source>
<target>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.</target>
</segment>
</unit>
<unit id="D055xh8" name="attachment.sandbox.warning">
<segment state="translated">
<source>attachment.sandbox.warning</source>
<target>ADVARSEL: Du ser en brugeruploadet vedhæftet fil. Dette er indhold, der ikke er tillid til. Vær forsigtig.</target>
</segment>
</unit>
<unit id="bRcdnJK" name="attachment.sandbox.back_to_partdb">
<segment state="translated">
<source>attachment.sandbox.back_to_partdb</source>
<target>Tilbage til Part-DB</target>
</segment>
</unit>
<unit id="MzyA7N8" name="settings.system.attachments.showHTMLAttachments">
<segment state="translated">
<source>settings.system.attachments.showHTMLAttachments</source>
<target>Vis uploadede HTML-filvedhæftninger (sandboxed)</target>
</segment>
</unit>
<unit id="V_LJkRy" name="settings.system.attachments.showHTMLAttachments.help">
<segment state="translated">
<source>settings.system.attachments.showHTMLAttachments.help</source>
<target>⚠️ 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.</target>
</segment>
</unit>
<unit id="BQo2xWi" name="attachment.sandbox.title">
<segment state="translated">
<source>attachment.sandbox.title</source>
<target>HTML [Vedhæftning]</target>
</segment>
</unit>
<unit id="sJ6v9uJ" name="attachment.sandbox.as_plain_text">
<segment state="translated">
<source>attachment.sandbox.as_plain_text</source>
<target>Vis som alm. tekst</target>
</segment>
</unit>
<unit id="Ehsj93c" name="modal.cancel">
<segment state="translated">
<source>modal.cancel</source>
<target>Annuller</target>
</segment>
</unit>
<unit id="jdpoFf2" name="update_manager.web_updates_allowed">
<segment state="translated">
<source>update_manager.web_updates_allowed</source>
<target>Web-opdateringer tilladt</target>
</segment>
</unit>
<unit id="bdWa7is" name="update_manager.backup_restore_allowed">
<segment state="translated">
<source>update_manager.backup_restore_allowed</source>
<target>Indlæsning af sikkerhedskopi (backup) tilladt</target>
</segment>
</unit>
<unit id="kllGQEN" name="update_manager.backup_download_allowed">
<segment state="translated">
<source>update_manager.backup_download_allowed</source>
<target>Download af sikkerhedskopi tilladt</target>
</segment>
</unit>
<unit id="b8JxfcX" name="part.create_from_info_provider.lot_filled_from_barcode">
<segment state="translated">
<source>part.create_from_info_provider.lot_filled_from_barcode</source>
<target>[Part_lot] oprettet fra stregkode: Kontroller venligst, om dataene er korrekte og ønskede.</target>
</segment>
</unit>
<unit id="F8pQuL9" name="project.bom_import.field_mapping.error.check_delimiter">
<segment state="translated">
<source>project.bom_import.field_mapping.error.check_delimiter</source>
<target>Felttilknytningsfejl: Kontroller, om du har valgt den rigtige tegn-afgrænser!</target>
</segment>
</unit>
</file>
</xliff>
</xliff>

View file

@ -2779,6 +2779,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr
<target>Name</target>
</segment>
</unit>
<unit id="sIvAlUe" name="part.table.si_value">
<segment state="translated">
<source>part.table.si_value</source>
<target>SI-Wert</target>
</segment>
</unit>
<unit id="rW_SFJE" name="part.table.id">
<segment state="translated">
<source>part.table.id</source>
@ -7211,6 +7217,18 @@ Element 1 -&gt; Element 1.2</target>
<target>Unterprojekte</target>
</segment>
</unit>
<unit id="prjTtlBP" name="project.info.total_build_price">
<segment state="translated">
<source>project.info.total_build_price</source>
<target>Gesamterstellpreis</target>
</segment>
</unit>
<unit id="prjUntBP" name="project.info.per_unit_price">
<segment state="translated">
<source>project.info.per_unit_price</source>
<target>pro Einheit</target>
</segment>
</unit>
<unit id="7nV.Cmd" name="project.info.bom_add_parts">
<segment state="translated">
<source>project.info.bom_add_parts</source>
@ -7235,6 +7253,12 @@ Element 1 -&gt; Element 1.2</target>
<target>Preis</target>
</segment>
</unit>
<unit id="bomExPrc" name="project.bom.ext_price">
<segment state="translated">
<source>project.bom.ext_price</source>
<target>Gesamtpreis</target>
</segment>
</unit>
<unit id="8tP3lQI" name="part.info.withdraw_modal.title.withdraw">
<segment state="translated">
<source>part.info.withdraw_modal.title.withdraw</source>
@ -10028,6 +10052,90 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<target>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.</target>
</segment>
</unit>
<unit id="e2e7mR1" name="settings.misc.kicad_eda.editor.title">
<segment state="translated">
<source>settings.misc.kicad_eda.editor.title</source>
<target>KiCad Autovervollständigungslisten</target>
</segment>
</unit>
<unit id="qjv1VVx" name="settings.misc.kicad_eda.editor.link">
<segment state="translated">
<source>settings.misc.kicad_eda.editor.link</source>
<target>Autovervollständigungseinstellungen</target>
</segment>
</unit>
<unit id="f0qkcqg" name="settings.misc.kicad_eda.editor.description">
<segment state="translated">
<source>settings.misc.kicad_eda.editor.description</source>
<target>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.</target>
</segment>
</unit>
<unit id="AS3yDlb" name="settings.misc.kicad_eda.editor.footprints">
<segment state="translated">
<source>settings.misc.kicad_eda.editor.footprints</source>
<target>Footprint-Liste</target>
</segment>
</unit>
<unit id="Jj_YR7n" name="settings.misc.kicad_eda.editor.footprints.help">
<segment state="translated">
<source>settings.misc.kicad_eda.editor.footprints.help</source>
<target>Ein Eintrag pro Zeile. Wird als Autovervollständigungsvorschlag für KiCad-Footprintfelder verwendet.</target>
</segment>
</unit>
<unit id="ELd3KQK" name="settings.misc.kicad_eda.editor.symbols">
<segment state="translated">
<source>settings.misc.kicad_eda.editor.symbols</source>
<target>Symbolliste</target>
</segment>
</unit>
<unit id="A9TOJgM" name="settings.misc.kicad_eda.editor.symbols.help">
<segment state="translated">
<source>settings.misc.kicad_eda.editor.symbols.help</source>
<target>Ein Eintrag pro Zeile. Wird als Autovervollständigungsvorschlag für KiCad-Symbolfelder verwendet.</target>
</segment>
</unit>
<unit id="tWYlL0u" name="settings.misc.kicad_eda.use_custom_list">
<segment state="translated">
<source>settings.misc.kicad_eda.use_custom_list</source>
<target>Benutzerdefinierte Autovervollständigungslisten verwenden</target>
</segment>
</unit>
<unit id="v0LK7n6" name="settings.misc.kicad_eda.use_custom_list.help">
<segment state="translated">
<source>settings.misc.kicad_eda.use_custom_list.help</source>
<target>Wenn aktiviert, verwendet die KiCad Autovervollständigung public/kicad/footprints_custom.txt und public/kicad/symbols_custom.txt anstelle der automatisch generierten Standarddateien.</target>
</segment>
</unit>
<unit id="Yl_fqfV" name="settings.misc.kicad_eda.editor.custom_footprints">
<segment state="translated">
<source>settings.misc.kicad_eda.editor.custom_footprints</source>
<target>Benutzerdefinierte Footprint-Liste</target>
</segment>
</unit>
<unit id="GuD2JcQ" name="settings.misc.kicad_eda.editor.custom_symbols">
<segment state="translated">
<source>settings.misc.kicad_eda.editor.custom_symbols</source>
<target>Benutzerdefinierte Symbolliste</target>
</segment>
</unit>
<unit id="k6m9b5F" name="settings.misc.kicad_eda.editor.default_footprints">
<segment state="translated">
<source>settings.misc.kicad_eda.editor.default_footprints</source>
<target>Standard Footprint-Liste</target>
</segment>
</unit>
<unit id="bKkF8mM" name="settings.misc.kicad_eda.editor.default_symbols">
<segment state="translated">
<source>settings.misc.kicad_eda.editor.default_symbols</source>
<target>Standardsymboliste</target>
</segment>
</unit>
<unit id="mIj_i4E" name="settings.misc.kicad_eda.editor.default_files_help">
<segment state="translated">
<source>settings.misc.kicad_eda.editor.default_files_help</source>
<target>Automatisch generierte Datei wird nur zur Referenz angezeigt. Änderungen müssen in der benutzerdefinierten Liste vorgenommen werden.</target>
</segment>
</unit>
<unit id="VwvmcWE" name="settings.behavior.sidebar">
<segment state="translated">
<source>settings.behavior.sidebar</source>

View file

@ -2780,6 +2780,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can
<target>Name</target>
</segment>
</unit>
<unit id="sIvAlUe" name="part.table.si_value">
<segment state="translated">
<source>part.table.si_value</source>
<target>SI Value</target>
</segment>
</unit>
<unit id="rW_SFJE" name="part.table.id">
<segment state="translated">
<source>part.table.id</source>
@ -7212,6 +7218,18 @@ Element 1 -&gt; Element 1.2</target>
<target>Subprojects</target>
</segment>
</unit>
<unit id="prjTtlBP" name="project.info.total_build_price">
<segment state="translated">
<source>project.info.total_build_price</source>
<target>Total build price</target>
</segment>
</unit>
<unit id="prjUntBP" name="project.info.per_unit_price">
<segment state="translated">
<source>project.info.per_unit_price</source>
<target>per unit</target>
</segment>
</unit>
<unit id="7nV.Cmd" name="project.info.bom_add_parts">
<segment state="translated">
<source>project.info.bom_add_parts</source>
@ -7236,6 +7254,12 @@ Element 1 -&gt; Element 1.2</target>
<target>Price</target>
</segment>
</unit>
<unit id="bomExPrc" name="project.bom.ext_price">
<segment state="translated">
<source>project.bom.ext_price</source>
<target>Extended Price</target>
</segment>
</unit>
<unit id="8tP3lQI" name="part.info.withdraw_modal.title.withdraw">
<segment state="translated">
<source>part.info.withdraw_modal.title.withdraw</source>

View file

@ -7259,6 +7259,12 @@ Elemento 3</target>
<target>Precio</target>
</segment>
</unit>
<unit id="bomExPrc" name="project.bom.ext_price">
<segment state="initial">
<source>project.bom.ext_price</source>
<target>Extended Price</target>
</segment>
</unit>
<unit id="8tP3lQI" name="part.info.withdraw_modal.title.withdraw">
<segment state="translated">
<source>part.info.withdraw_modal.title.withdraw</source>

File diff suppressed because it is too large Load diff

View file

@ -7198,6 +7198,12 @@
<target>Ár</target>
</segment>
</unit>
<unit id="bomExPrc" name="project.bom.ext_price">
<segment state="initial">
<source>project.bom.ext_price</source>
<target>Extended Price</target>
</segment>
</unit>
<unit id="8tP3lQI" name="part.info.withdraw_modal.title.withdraw">
<segment state="translated">
<source>part.info.withdraw_modal.title.withdraw</source>

View file

@ -7186,6 +7186,12 @@ Element 3</target>
<target>Prezzo</target>
</segment>
</unit>
<unit id="bomExPrc" name="project.bom.ext_price">
<segment state="initial">
<source>project.bom.ext_price</source>
<target>Extended Price</target>
</segment>
</unit>
<unit id="8tP3lQI" name="part.info.withdraw_modal.title.withdraw">
<segment state="translated">
<source>part.info.withdraw_modal.title.withdraw</source>

View file

@ -7256,6 +7256,12 @@ Element 3</target>
<target>Cena</target>
</segment>
</unit>
<unit id="bomExPrc" name="project.bom.ext_price">
<segment state="initial">
<source>project.bom.ext_price</source>
<target>Extended Price</target>
</segment>
</unit>
<unit id="8tP3lQI" name="part.info.withdraw_modal.title.withdraw">
<segment state="translated">
<source>part.info.withdraw_modal.title.withdraw</source>

View file

@ -7260,6 +7260,12 @@
<target>Цена</target>
</segment>
</unit>
<unit id="bomExPrc" name="project.bom.ext_price">
<segment state="initial">
<source>project.bom.ext_price</source>
<target>Extended Price</target>
</segment>
</unit>
<unit id="8tP3lQI" name="part.info.withdraw_modal.title.withdraw">
<segment state="translated">
<source>part.info.withdraw_modal.title.withdraw</source>

View file

@ -7259,6 +7259,12 @@ Element 3</target>
<target>价格</target>
</segment>
</unit>
<unit id="bomExPrc" name="project.bom.ext_price">
<segment state="initial">
<source>project.bom.ext_price</source>
<target>Extended Price</target>
</segment>
</unit>
<unit id="hO.xnng" name="part.info.withdraw_modal.title.withdraw">
<segment state="translated">
<source>part.info.withdraw_modal.title.withdraw</source>

View file

@ -4,31 +4,31 @@
<unit id="cRbk.cm" name="part.master_attachment.must_be_picture">
<segment state="translated">
<source>part.master_attachment.must_be_picture</source>
<target>Forhåndsvisnings-bilaget skal være et rigtigt billede!</target>
<target>Forhåndsvisningsvedhæftningen skal være et gyldigt billede!</target>
</segment>
</unit>
<unit id="v8HkcJB" name="structural.entity.unique_name">
<segment state="translated">
<source>structural.entity.unique_name</source>
<target>Der eksisterer allerede et element med dette navn på dette niveau!</target>
<target>Et element med dette navn findes allerede på dette niveau!</target>
</segment>
</unit>
<unit id="dW7b2B_" name="parameters.validator.min_lesser_typical">
<segment state="translated">
<source>parameters.validator.min_lesser_typical</source>
<target>Værdi skal være mindre end eller lig med den typiske værdi ({{ compared_value }}).</target>
<target>Værdien skal være mindre end eller lig med den typiske værdi ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="Yfp2uC5" name="parameters.validator.min_lesser_max">
<segment state="translated">
<source>parameters.validator.min_lesser_max</source>
<target>Værdi skal være mindre end maksumumværdien ({{ compared_value }}).</target>
<target>Værdien skal være mindre end den maksimale værdi ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="P6b.8Ou" name="parameters.validator.max_greater_typical">
<segment state="translated">
<source>parameters.validator.max_greater_typical</source>
<target>Værdi skal være større eller lig med den typiske værdi ({{ compared_value }}).</target>
<target>Værdien skal være større end eller lig med den typiske værdi ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="P41193Y" name="validator.user.username_already_used">
@ -247,5 +247,11 @@
<target>Der er allerede defineret en oversættelse for denne type og sprog!</target>
</segment>
</unit>
<unit id="zT_j_oQ" name="validator.invalid_gtin">
<segment state="translated">
<source>validator.invalid_gtin</source>
<target>Dette er ikke en gyldig GTIN / EAN!</target>
</segment>
</unit>
</file>
</xliff>
</xliff>

View file

@ -112,7 +112,7 @@
<unit id="gZ5FFL1" name="part.ipn.must_be_unique">
<segment state="translated">
<source>part.ipn.must_be_unique</source>
<target>Le numéro de pièce interne doit être unique.{{ value }} est déjà utilisé !</target>
<target>Le numéro de pièce interne doit être unique. {{ value }} est déjà utilisé !</target>
</segment>
</unit>
<unit id="P31Yg.d" name="validator.project.bom_entry.name_or_part_needed">
@ -223,13 +223,13 @@
<target>Suite à des limitations techniques, il n'est pas possible de sélectionner une date après le 19-01-2038 sur les systèmes 32-bit !</target>
</segment>
</unit>
<unit id="89nojXY" name="validator.fileSize.invalidFormat">
<unit id="iM9yb_p" name="validator.fileSize.invalidFormat">
<segment state="translated">
<source>validator.fileSize.invalidFormat</source>
<target>Taille de fichier invalide. Utilisez un nombre avec le suffixe K, G, M pour Kilo, Mega ou Gigabytes.</target>
</segment>
</unit>
<unit id="iXcU7ce" name="validator.invalid_range">
<unit id="ZFxQ0BZ" name="validator.invalid_range">
<segment state="translated">
<source>validator.invalid_range</source>
<target>L'écart fournit est invalide !</target>
@ -241,5 +241,17 @@
<target>Code invalide. Vérifiez que votre application d'authentification est paramétrée correctement que le serveur et périphérique d'authentification ont l'heure correcte.</target>
</segment>
</unit>
<unit id="I330cr5" name="settings.synonyms.type_synonyms.collection_type.duplicate">
<segment state="translated">
<source>settings.synonyms.type_synonyms.collection_type.duplicate</source>
<target>Il existe déjà une traduction définit pour ce type et langage !</target>
</segment>
</unit>
<unit id="zT_j_oQ" name="validator.invalid_gtin">
<segment state="translated">
<source>validator.invalid_gtin</source>
<target>Cela n'est pas un GTIN / EAN valide !</target>
</segment>
</unit>
</file>
</xliff>
</xliff>

View file

@ -2174,9 +2174,9 @@
integrity sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==
"@simple-git/argv-parser@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@simple-git/argv-parser/-/argv-parser-1.1.0.tgz#6680aed3fa68f131ca0d7efa90e52b5b23ca3183"
integrity sha512-sUKOu2lb5vGIWADNNLpscyj07DAeQZU3KLbnE2Tj53tW6BbDQKMly2CCfnR4oYzqtRELCPWfwaPg+Q0T8qfKBg==
version "1.1.1"
resolved "https://registry.yarnpkg.com/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz#275b839c6eeb5030872c73b1ea839a416885da9d"
integrity sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==
dependencies:
"@simple-git/args-pathspec" "^1.0.3"
@ -2732,9 +2732,9 @@ base64-js@^1.1.2, base64-js@^1.3.0:
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.10.12:
version "2.10.18"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz#565745085ba7743af7d4072707ad132db3a5a42f"
integrity sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==
version "2.10.19"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz#7697721c22f94f66195d0c34299b1a91e3299493"
integrity sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==
big.js@^5.2.2:
version "5.2.2"
@ -2799,6 +2799,13 @@ browser-stdout@^1.3.1:
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
browserify-zlib@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==
dependencies:
pako "~1.0.5"
browserslist@^4.0.0, browserslist@^4.24.0, browserslist@^4.28.1, browserslist@^4.28.2:
version "4.28.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.2.tgz#f50b65362ef48974ca9f50b3680566d786b811d2"
@ -2862,9 +2869,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001782:
version "1.0.30001787"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz#fd25c5e42e2d35df5c75eddda00d15d9c0c68f81"
integrity sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==
version "1.0.30001788"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz#31e97d1bfec332b3f2d7eea7781460c97629b3bf"
integrity sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==
ccount@^2.0.0:
version "2.0.1"
@ -3488,9 +3495,9 @@ domhandler@^5.0.2, domhandler@^5.0.3:
domelementtype "^2.3.0"
dompurify@^3.0.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.3.tgz#680cae8af3e61320ddf3666a3bc843f7b291b2b6"
integrity sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==
version "3.4.0"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.0.tgz#b1fc33ebdadb373241621e0a30e4ad81573dfd0b"
integrity sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==
optionalDependencies:
"@types/trusted-types" "^2.0.7"
@ -3518,9 +3525,9 @@ eastasianwidth@^0.2.0:
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
electron-to-chromium@^1.5.328:
version "1.5.335"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz#0b957cea44ef86795c227c616d16b4803d119daa"
integrity sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==
version "1.5.337"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.337.tgz#73051b9160d3960eea398d73323184cbdd6914de"
integrity sha512-15gKW9mRUNP9RdzhedJNypFUxtYWSXohFz2nTLzM272xbRXHws68kNDzyATG3qej+vUj/7Sn9hf5XTDh0XK6/w==
emoji-regex@^8.0.0:
version "8.0.0"
@ -3854,9 +3861,9 @@ get-stream@^6.0.0:
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
get-tsconfig@^4.10.1:
version "4.13.7"
resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.13.7.tgz#b9d8b199b06033ceeea1a93df7ea5765415089bc"
integrity sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==
version "4.14.0"
resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz#985d85c52a9903864280ccc2448d413fbf1efed8"
integrity sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==
dependencies:
resolve-pkg-maps "^1.0.0"
@ -4406,7 +4413,7 @@ json-formatter-js@^2.3.4:
resolved "https://registry.yarnpkg.com/json-formatter-js/-/json-formatter-js-2.5.23.tgz#b7dd0a1da7e6cbea8e76743d7d8dc1238866cc73"
integrity sha512-Cbm8wHXjo/C56aCePP1VuKvjxoMEmL7g7Ckss1oWFFlCsvOEEbye1kTeaNNaqba1Cl6YpIOYAnK65pUQ8mDIUQ==
json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1:
json-parse-even-better-errors@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
@ -5062,17 +5069,10 @@ micromatch@^4.0.0, micromatch@^4.0.8:
braces "^3.0.3"
picomatch "^2.3.1"
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.27:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime-db@^1.54.0:
version "1.54.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
mimic-fn@^2.1.0:
version "2.1.0"
@ -5421,7 +5421,7 @@ pako@^0.2.5:
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==
pako@~1.0.2:
pako@~1.0.2, pako@~1.0.5:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
@ -5528,9 +5528,11 @@ plural-forms@^0.5.5:
integrity sha512-rJw4xp22izsfJOVqta5Hyvep2lR3xPkFUtj7dyQtpf/FbxUiX7PQCajTn2EHDRylizH5N/Uqqodfdu22I0ju+g==
png-js@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d"
integrity sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==
version "1.1.0"
resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.1.0.tgz#60a135216601f807b88a6d61ac93bd42a32c5ee1"
integrity sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==
dependencies:
browserify-zlib "^0.2.0"
pofile@^1.1.4:
version "1.1.4"
@ -5832,9 +5834,9 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.0.0, postcss@^8.2.14, postcss@^8.4.12, postcss@^8.4.40:
version "8.5.9"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.9.tgz#f6ee9e0b94f0f19c97d2f172bfbd7fc71fe1cca4"
integrity sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==
version "8.5.10"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.10.tgz#8992d8c30acf3f12169e7c09514a12fed7e48356"
integrity sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==
dependencies:
nanoid "^3.3.11"
picocolors "^1.1.1"
@ -6938,9 +6940,9 @@ webpack-sources@^3.0.0, webpack-sources@^3.3.4:
integrity sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==
webpack@^5.74.0:
version "5.106.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.106.1.tgz#0a3eeb43a50e4f67fbecd206e1e6fc2c89fc2b6f"
integrity sha512-EW8af29ak8Oaf4T8k8YsajjrDBDYgnKZ5er6ljWFJsXABfTNowQfvHLftwcepVgdz+IoLSdEAbBiM9DFXoll9w==
version "5.106.2"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.106.2.tgz#ca8174b4fd80f055cc5a45fcc5577d6db76c8ac5"
integrity sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"
@ -6958,9 +6960,8 @@ webpack@^5.74.0:
events "^3.2.0"
glob-to-regexp "^0.4.1"
graceful-fs "^4.2.11"
json-parse-even-better-errors "^2.3.1"
loader-runner "^4.3.1"
mime-types "^2.1.27"
mime-db "^1.54.0"
neo-async "^2.6.2"
schema-utils "^4.3.3"
tapable "^2.3.0"