Compare commits

..

1 commit

Author SHA1 Message Date
github-actions[bot]
7217ea8521 Update KiCad symbols and footprints lists
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
2026-03-09 04:25:15 +00:00
31 changed files with 1275 additions and 2196 deletions

View file

@ -36,7 +36,7 @@ jobs:
- -
name: Docker meta name: Docker meta
id: docker_meta id: docker_meta
uses: docker/metadata-action@v6 uses: docker/metadata-action@v5
with: with:
# list of Docker images to use as base name for tags # list of Docker images to use as base name for tags
images: | images: |
@ -66,11 +66,11 @@ jobs:
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v3
- -
name: Login to DockerHub name: Login to DockerHub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v4 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@ -78,7 +78,7 @@ jobs:
- -
name: Build and push by digest name: Build and push by digest
id: build id: build
uses: docker/build-push-action@v7 uses: docker/build-push-action@v6
with: with:
context: . context: .
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
@ -121,12 +121,12 @@ jobs:
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v3
- -
name: Docker meta name: Docker meta
id: docker_meta id: docker_meta
uses: docker/metadata-action@v6 uses: docker/metadata-action@v5
with: with:
images: | images: |
jbtronics/part-db1 jbtronics/part-db1
@ -142,7 +142,7 @@ jobs:
- -
name: Login to DockerHub name: Login to DockerHub
uses: docker/login-action@v4 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}

View file

@ -36,7 +36,7 @@ jobs:
- -
name: Docker meta name: Docker meta
id: docker_meta id: docker_meta
uses: docker/metadata-action@v6 uses: docker/metadata-action@v5
with: with:
# list of Docker images to use as base name for tags # list of Docker images to use as base name for tags
images: | images: |
@ -66,11 +66,11 @@ jobs:
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v3
- -
name: Login to DockerHub name: Login to DockerHub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v4 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@ -78,7 +78,7 @@ jobs:
- -
name: Build and push by digest name: Build and push by digest
id: build id: build
uses: docker/build-push-action@v7 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: Dockerfile-frankenphp file: Dockerfile-frankenphp
@ -122,12 +122,12 @@ jobs:
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v3
- -
name: Docker meta name: Docker meta
id: docker_meta id: docker_meta
uses: docker/metadata-action@v6 uses: docker/metadata-action@v5
with: with:
images: | images: |
partdborg/part-db partdborg/part-db
@ -143,7 +143,7 @@ jobs:
- -
name: Login to DockerHub name: Login to DockerHub
uses: docker/login-action@v4 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}

View file

@ -1 +1 @@
2.9.1 2.9.0

795
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1712,7 +1712,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* length?: scalar|Param|null, // Default: 5 * length?: scalar|Param|null, // Default: 5
* width?: scalar|Param|null, // Default: 130 * width?: scalar|Param|null, // Default: 130
* height?: scalar|Param|null, // Default: 50 * height?: scalar|Param|null, // Default: 50
* font?: scalar|Param|null, // Default: "E:\\PHP\\Part-DB-server\\vendor\\gregwar\\captcha-bundle\\DependencyInjection/../Generator/Font/captcha.ttf" * font?: scalar|Param|null, // Default: "C:\\Users\\mail\\Documents\\PHP\\Part-DB-server\\vendor\\gregwar\\captcha-bundle\\DependencyInjection/../Generator/Font/captcha.ttf"
* keep_value?: scalar|Param|null, // Default: false * keep_value?: scalar|Param|null, // Default: false
* charset?: scalar|Param|null, // Default: "abcdefhjkmnprstuvwxyz23456789" * charset?: scalar|Param|null, // Default: "abcdefhjkmnprstuvwxyz23456789"
* as_file?: scalar|Param|null, // Default: false * as_file?: scalar|Param|null, // Default: false
@ -2390,9 +2390,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* serialize_payload_fields?: mixed, // Set to null to serialize all payload fields when a validation error is thrown, or set the fields you want to include explicitly. // Default: [] * serialize_payload_fields?: mixed, // Set to null to serialize all payload fields when a validation error is thrown, or set the fields you want to include explicitly. // Default: []
* query_parameter_validation?: bool|Param, // Deprecated: Will be removed in API Platform 5.0. // Default: true * query_parameter_validation?: bool|Param, // Deprecated: Will be removed in API Platform 5.0. // Default: true
* }, * },
* jsonapi?: array{
* use_iri_as_id?: bool|Param, // Set to false to use entity identifiers instead of IRIs as the "id" field in JSON:API responses. // Default: true
* },
* eager_loading?: bool|array{ * eager_loading?: bool|array{
* enabled?: bool|Param, // Default: true * enabled?: bool|Param, // Default: true
* fetch_partial?: bool|Param, // Fetch only partial data according to serialization groups. If enabled, Doctrine ORM entities will not work as expected if any of the other fields are used. // Default: false * fetch_partial?: bool|Param, // Fetch only partial data according to serialization groups. If enabled, Doctrine ORM entities will not work as expected if any of the other fields are used. // Default: false
@ -2404,12 +2401,11 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* enable_json_streamer?: bool|Param, // Enable json streamer. // Default: false * enable_json_streamer?: bool|Param, // Enable json streamer. // Default: false
* enable_swagger_ui?: bool|Param, // Enable Swagger UI // Default: true * enable_swagger_ui?: bool|Param, // Enable Swagger UI // Default: true
* enable_re_doc?: bool|Param, // Enable ReDoc // Default: true * enable_re_doc?: bool|Param, // Enable ReDoc // Default: true
* enable_scalar?: bool|Param, // Enable Scalar API Reference // Default: true
* enable_entrypoint?: bool|Param, // Enable the entrypoint // Default: true * enable_entrypoint?: bool|Param, // Enable the entrypoint // Default: true
* enable_docs?: bool|Param, // Enable the docs // Default: true * enable_docs?: bool|Param, // Enable the docs // Default: true
* enable_profiler?: bool|Param, // Enable the data collector and the WebProfilerBundle integration. // Default: true * enable_profiler?: bool|Param, // Enable the data collector and the WebProfilerBundle integration. // Default: true
* enable_phpdoc_parser?: bool|Param, // Enable resource metadata collector using PHPStan PhpDocParser. // Default: true * enable_phpdoc_parser?: bool|Param, // Enable resource metadata collector using PHPStan PhpDocParser. // Default: true
* enable_link_security?: bool|Param, // Deprecated: This option is always enabled and will be removed in API Platform 5.0. // Enable security for Links (sub resources). // Default: true * enable_link_security?: bool|Param, // Enable security for Links (sub resources) // Default: false
* collection?: array{ * collection?: array{
* exists_parameter_name?: scalar|Param|null, // The name of the query parameter to filter on nullable field values. // Default: "exists" * exists_parameter_name?: scalar|Param|null, // The name of the query parameter to filter on nullable field values. // Default: "exists"
* order?: scalar|Param|null, // The default order of results. // Default: "ASC" * order?: scalar|Param|null, // The default order of results. // Default: "ASC"
@ -2493,7 +2489,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* max_header_length?: int|Param, // Max header length supported by the cache server. // Default: 7500 * max_header_length?: int|Param, // Max header length supported by the cache server. // Default: 7500
* request_options?: mixed, // To pass options to the client charged with the request. // Default: [] * request_options?: mixed, // To pass options to the client charged with the request. // Default: []
* purger?: scalar|Param|null, // Specify a purger to use (available values: "api_platform.http_cache.purger.varnish.ban", "api_platform.http_cache.purger.varnish.xkey", "api_platform.http_cache.purger.souin"). // Default: "api_platform.http_cache.purger.varnish" * purger?: scalar|Param|null, // Specify a purger to use (available values: "api_platform.http_cache.purger.varnish.ban", "api_platform.http_cache.purger.varnish.xkey", "api_platform.http_cache.purger.souin"). // Default: "api_platform.http_cache.purger.varnish"
* xkey?: array{ // Deprecated: The "xkey" configuration is deprecated, use your own purger to customize surrogate keys or the appropriate parameters. * xkey?: array{ // Deprecated: The "xkey" configuration is deprecated, use your own purger to customize surrogate keys or the appropriate paramters.
* glue?: scalar|Param|null, // xkey glue between keys // Default: " " * glue?: scalar|Param|null, // xkey glue between keys // Default: " "
* }, * },
* }, * },
@ -2509,9 +2505,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* elasticsearch?: bool|array{ * elasticsearch?: bool|array{
* enabled?: bool|Param, // Default: false * enabled?: bool|Param, // Default: false
* hosts?: list<scalar|Param|null>, * hosts?: list<scalar|Param|null>,
* ssl_ca_bundle?: scalar|Param|null, // Path to the SSL CA bundle file for Elasticsearch SSL verification. // Default: null
* ssl_verification?: bool|Param, // Enable or disable SSL verification for Elasticsearch connections. // Default: true
* client?: "elasticsearch"|"opensearch"|Param, // The search engine client to use: "elasticsearch" or "opensearch". // Default: "elasticsearch"
* }, * },
* openapi?: array{ * openapi?: array{
* contact?: array{ * contact?: array{
@ -2530,18 +2523,12 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* identifier?: scalar|Param|null, // An SPDX license expression for the API. The identifier field is mutually exclusive of the url field. // Default: null * identifier?: scalar|Param|null, // An SPDX license expression for the API. The identifier field is mutually exclusive of the url field. // Default: null
* }, * },
* swagger_ui_extra_configuration?: mixed, // To pass extra configuration to Swagger UI, like docExpansion or filter. // Default: [] * swagger_ui_extra_configuration?: mixed, // To pass extra configuration to Swagger UI, like docExpansion or filter. // Default: []
* scalar_extra_configuration?: mixed, // To pass extra configuration to Scalar API Reference, like theme or darkMode. // Default: []
* overrideResponses?: bool|Param, // Whether API Platform adds automatic responses to the OpenAPI documentation. // Default: true * overrideResponses?: bool|Param, // Whether API Platform adds automatic responses to the OpenAPI documentation. // Default: true
* error_resource_class?: scalar|Param|null, // The class used to represent errors in the OpenAPI documentation. // Default: null * error_resource_class?: scalar|Param|null, // The class used to represent errors in the OpenAPI documentation. // Default: null
* validation_error_resource_class?: scalar|Param|null, // The class used to represent validation errors in the OpenAPI documentation. // Default: null * validation_error_resource_class?: scalar|Param|null, // The class used to represent validation errors in the OpenAPI documentation. // Default: null
* }, * },
* maker?: bool|array{ * maker?: bool|array{
* enabled?: bool|Param, // Default: true * enabled?: bool|Param, // Default: true
* namespace_prefix?: scalar|Param|null, // Add a prefix to all maker generated classes. e.g set it to "Api" to set the maker namespace to "App\Api\" (if the maker.root_namespace config is App). e.g. App\Api\State\MyStateProcessor // Default: ""
* },
* mcp?: bool|array{
* enabled?: bool|Param, // Default: true
* format?: scalar|Param|null, // The serialization format used for MCP tool input/output. Must be a format registered in api_platform.formats (e.g. "jsonld", "json", "jsonapi"). // Default: "jsonld"
* }, * },
* exception_to_status?: array<string, int|Param>, * exception_to_status?: array<string, int|Param>,
* formats?: array<string, array{ // Default: {"jsonld":{"mime_types":["application/ld+json"]}} * formats?: array<string, array{ // Default: {"jsonld":{"mime_types":["application/ld+json"]}}
@ -2626,37 +2613,12 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* rules?: mixed, * rules?: mixed,
* policy?: mixed, * policy?: mixed,
* middleware?: mixed, * middleware?: mixed,
* parameters?: array<string, array{ // Default: [] * parameters?: mixed,
* key?: mixed,
* schema?: mixed,
* open_api?: mixed,
* provider?: mixed,
* filter?: mixed,
* property?: mixed,
* description?: mixed,
* properties?: mixed,
* required?: mixed,
* priority?: mixed,
* hydra?: mixed,
* constraints?: mixed,
* security?: mixed,
* security_message?: mixed,
* extra_properties?: mixed,
* filter_context?: mixed,
* native_type?: mixed,
* cast_to_array?: mixed,
* cast_to_native_type?: mixed,
* cast_fn?: mixed,
* default?: mixed,
* filter_class?: mixed,
* ...<mixed>
* }>,
* strict_query_parameter_validation?: mixed, * strict_query_parameter_validation?: mixed,
* hide_hydra_operation?: mixed, * hide_hydra_operation?: mixed,
* json_stream?: mixed, * json_stream?: mixed,
* extra_properties?: mixed, * extra_properties?: mixed,
* map?: mixed, * map?: mixed,
* mcp?: mixed,
* route_name?: mixed, * route_name?: mixed,
* errors?: mixed, * errors?: mixed,
* read?: mixed, * read?: mixed,
@ -2664,7 +2626,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* validate?: mixed, * validate?: mixed,
* write?: mixed, * write?: mixed,
* serialize?: mixed, * serialize?: mixed,
* content_negotiation?: mixed,
* priority?: mixed, * priority?: mixed,
* name?: mixed, * name?: mixed,
* allow_create?: mixed, * allow_create?: mixed,

View file

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

View file

@ -1,4 +1,4 @@
# Generated on Mon Mar 30 04:50:41 UTC 2026 # Generated on Mon Mar 9 04:24:12 UTC 2026
# This file contains all symbols available in the offical KiCAD library # This file contains all symbols available in the offical KiCAD library
4xxx:14528 4xxx:14528
4xxx:14529 4xxx:14529
@ -15731,7 +15731,6 @@ Power_Management:RT9742AGJ5F
Power_Management:RT9742ANGJ5F Power_Management:RT9742ANGJ5F
Power_Management:RT9742BGJ5F Power_Management:RT9742BGJ5F
Power_Management:RT9742BNGJ5F Power_Management:RT9742BNGJ5F
Power_Management:RT9742SNGV
Power_Management:SN6505ADBV Power_Management:SN6505ADBV
Power_Management:SN6505BDBV Power_Management:SN6505BDBV
Power_Management:SN6507DGQ Power_Management:SN6507DGQ
@ -18693,7 +18692,6 @@ Regulator_Linear:TPS7A0530PDBZ
Regulator_Linear:TPS7A0531PDBV Regulator_Linear:TPS7A0531PDBV
Regulator_Linear:TPS7A0533PDBV Regulator_Linear:TPS7A0533PDBV
Regulator_Linear:TPS7A0533PDBZ Regulator_Linear:TPS7A0533PDBZ
Regulator_Linear:TPS7A20xxxDBV
Regulator_Linear:TPS7A20xxxDQN Regulator_Linear:TPS7A20xxxDQN
Regulator_Linear:TPS7A3301RGW Regulator_Linear:TPS7A3301RGW
Regulator_Linear:TPS7A39 Regulator_Linear:TPS7A39

View file

@ -240,8 +240,7 @@ class ProjectController extends AbstractController
} }
// Detect fields and get suggestions // Detect fields and get suggestions
$detected_delimiter = $BOMImporter->detectDelimiter($file_content); $detected_fields = $BOMImporter->detectFields($file_content);
$detected_fields = $BOMImporter->detectFields($file_content, $detected_delimiter);
$suggested_mapping = $BOMImporter->getSuggestedFieldMapping($detected_fields); $suggested_mapping = $BOMImporter->getSuggestedFieldMapping($detected_fields);
// Create mapping of original field names to sanitized field names for template // Create mapping of original field names to sanitized field names for template
@ -258,7 +257,7 @@ class ProjectController extends AbstractController
$builder->add('delimiter', ChoiceType::class, [ $builder->add('delimiter', ChoiceType::class, [
'label' => 'project.bom_import.delimiter', 'label' => 'project.bom_import.delimiter',
'required' => true, 'required' => true,
'data' => $detected_delimiter, 'data' => ',',
'choices' => [ 'choices' => [
'project.bom_import.delimiter.comma' => ',', 'project.bom_import.delimiter.comma' => ',',
'project.bom_import.delimiter.semicolon' => ';', 'project.bom_import.delimiter.semicolon' => ';',

View file

@ -721,36 +721,26 @@ class BOMImporter
return $mapped; return $mapped;
} }
/**
* Try to detect the separator used in the CSV data by analyzing the first line and counting occurrences of common delimiters.
* @param string $data
* @return string
*/
public function detectDelimiter(string $data): string
{
$delimiters = [',', ';', "\t"];
$lines = explode("\n", $data, 2);
$header_line = $lines[0] ?? '';
$delimiter_counts = [];
foreach ($delimiters as $delim) {
$delimiter_counts[$delim] = substr_count($header_line, $delim);
}
// Choose the delimiter with the highest count, default to comma if all are zero
$max_count = max($delimiter_counts);
$delimiter = array_search($max_count, $delimiter_counts, true);
if ($max_count === 0 || $delimiter === false) {
$delimiter = ',';
}
return $delimiter;
}
/** /**
* Detect available fields in CSV data for field mapping UI * Detect available fields in CSV data for field mapping UI
*/ */
public function detectFields(string $data, ?string $delimiter = null): array public function detectFields(string $data, ?string $delimiter = null): array
{ {
if ($delimiter === null) { if ($delimiter === null) {
$delimiter = $this->detectDelimiter($data); // Detect delimiter by counting occurrences in the first row (header)
$delimiters = [',', ';', "\t"];
$lines = explode("\n", $data, 2);
$header_line = $lines[0] ?? '';
$delimiter_counts = [];
foreach ($delimiters as $delim) {
$delimiter_counts[$delim] = substr_count($header_line, $delim);
}
// Choose the delimiter with the highest count, default to comma if all are zero
$max_count = max($delimiter_counts);
$delimiter = array_search($max_count, $delimiter_counts, true);
if ($max_count === 0 || $delimiter === false) {
$delimiter = ',';
}
} }
// Handle potential BOM (Byte Order Mark) at the beginning // Handle potential BOM (Byte Order Mark) at the beginning
$data = preg_replace('/^\xEF\xBB\xBF/', '', $data); $data = preg_replace('/^\xEF\xBB\xBF/', '', $data);

View file

@ -315,14 +315,7 @@ class GenericWebProvider implements InfoProviderInterface
//Remove any leading slashes //Remove any leading slashes
$url = ltrim($url, '/'); $url = ltrim($url, '/');
//If the URL starts with https:/ or http:/, add the missing slash $url = 'https://'.$url;
//Traefik removes the double slash as secruity measure, so we want to be forgiving and add it back if needed
//See https://github.com/Part-DB/Part-DB-server/issues/1296
if (preg_match('/^https?:\/[^\/]/', $url)) {
$url = preg_replace('/^(https?:)\/([^\/])/', '$1//$2', $url);
} else {
$url = 'https://'.$url;
}
} }
//If this is not a valid URL with host, domain and path, throw an exception //If this is not a valid URL with host, domain and path, throw an exception

View file

@ -105,10 +105,6 @@ final class BarcodeScanHelper
return new AmazonBarcodeScanResult($input); return new AmazonBarcodeScanResult($input);
} }
if ($type === BarcodeSourceType::TME) {
return TMEBarcodeScanResult::parse($input);
}
//Null means auto and we try the different formats //Null means auto and we try the different formats
$result = $this->parseInternalBarcode($input); $result = $this->parseInternalBarcode($input);
@ -148,11 +144,6 @@ final class BarcodeScanHelper
return new AmazonBarcodeScanResult($input); return new AmazonBarcodeScanResult($input);
} }
// Try TME barcode
if (TMEBarcodeScanResult::isTMEBarcode($input)) {
return TMEBarcodeScanResult::parse($input);
}
throw new InvalidArgumentException('Unknown barcode'); throw new InvalidArgumentException('Unknown barcode');
} }
@ -171,7 +162,6 @@ final class BarcodeScanHelper
return LCSCBarcodeScanResult::parse($input); return LCSCBarcodeScanResult::parse($input);
} }
private function parseUserDefinedBarcode(string $input): ?LocalBarcodeScanResult private function parseUserDefinedBarcode(string $input): ?LocalBarcodeScanResult
{ {
$lot_repo = $this->entityManager->getRepository(PartLot::class); $lot_repo = $this->entityManager->getRepository(PartLot::class);

View file

@ -150,10 +150,6 @@ final readonly class BarcodeScanResultHandler
?? $this->em->getRepository(Part::class)->getPartBySPN($barcodeScan->asin); ?? $this->em->getRepository(Part::class)->getPartBySPN($barcodeScan->asin);
} }
if ($barcodeScan instanceof TMEBarcodeScanResult) {
return $this->resolvePartFromTME($barcodeScan);
}
return null; return null;
} }
@ -221,8 +217,8 @@ final readonly class BarcodeScanResultHandler
* Resolve LCSC barcode -> Part. * Resolve LCSC barcode -> Part.
* Strategy: * Strategy:
* 1) Try providerReference.provider_id == pc (LCSC "Cxxxxxx") if you store it there * 1) Try providerReference.provider_id == pc (LCSC "Cxxxxxx") if you store it there
* 2) Fallback to manufacturer_product_number == pm (MPN)
* Returns first match (consistent with EIGP114 logic) * Returns first match (consistent with EIGP114 logic)
* 2) Fallback to search across supplier part number (SPN)
*/ */
private function resolvePartFromLCSC(LCSCBarcodeScanResult $barcodeScan): ?Part private function resolvePartFromLCSC(LCSCBarcodeScanResult $barcodeScan): ?Part
{ {
@ -235,31 +231,16 @@ final readonly class BarcodeScanResultHandler
} }
} }
// fallback to search by SPN // Fallback to MPN (pm)
return $this->em->getRepository(Part::class)->getPartBySPN($pc); $pm = $barcodeScan->mpn; // e.g. RC0402FR-071ML
} if (!$pm) {
return null;
private function resolvePartFromTME(TMEBarcodeScanResult $barcodeScan): ?Part
{
$pn = $barcodeScan->tmePartNumber;
if ($pn) {
$part = $this->em->getRepository(Part::class)->getPartByProviderInfo($pn);
if ($part !== null) {
return $part;
}
//Try to find the part by SPN/SKU
$part = $this->em->getRepository(Part::class)->getPartBySPN($pn);
if ($part !== null) {
return $part;
}
} }
// Fallback: search by MPN return $this->em->getRepository(Part::class)->getPartByMPN($pm);
return $this->em->getRepository(Part::class)->getPartByMPN($barcodeScan->mpn, $barcodeScan->manufacturer);
} }
/** /**
* Tries to extract creation information for a part from the given barcode scan result. This can be used to * Tries to extract creation information for a part from the given barcode scan result. This can be used to
* automatically fill in the info provider reference of a part, when creating a new part based on the scan result. * automatically fill in the info provider reference of a part, when creating a new part based on the scan result.
@ -271,20 +252,6 @@ final readonly class BarcodeScanResultHandler
*/ */
public function getCreateInfos(BarcodeScanResultInterface $scanResult): ?array public function getCreateInfos(BarcodeScanResultInterface $scanResult): ?array
{ {
// TME
if ($scanResult instanceof TMEBarcodeScanResult) {
if ($scanResult->tmePartNumber === null) {
return null;
}
return [
'providerKey' => 'tme',
'providerId' => $scanResult->tmePartNumber,
'lotAmount' => $scanResult->quantity,
'lotName' => $scanResult->purchaseOrder,
'lotUserBarcode' => $scanResult->rawInput,
];
}
// LCSC // LCSC
if ($scanResult instanceof LCSCBarcodeScanResult) { if ($scanResult instanceof LCSCBarcodeScanResult) {
return [ return [

View file

@ -52,7 +52,4 @@ enum BarcodeSourceType: string
case LCSC = 'lcsc'; case LCSC = 'lcsc';
case AMAZON = 'amazon'; case AMAZON = 'amazon';
/** For TME (tme.eu) formatted QR codes */
case TME = 'tme';
} }

View file

@ -254,16 +254,12 @@ readonly class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
*/ */
public static function isFormat06Code(string $input): bool public static function isFormat06Code(string $input): bool
{ {
//Code should begin with [)><RS>06<GS> as per the standard //Code must begin with [)><RS>06<GS>
if(!str_starts_with($input, "[)>\u{1E}06\u{1D}") if(!str_starts_with($input, "[)>\u{1E}06\u{1D}")){
// some codes don't contain record separators return false;
&& !str_starts_with($input, "[)>06\u{1D}")
// This is found on old Mouser parts
&& !str_starts_with($input, ">[)>06\u{1D}"))
{
return false;
} }
//Digikey and Mouser don't put a trailer onto the barcode, so we just check for the header
//Digikey does not put a trailer onto the barcode, so we just check for the header
return true; return true;
} }

View file

@ -1,143 +0,0 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 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\Services\LabelSystem\BarcodeScanner;
use InvalidArgumentException;
/**
* This class represents the content of a tme.eu barcode label.
* The format is space-separated KEY:VALUE tokens, e.g.:
* QTY:1000 PN:SMD0603-5K1-1% PO:32723349/7 MFR:ROYALOHM MPN:0603SAF5101T5E CoO:TH RoHS https://www.tme.eu/details/...
*/
readonly class TMEBarcodeScanResult implements BarcodeScanResultInterface
{
/** @var int|null Quantity (QTY) */
public ?int $quantity;
/** @var string|null TME part number (PN) */
public ?string $tmePartNumber;
/** @var string|null Purchase order number (PO) */
public ?string $purchaseOrder;
/** @var string|null Manufacturer name (MFR) */
public ?string $manufacturer;
/** @var string|null Manufacturer part number (MPN) */
public ?string $mpn;
/** @var string|null Country of origin (CoO) */
public ?string $countryOfOrigin;
/** @var bool Whether the part is RoHS compliant */
public bool $rohs;
/** @var string|null The product URL */
public ?string $productUrl;
/**
* @param array<string, string> $fields Parsed key-value fields (keys uppercased)
* @param string $rawInput Original barcode string
*/
public function __construct(
public array $fields,
public string $rawInput,
) {
$this->quantity = isset($this->fields['QTY']) ? (int) $this->fields['QTY'] : null;
$this->tmePartNumber = $this->fields['PN'] ?? null;
$this->purchaseOrder = $this->fields['PO'] ?? null;
$this->manufacturer = $this->fields['MFR'] ?? null;
$this->mpn = $this->fields['MPN'] ?? null;
$this->countryOfOrigin = $this->fields['COO'] ?? null;
$this->rohs = isset($this->fields['ROHS']);
$this->productUrl = $this->fields['URL'] ?? null;
}
public function getSourceType(): BarcodeSourceType
{
return BarcodeSourceType::TME;
}
public function getDecodedForInfoMode(): array
{
return [
'Barcode type' => 'TME',
'TME Part No. (PN)' => $this->tmePartNumber ?? '',
'MPN' => $this->mpn ?? '',
'Manufacturer (MFR)' => $this->manufacturer ?? '',
'Qty' => $this->quantity !== null ? (string) $this->quantity : '',
'Purchase Order (PO)' => $this->purchaseOrder ?? '',
'Country of Origin (CoO)' => $this->countryOfOrigin ?? '',
'RoHS' => $this->rohs ? 'Yes' : 'No',
'URL' => $this->productUrl ?? '',
];
}
/**
* Returns true if the input looks like a TME barcode label (contains tme.eu URL).
*/
public static function isTMEBarcode(string $input): bool
{
return str_contains(strtolower($input), 'tme.eu');
}
/**
* Parse the TME barcode string into a TMEBarcodeScanResult.
*/
public static function parse(string $input): self
{
$raw = trim($input);
if (!self::isTMEBarcode($raw)) {
throw new InvalidArgumentException('Not a TME barcode');
}
$fields = [];
// Split on whitespace; each token is either KEY:VALUE, a bare keyword, or the URL
$tokens = preg_split('/\s+/', $raw);
foreach ($tokens as $token) {
if ($token === '') {
continue;
}
// The TME URL
if (str_starts_with(strtolower($token), 'http')) {
$fields['URL'] = $token;
continue;
}
$colonPos = strpos($token, ':');
if ($colonPos !== false) {
$key = strtoupper(substr($token, 0, $colonPos));
$value = substr($token, $colonPos + 1);
$fields[$key] = $value;
} else {
// Bare keyword like "RoHS"
$fields[strtoupper($token)] = '';
}
}
return new self($fields, $raw);
}
}

View file

@ -47,17 +47,17 @@
</td> </td>
<td> <td>
{{ detail.price | format_money(detail.currency) }} / {{ detail.PriceRelatedQuantity | format_amount(part.partUnit) }} {{ detail.price | format_money(detail.currency) }} / {{ detail.PriceRelatedQuantity | format_amount(part.partUnit) }}
{% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency, app.user.currency ?? null) %} {% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency) %}
{% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %} {% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %}
<span class="text-muted">({{ tmp | format_money(app.user.currency ?? null) }})</span> <span class="text-muted">({{ pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency, app.user.currency ?? null) | format_money(app.user.currency ?? null) }})</span>
{% endif %} {% endif %}
<small class="text-muted">{{- helper.vat_text(detail.includesVAT) -}}</small> <small class="text-muted">{{- helper.vat_text(detail.includesVAT) -}}</small>
</td> </td>
<td> <td>
{{ detail.PricePerUnit | format_money(detail.currency) }} {{ detail.PricePerUnit | format_money(detail.currency) }}
{% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency, app.user.currency ?? null) %} {% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency) %}
{% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %} {% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %}
<span class="text-muted">({{ tmp | format_money(app.user.currency ?? null) }})</span> <span class="text-muted">({{ pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency, app.user.currency ?? null) | format_money(app.user.currency ?? null) }})</span>
{% endif %} {% endif %}
<small class="text-muted">{{- helper.vat_text(detail.includesVAT) -}}</small> <small class="text-muted">{{- helper.vat_text(detail.includesVAT) -}}</small>
</td> </td>

View file

@ -48,59 +48,51 @@
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>{% trans %}project.bom_import.field_mapping.csv_field{% endtrans %}</th> <th>{% trans %}project.bom_import.field_mapping.csv_field{% endtrans %}</th>
<th>{% trans %}project.bom_import.field_mapping.maps_to{% endtrans %}</th> <th>{% trans %}project.bom_import.field_mapping.maps_to{% endtrans %}</th>
<th>{% trans %}project.bom_import.field_mapping.suggestion{% endtrans %}</th> <th>{% trans %}project.bom_import.field_mapping.suggestion{% endtrans %}</th>
<th>{% trans %}project.bom_import.field_mapping.priority{% endtrans %}</th> <th>{% trans %}project.bom_import.field_mapping.priority{% endtrans %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for field in detected_fields %} {% for field in detected_fields %}
<tr> <tr>
<td> <td>
<code>{{ field }}</code> <code>{{ field }}</code>
</td> </td>
<td> <td>
{# TODO: This is more a workaround than a proper fix. Ideally the controller should be fixed in a way, that we get the correct fields here #}
{% if field_name_mapping[field] is defined %}
{% set field_name = field_name_mapping[field] %}
{{ form_widget(form['mapping_' ~ field_name_mapping[field]], { {{ form_widget(form['mapping_' ~ field_name_mapping[field]], {
'attr': { 'attr': {
'class': 'form-select field-mapping-select', 'class': 'form-select field-mapping-select',
'data-field': field 'data-field': field
} }
}) }} }) }}
{% else %} </td>
<b class="text-danger"> <td>
{% trans %}project.bom_import.field_mapping.error.check_delimiter{% endtrans %} {% if suggested_mapping[field] is defined %}
</b> <span class="badge bg-success">
{% endif %}
</td>
<td>
{% if suggested_mapping[field] is defined %}
<span class="badge bg-success">
<i class="fa-solid fa-magic fa-fw"></i> <i class="fa-solid fa-magic fa-fw"></i>
{{ suggested_mapping[field] }} {{ suggested_mapping[field] }}
</span> </span>
{% else %} {% else %}
<span class="text-muted"> <span class="text-muted">
<i class="fa-solid fa-question fa-fw"></i> <i class="fa-solid fa-question fa-fw"></i>
{% trans %}project.bom_import.field_mapping.no_suggestion{% endtrans %} {% trans %}project.bom_import.field_mapping.no_suggestion{% endtrans %}
</span> </span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
<input type="number" <input type="number"
class="form-control form-control-sm priority-input" class="form-control form-control-sm priority-input"
min="1" min="1"
value="10" value="10"
style="width: 80px;" style="width: 80px;"
data-field="{{ field }}" data-field="{{ field }}"
title="{% trans %}project.bom_import.field_mapping.priority_help{% endtrans %}"> title="{% trans %}project.bom_import.field_mapping.priority_help{% endtrans %}">
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>

View file

@ -23,7 +23,7 @@
{% if saml_enabled %} {% if saml_enabled %}
<div class="{{ offset_label }} {{ col_input }}"> <div class="{{ offset_label }} {{ col_input }}">
<a class="btn btn-secondary" href="{{ path('saml_login') }}" data-turbo="false"><i class="fa-solid fa-house-user"></i> {% trans %}login.sso_saml_login{% endtrans %}</a> <a class="btn btn-secondary" href="{{ path('saml_login') }}"><i class="fa-solid fa-house-user"></i> {% trans %}login.sso_saml_login{% endtrans %}</a>
<p class="text-muted">{% trans %}login.local_login_hint{% endtrans %}</p> <p class="text-muted">{% trans %}login.local_login_hint{% endtrans %}</p>
</div> </div>

View file

@ -1,68 +0,0 @@
<?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 Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\ORM\Query\AST\Node;
use Doctrine\ORM\Query\SqlWalker;
use PHPUnit\Framework\TestCase;
abstract class AbstractDoctrineFunctionTestCase extends TestCase
{
protected function createSqlWalker(AbstractPlatform $platform, string $serverVersion = '11.0.0-MariaDB'): SqlWalker
{
$connection = $this->createMock(Connection::class);
$connection->method('getDatabasePlatform')->willReturn($platform);
$connection->method('getServerVersion')->willReturn($serverVersion);
$sqlWalker = $this->getMockBuilder(SqlWalker::class)
->disableOriginalConstructor()
->onlyMethods(['getConnection'])
->getMock();
$sqlWalker->method('getConnection')->willReturn($connection);
return $sqlWalker;
}
protected function createNode(string $sql): Node
{
$node = $this->createMock(Node::class);
$node->method('dispatch')->willReturn($sql);
return $node;
}
protected function setObjectProperty(object $object, string $property, mixed $value): void
{
$reflection = new \ReflectionProperty($object, $property);
$reflection->setValue($object, $value);
}
protected function setStaticProperty(string $class, string $property, mixed $value): void
{
$reflection = new \ReflectionProperty($class, $property);
$reflection->setValue(null, $value);
}
}

View file

@ -1,42 +0,0 @@
<?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\ArrayPosition;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
final class ArrayPositionTest extends AbstractDoctrineFunctionTestCase
{
public function testArrayPositionBuildsSql(): void
{
$function = new ArrayPosition('ARRAY_POSITION');
$this->setObjectProperty($function, 'array', $this->createNode(':ids'));
$this->setObjectProperty($function, 'field', $this->createNode('p.id'));
$sql = $function->getSql($this->createSqlWalker(new PostgreSQLPlatform()));
$this->assertSame('ARRAY_POSITION(:ids, p.id)', $sql);
}
}

View file

@ -1,45 +0,0 @@
<?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\Field2;
use Doctrine\DBAL\Platforms\MySQLPlatform;
final class Field2Test extends AbstractDoctrineFunctionTestCase
{
public function testField2BuildsSql(): void
{
$function = new Field2('FIELD2');
$this->setObjectProperty($function, 'field', $this->createNode('p.id'));
$this->setObjectProperty($function, 'values', [
$this->createNode('1'),
$this->createNode('2'),
$this->createNode('3'),
]);
$sql = $function->getSql($this->createSqlWalker(new MySQLPlatform()));
$this->assertSame('FIELD2(p.id, 1, 2, 3)', $sql);
}
}

View file

@ -1,66 +0,0 @@
<?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\ILike;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\DBAL\Platforms\SQLServerPlatform;
use PHPUnit\Framework\Attributes\DataProvider;
final class ILikeTest extends AbstractDoctrineFunctionTestCase
{
public static function iLikePlatformProvider(): \Generator
{
yield 'mysql' => [new MySQLPlatform(), '(part_name LIKE :pattern)'];
yield 'postgres' => [new PostgreSQLPlatform(), '(part_name ILIKE :pattern)'];
yield 'sqlite' => [new SQLitePlatform(), "(part_name LIKE :pattern ESCAPE '\\')"];
}
#[DataProvider('iLikePlatformProvider')]
public function testILikeUsesExpectedOperator(AbstractPlatform $platform, string $expectedSql): void
{
$function = new ILike('ILIKE');
$function->value = $this->createNode('part_name');
$function->expr = $this->createNode(':pattern');
$sql = $function->getSql($this->createSqlWalker($platform));
$this->assertSame($expectedSql, $sql);
}
public function testILikeThrowsOnUnsupportedPlatform(): void
{
$function = new ILike('ILIKE');
$function->value = $this->createNode('part_name');
$function->expr = $this->createNode(':pattern');
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('does not support case insensitive like expressions');
$function->getSql($this->createSqlWalker(new SQLServerPlatform()));
}
}

View file

@ -1,95 +0,0 @@
<?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\Natsort;
use Doctrine\DBAL\Platforms\MariaDBPlatform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
final class NatsortTest extends AbstractDoctrineFunctionTestCase
{
protected function setUp(): void
{
parent::setUp();
Natsort::allowSlowNaturalSort(false);
$this->setStaticProperty(Natsort::class, 'supportsNaturalSort', null);
}
public function testNatsortUsesPostgresCollation(): void
{
$function = new Natsort('NATSORT');
$this->setObjectProperty($function, 'field', $this->createNode('part_name'));
$sql = $function->getSql($this->createSqlWalker(new PostgreSQLPlatform()));
$this->assertSame('part_name COLLATE numeric', $sql);
}
public function testNatsortUsesMariaDbNativeFunctionOnSupportedVersion(): void
{
$function = new Natsort('NATSORT');
$this->setObjectProperty($function, 'field', $this->createNode('part_name'));
$sql = $function->getSql($this->createSqlWalker(new MariaDBPlatform(), '10.11.2-MariaDB'));
$this->assertSame('NATURAL_SORT_KEY(part_name)', $sql);
}
public function testNatsortFallsBackWithoutSlowSort(): void
{
$function = new Natsort('NATSORT');
$this->setObjectProperty($function, 'field', $this->createNode('part_name'));
$sql = $function->getSql($this->createSqlWalker(new MariaDBPlatform(), '10.6.10-MariaDB'));
$this->assertSame('part_name', $sql);
}
public function testNatsortUsesSlowSortFunctionOnMySqlWhenEnabled(): void
{
Natsort::allowSlowNaturalSort();
$function = new Natsort('NATSORT');
$this->setObjectProperty($function, 'field', $this->createNode('part_name'));
$sql = $function->getSql($this->createSqlWalker(new MySQLPlatform()));
$this->assertSame('NatSortKey(part_name, 0)', $sql);
}
public function testNatsortUsesSlowSortCollationOnSqliteWhenEnabled(): void
{
Natsort::allowSlowNaturalSort();
$function = new Natsort('NATSORT');
$this->setObjectProperty($function, 'field', $this->createNode('part_name'));
$sql = $function->getSql($this->createSqlWalker(new SQLitePlatform()));
$this->assertSame('part_name COLLATE NATURAL_CMP', $sql);
}
}

View file

@ -1,66 +0,0 @@
<?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\Regexp;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\DBAL\Platforms\SQLServerPlatform;
use PHPUnit\Framework\Attributes\DataProvider;
final class RegexpTest extends AbstractDoctrineFunctionTestCase
{
public static function regexpPlatformProvider(): \Generator
{
yield 'mysql' => [new MySQLPlatform(), '(part_name REGEXP :regex)'];
yield 'sqlite' => [new SQLitePlatform(), '(part_name REGEXP :regex)'];
yield 'postgres' => [new PostgreSQLPlatform(), '(part_name ~* :regex)'];
}
#[DataProvider('regexpPlatformProvider')]
public function testRegexpUsesExpectedOperator(AbstractPlatform $platform, string $expectedSql): void
{
$function = new Regexp('REGEXP');
$this->setObjectProperty($function, 'value', $this->createNode('part_name'));
$this->setObjectProperty($function, 'regexp', $this->createNode(':regex'));
$sql = $function->getSql($this->createSqlWalker($platform));
$this->assertSame($expectedSql, $sql);
}
public function testRegexpThrowsOnUnsupportedPlatform(): void
{
$function = new Regexp('REGEXP');
$this->setObjectProperty($function, 'value', $this->createNode('part_name'));
$this->setObjectProperty($function, 'regexp', $this->createNode(':regex'));
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('does not support regular expressions');
$function->getSql($this->createSqlWalker(new SQLServerPlatform()));
}
}

View file

@ -115,8 +115,8 @@ final class BarcodeScanResultHandlerTest extends KernelTestCase
public function testLCSCBarcodeResolvePartOrNullReturnsNullWhenNotFound(): void public function testLCSCBarcodeResolvePartOrNullReturnsNullWhenNotFound(): void
{ {
$scan = new LCSCBarcodeScanResult( $scan = new LCSCBarcodeScanResult(
fields: ['pc' => 'C0000000', 'pm' => 'NON_EXISTENT_MPN_12345'], fields: ['pc' => 'C0000000', 'pm' => ''],
rawInput: '{pc:C0000000,pm:NON_EXISTENT_MPN_12345}' rawInput: '{pc:C0000000,pm:}'
); );
$this->assertNull($this->service->resolvePart($scan)); $this->assertNull($this->service->resolvePart($scan));

View file

@ -93,13 +93,6 @@ final class EIGP114BarcodeScanResultTest extends TestCase
//Valid code (digikey, without trailer) //Valid code (digikey, without trailer)
$this->assertTrue(EIGP114BarcodeScanResult::isFormat06Code("[)>\x1e06\x1dPQ1045-ND\x1d1P364019-01\x1d30PQ1045-ND\x1dK12432 TRAVIS FOSS P\x1d1K85732873\x1d10K103332956\x1d9D231013\x1d1TQJ13P\x1d11K1\x1d4LTW\x1dQ3\x1d11ZPICK\x1d12Z7360988\x1d13Z999999\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000000000000000000000")); $this->assertTrue(EIGP114BarcodeScanResult::isFormat06Code("[)>\x1e06\x1dPQ1045-ND\x1d1P364019-01\x1d30PQ1045-ND\x1dK12432 TRAVIS FOSS P\x1d1K85732873\x1d10K103332956\x1d9D231013\x1d1TQJ13P\x1d11K1\x1d4LTW\x1dQ3\x1d11ZPICK\x1d12Z7360988\x1d13Z999999\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000000000000000000000"));
//Valid code (without record separator)
$this->assertTrue(EIGP114BarcodeScanResult::isFormat06Code("[)>06\x1DP596-777A1-ND\x1D1PXAF4444\x1DQ3\x1D10D1452\x1D1TBF1103\x1D4LUS\x1E\x04"));
//Old mouser format
$this->assertTrue(EIGP114BarcodeScanResult::isFormat06Code(">[)>06\x1DP596-777A1-ND\x1D1PXAF4444\x1DQ3\x1D10D1452\x1D1TBF1103\x1D4LUS\x1E\x04"));
} }
public function testParseFormat06CodeInvalid(): void public function testParseFormat06CodeInvalid(): void
@ -108,32 +101,6 @@ final class EIGP114BarcodeScanResultTest extends TestCase
EIGP114BarcodeScanResult::parseFormat06Code(''); EIGP114BarcodeScanResult::parseFormat06Code('');
} }
public function testParseWithoutRecordSeparator(): void
{
$barcode = EIGP114BarcodeScanResult::parseFormat06Code("[)>06\x1DP596-777A1-ND\x1D1PXAF4444\x1DQ3\x1D10D1452\x1D1TBF1103\x1D4LUS\x1E\x04");
$this->assertSame([
'P' => '596-777A1-ND',
'1P' => 'XAF4444',
'Q' => '3',
'10D' => '1452',
'1T' => 'BF1103',
'4L' => 'US',
], $barcode->data);
}
public function testParseOldMouserFormat(): void
{
$barcode = EIGP114BarcodeScanResult::parseFormat06Code(">[)>06\x1DP596-777A1-ND\x1D1PXAF4444\x1DQ3\x1D10D1452\x1D1TBF1103\x1D4LUS\x1E\x04");
$this->assertSame([
'P' => '596-777A1-ND',
'1P' => 'XAF4444',
'Q' => '3',
'10D' => '1452',
'1T' => 'BF1103',
'4L' => 'US',
], $barcode->data);
}
public function testParseFormat06Code(): void public function testParseFormat06Code(): void
{ {
$barcode = EIGP114BarcodeScanResult::parseFormat06Code("[)>\x1E06\x1DP596-777A1-ND\x1D1PXAF4444\x1DQ3\x1D10D1452\x1D1TBF1103\x1D4LUS\x1E\x04"); $barcode = EIGP114BarcodeScanResult::parseFormat06Code("[)>\x1E06\x1DP596-777A1-ND\x1D1PXAF4444\x1DQ3\x1D10D1452\x1D1TBF1103\x1D4LUS\x1E\x04");

View file

@ -1,110 +0,0 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 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/>.
*/
namespace App\Tests\Services\LabelSystem\BarcodeScanner;
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
use App\Services\LabelSystem\BarcodeScanner\TMEBarcodeScanResult;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
class TMEBarcodeScanResultTest extends TestCase
{
private const EXAMPLE1 = 'QTY:1000 PN:SMD0603-5K1-1% PO:32723349/7 MFR:ROYALOHM MPN:0603SAF5101T5E CoO:TH RoHS https://www.tme.eu/details/SMD0603-5K1-1%25';
private const EXAMPLE2 = 'QTY:5 PN:ETQP3M6R8KVP PO:31199729/3 MFR:PANASONIC MPN:ETQP3M6R8KVP RoHS https://www.tme.eu/details/ETQP3M6R8KVP';
public function testIsTMEBarcode(): void
{
$this->assertFalse(TMEBarcodeScanResult::isTMEBarcode('invalid'));
$this->assertFalse(TMEBarcodeScanResult::isTMEBarcode('QTY:5 PN:ABC MPN:XYZ'));
$this->assertFalse(TMEBarcodeScanResult::isTMEBarcode(''));
$this->assertTrue(TMEBarcodeScanResult::isTMEBarcode(self::EXAMPLE1));
$this->assertTrue(TMEBarcodeScanResult::isTMEBarcode(self::EXAMPLE2));
}
public function testParseInvalidThrows(): void
{
$this->expectException(InvalidArgumentException::class);
TMEBarcodeScanResult::parse('not-a-tme-barcode');
}
public function testParseExample1(): void
{
$scan = TMEBarcodeScanResult::parse(self::EXAMPLE1);
$this->assertSame(1000, $scan->quantity);
$this->assertSame('SMD0603-5K1-1%', $scan->tmePartNumber);
$this->assertSame('32723349/7', $scan->purchaseOrder);
$this->assertSame('ROYALOHM', $scan->manufacturer);
$this->assertSame('0603SAF5101T5E', $scan->mpn);
$this->assertSame('TH', $scan->countryOfOrigin);
$this->assertTrue($scan->rohs);
$this->assertSame('https://www.tme.eu/details/SMD0603-5K1-1%25', $scan->productUrl);
$this->assertSame(self::EXAMPLE1, $scan->rawInput);
}
public function testParseExample2(): void
{
$scan = TMEBarcodeScanResult::parse(self::EXAMPLE2);
$this->assertSame(5, $scan->quantity);
$this->assertSame('ETQP3M6R8KVP', $scan->tmePartNumber);
$this->assertSame('31199729/3', $scan->purchaseOrder);
$this->assertSame('PANASONIC', $scan->manufacturer);
$this->assertSame('ETQP3M6R8KVP', $scan->mpn);
$this->assertNull($scan->countryOfOrigin);
$this->assertTrue($scan->rohs);
$this->assertSame('https://www.tme.eu/details/ETQP3M6R8KVP', $scan->productUrl);
}
public function testGetSourceType(): void
{
$scan = TMEBarcodeScanResult::parse(self::EXAMPLE2);
$this->assertSame(BarcodeSourceType::TME, $scan->getSourceType());
}
public function testParseUppercaseUrl(): void
{
$input = 'QTY:500 PN:M0.6W-10K MFR:ROYAL.OHM MPN:MF006FF1002A50 PO:7792659/8 HTTPS://WWW.TME.EU/DETAILS/M0.6W-10K';
$this->assertTrue(TMEBarcodeScanResult::isTMEBarcode($input));
$scan = TMEBarcodeScanResult::parse($input);
$this->assertSame(500, $scan->quantity);
$this->assertSame('M0.6W-10K', $scan->tmePartNumber);
$this->assertSame('ROYAL.OHM', $scan->manufacturer);
$this->assertSame('MF006FF1002A50', $scan->mpn);
$this->assertSame('7792659/8', $scan->purchaseOrder);
$this->assertSame('HTTPS://WWW.TME.EU/DETAILS/M0.6W-10K', $scan->productUrl);
}
public function testGetDecodedForInfoMode(): void
{
$scan = TMEBarcodeScanResult::parse(self::EXAMPLE1);
$decoded = $scan->getDecodedForInfoMode();
$this->assertSame('TME', $decoded['Barcode type']);
$this->assertSame('SMD0603-5K1-1%', $decoded['TME Part No. (PN)']);
$this->assertSame('0603SAF5101T5E', $decoded['MPN']);
$this->assertSame('ROYALOHM', $decoded['Manufacturer (MFR)']);
$this->assertSame('1000', $decoded['Qty']);
$this->assertSame('Yes', $decoded['RoHS']);
}
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="de"> <xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="de">
<file id="messages.de"> <file id="messages.en">
<unit id="x_wTSQS" name="attachment_type.caption"> <unit id="x_wTSQS" name="attachment_type.caption">
<segment state="translated"> <segment state="translated">
<source>attachment_type.caption</source> <source>attachment_type.caption</source>
@ -12861,12 +12861,6 @@ Buerklin-API-Authentication-Server:
<target>Amazon Barcode</target> <target>Amazon Barcode</target>
</segment> </segment>
</unit> </unit>
<unit id="d.V2Pid" name="scan_dialog.mode.tme">
<segment state="translated">
<source>scan_dialog.mode.tme</source>
<target>TME Barcode</target>
</segment>
</unit>
<unit id="BQWuR_G" name="settings.ips.canopy"> <unit id="BQWuR_G" name="settings.ips.canopy">
<segment state="translated"> <segment state="translated">
<source>settings.ips.canopy</source> <source>settings.ips.canopy</source>
@ -12951,11 +12945,5 @@ Buerklin-API-Authentication-Server:
<target>[Part_lot] aus Barcode erstellt: Bitte überprüfen Sie, ob die Daten korrekt und gewünscht sind.</target> <target>[Part_lot] aus Barcode erstellt: Bitte überprüfen Sie, ob die Daten korrekt und gewünscht sind.</target>
</segment> </segment>
</unit> </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>Zuordnungsfehler: Bitte prüfen Sie, ob Sie das richtige Trennzeichen ausgewählt haben!</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -12863,12 +12863,6 @@ Buerklin-API Authentication server:
<target>Amazon barcode</target> <target>Amazon barcode</target>
</segment> </segment>
</unit> </unit>
<unit id="d.V2Pid" name="scan_dialog.mode.tme">
<segment state="translated">
<source>scan_dialog.mode.tme</source>
<target>TME barcode</target>
</segment>
</unit>
<unit id="BQWuR_G" name="settings.ips.canopy"> <unit id="BQWuR_G" name="settings.ips.canopy">
<segment state="translated"> <segment state="translated">
<source>settings.ips.canopy</source> <source>settings.ips.canopy</source>
@ -12953,11 +12947,5 @@ Buerklin-API Authentication server:
<target>[Part_lot] created from barcode: Please check if the data is correct and desired.</target> <target>[Part_lot] created from barcode: Please check if the data is correct and desired.</target>
</segment> </segment>
</unit> </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>Mapping error: Check if you have selected the right delimiter!</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -247,11 +247,5 @@
<target>该类型在此语言下已存在翻译定义!</target> <target>该类型在此语言下已存在翻译定义!</target>
</segment> </segment>
</unit> </unit>
<unit id="zT_j_oQ" name="validator.invalid_gtin">
<segment state="translated">
<source>validator.invalid_gtin</source>
<target>无效的GTIN / EAN 码。</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

1681
yarn.lock

File diff suppressed because it is too large Load diff