mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-05-18 17:31:35 +00:00
Merge branch 'master' into add-edit-kicad-suggestion-list-editor
This commit is contained in:
commit
5f66ec5ee6
36 changed files with 4324 additions and 4025 deletions
4
.env
4
.env
|
|
@ -121,6 +121,10 @@ SAML_SP_PRIVATE_KEY="MIIE..."
|
||||||
# In demo mode things it is not possible for a user to change his password and his settings.
|
# In demo mode things it is not possible for a user to change his password and his settings.
|
||||||
DEMO_MODE=0
|
DEMO_MODE=0
|
||||||
|
|
||||||
|
# When this is set to 1, users can make Part-DB directly download a file specified as a URL from the local network and create it as a local file.
|
||||||
|
# This allows users access to all resources available in the local network, which could be a security risk, so use this only if you trust your users and have a secure local network.
|
||||||
|
ALLOW_ATTACHMENT_DOWNLOADS_FROM_LOCALNETWORK=0
|
||||||
|
|
||||||
# Change this to true, if no url rewriting (like mod_rewrite for Apache) is available
|
# Change this to true, if no url rewriting (like mod_rewrite for Apache) is available
|
||||||
# In that case all URL contains the index.php front controller in URL
|
# In that case all URL contains the index.php front controller in URL
|
||||||
NO_URL_REWRITE_AVAILABLE=0
|
NO_URL_REWRITE_AVAILABLE=0
|
||||||
|
|
|
||||||
2
.github/workflows/assets_artifact_build.yml
vendored
2
.github/workflows/assets_artifact_build.yml
vendored
|
|
@ -67,7 +67,7 @@ jobs:
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Install yarn dependencies
|
- name: Install yarn dependencies
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
|
|
||||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
|
|
@ -106,7 +106,7 @@ jobs:
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Install yarn dependencies
|
- name: Install yarn dependencies
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
|
@ -129,7 +129,7 @@ jobs:
|
||||||
run: ./bin/phpunit --coverage-clover=coverage.xml
|
run: ./bin/phpunit --coverage-clover=coverage.xml
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v6
|
||||||
with:
|
with:
|
||||||
env_vars: PHP_VERSION,DB_TYPE
|
env_vars: PHP_VERSION,DB_TYPE
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -25,6 +25,10 @@
|
||||||
uploads/*
|
uploads/*
|
||||||
!uploads/.keep
|
!uploads/.keep
|
||||||
|
|
||||||
|
# Some people use Certbot or similar tools to make SSL certificates.
|
||||||
|
# Also see https://www.rfc-editor.org/rfc/rfc5785
|
||||||
|
public/.well-known/
|
||||||
|
|
||||||
# Do not keep cache files
|
# Do not keep cache files
|
||||||
.php_cs.cache
|
.php_cs.cache
|
||||||
.phpcs-cache
|
.phpcs-cache
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ RUN yarn build
|
||||||
RUN yarn cache clean && rm -rf node_modules/
|
RUN yarn cache clean && rm -rf node_modules/
|
||||||
|
|
||||||
# FrankenPHP base stage
|
# FrankenPHP base stage
|
||||||
FROM dunglas/frankenphp:1-php8.4 AS frankenphp_upstream
|
FROM dunglas/frankenphp:1-php8.4-bookworm AS frankenphp_upstream
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
RUN --mount=type=cache,id=apt-cache-$TARGETARCH,target=/var/cache/apt \
|
RUN --mount=type=cache,id=apt-cache-$TARGETARCH,target=/var/cache/apt \
|
||||||
--mount=type=cache,id=apt-lists-$TARGETARCH,target=/var/lib/apt/lists \
|
--mount=type=cache,id=apt-lists-$TARGETARCH,target=/var/lib/apt/lists \
|
||||||
|
|
|
||||||
|
|
@ -74,11 +74,11 @@ Part-DB is also used by small companies and universities for managing their inve
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
* A **web server** (like Apache2 or nginx) that is capable of
|
* A **web server** (like Apache2 or nginx) that is capable of
|
||||||
running [Symfony 6](https://symfony.com/doc/current/reference/requirements.html),
|
running [Symfony 7](https://symfony.com/doc/current/reference/requirements.html),
|
||||||
this includes a minimum PHP version of **PHP 8.2**
|
this includes a minimum PHP version of **PHP 8.2**
|
||||||
* A **MySQL** (at least 5.7) /**MariaDB** (at least 10.4) database server, or **PostgreSQL** 10+ if you do not want to use SQLite.
|
* A **MySQL** (at least 5.7) /**MariaDB** (at least 10.4) database server, or **PostgreSQL** 10+ if you do not want to use SQLite.
|
||||||
* Shell access to your server is highly recommended!
|
* Shell access to your server is highly recommended!
|
||||||
* For building the client-side assets **yarn** and **nodejs** (>= 20.0) is needed.
|
* For building the client-side assets **yarn** and **nodejs** (>= 22.0) is needed.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
|
||||||
1456
composer.lock
generated
1456
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -105,6 +105,8 @@ parameters:
|
||||||
|
|
||||||
env(DATABASE_EMULATE_NATURAL_SORT): 0
|
env(DATABASE_EMULATE_NATURAL_SORT): 0
|
||||||
|
|
||||||
|
env(ALLOW_ATTACHMENT_DOWNLOADS_FROM_LOCALNETWORK): 0
|
||||||
|
|
||||||
######################################################################################################################
|
######################################################################################################################
|
||||||
# Bulk Info Provider Import Configuration
|
# Bulk Info Provider Import Configuration
|
||||||
######################################################################################################################
|
######################################################################################################################
|
||||||
|
|
|
||||||
|
|
@ -1550,7 +1550,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* template_parameters?: array{ // Default parameters to be passed to the template
|
* template_parameters?: array{ // Default parameters to be passed to the template
|
||||||
* className?: scalar|Param|null, // Default class attribute to apply to the root table elements // Default: "table table-bordered"
|
* className?: scalar|Param|null, // Default class attribute to apply to the root table elements // Default: "table table-bordered"
|
||||||
* columnFilter?: "thead"|"tfoot"|"both"|Param|null, // If and where to enable the DataTables Filter module // Default: null
|
* columnFilter?: "thead"|"tfoot"|"both"|Param|null, // If and where to enable the DataTables Filter module // Default: null
|
||||||
* ...<mixed>
|
* ...<string, mixed>
|
||||||
* },
|
* },
|
||||||
* translation_domain?: scalar|Param|null, // Default translation domain to be used // Default: "messages"
|
* translation_domain?: scalar|Param|null, // Default translation domain to be used // Default: "messages"
|
||||||
* }
|
* }
|
||||||
|
|
@ -1705,14 +1705,14 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* use_underscore?: bool|Param, // Default: true
|
* use_underscore?: bool|Param, // Default: true
|
||||||
* unordered_list_markers?: list<scalar|Param|null>,
|
* unordered_list_markers?: list<scalar|Param|null>,
|
||||||
* },
|
* },
|
||||||
* ...<mixed>
|
* ...<string, mixed>
|
||||||
* },
|
* },
|
||||||
* }
|
* }
|
||||||
* @psalm-type GregwarCaptchaConfig = array{
|
* @psalm-type GregwarCaptchaConfig = array{
|
||||||
* 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: "C:\\Users\\mail\\Documents\\PHP\\Part-DB-server\\vendor\\gregwar\\captcha-bundle\\DependencyInjection/../Generator/Font/captcha.ttf"
|
* font?: scalar|Param|null, // Default: "/home/jan/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
|
||||||
|
|
@ -2493,7 +2493,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 paramters.
|
* xkey?: array{ // Deprecated: The "xkey" configuration is deprecated, use your own purger to customize surrogate keys or the appropriate parameters.
|
||||||
* glue?: scalar|Param|null, // xkey glue between keys // Default: " "
|
* glue?: scalar|Param|null, // xkey glue between keys // Default: " "
|
||||||
* },
|
* },
|
||||||
* },
|
* },
|
||||||
|
|
@ -2649,7 +2649,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* cast_fn?: mixed,
|
* cast_fn?: mixed,
|
||||||
* default?: mixed,
|
* default?: mixed,
|
||||||
* filter_class?: mixed,
|
* filter_class?: mixed,
|
||||||
* ...<mixed>
|
* ...<string, mixed>
|
||||||
* }>,
|
* }>,
|
||||||
* strict_query_parameter_validation?: mixed,
|
* strict_query_parameter_validation?: mixed,
|
||||||
* hide_hydra_operation?: mixed,
|
* hide_hydra_operation?: mixed,
|
||||||
|
|
@ -2669,7 +2669,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* name?: mixed,
|
* name?: mixed,
|
||||||
* allow_create?: mixed,
|
* allow_create?: mixed,
|
||||||
* item_uri_template?: mixed,
|
* item_uri_template?: mixed,
|
||||||
* ...<mixed>
|
* ...<string, mixed>
|
||||||
* },
|
* },
|
||||||
* }
|
* }
|
||||||
* @psalm-type ConfigType = array{
|
* @psalm-type ConfigType = array{
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
|
||||||
* `ATTACHMENT_DOWNLOAD_BY_DEFAULT`: When this is set to 1, the "download external file" checkbox is checked by default
|
* `ATTACHMENT_DOWNLOAD_BY_DEFAULT`: When this is set to 1, the "download external file" checkbox is checked by default
|
||||||
when adding a new attachment. Otherwise, it is unchecked by default. Use this if you wanna download all attachments
|
when adding a new attachment. Otherwise, it is unchecked by default. Use this if you wanna download all attachments
|
||||||
locally by default. Attachment download is only possible, when `ALLOW_ATTACHMENT_DOWNLOADS` is set to 1.
|
locally by default. Attachment download is only possible, when `ALLOW_ATTACHMENT_DOWNLOADS` is set to 1.
|
||||||
|
* `ALLOW_ATTACHMENT_DOWNLOADS_FROM_LOCALNETWORK` (default `0`): When this is set to 1, users can make Part-DB directly download a file specified as a URL from the local network and create it as a local file. This allows users access to all resources available in the local network, which could be a security risk, so use this only if you trust your users and have a secure local network.
|
||||||
* `ATTACHMENT_SHOW_HTML_FILES`: When enabled, user uploaded HTML attachments can be viewed directly in the browser.
|
* `ATTACHMENT_SHOW_HTML_FILES`: When enabled, user uploaded HTML attachments can be viewed directly in the browser.
|
||||||
Many potential malicious functions are restricted, still this is a potential security risk and should only be enabled,
|
Many potential malicious functions are restricted, still this is a potential security risk and should only be enabled,
|
||||||
if you trust the users who can upload files. When set to 0, HTML files are rendered as plain text.
|
if you trust the users who can upload files. When set to 0, HTML files are rendered as plain text.
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,11 @@ services:
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
{: .warning }
|
||||||
|
> If you run a root console inside the docker container, and wanna execute commands on the webserver behalf, be sure to use `sudo -E` command (with the `-E` flag) to preserve env variables from the current shell.
|
||||||
|
> Otherwise Part-DB console might use the wrong configuration to execute commands.
|
||||||
|
|
||||||
|
|
||||||
6. Create the initial database with
|
6. Create the initial database with
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ fulfilled by the official Part-DB docker image.*
|
||||||
|
|
||||||
Part-DB 2.0 requires at least PHP 8.2 (newer versions are recommended). So if your existing Part-DB installation is still
|
Part-DB 2.0 requires at least PHP 8.2 (newer versions are recommended). So if your existing Part-DB installation is still
|
||||||
running PHP 8.1, you will have to upgrade your PHP version first.
|
running PHP 8.1, you will have to upgrade your PHP version first.
|
||||||
The minimum required version of node.js is now 20.0 or newer, so if you are using 18.0, you will have to upgrade it too.
|
The minimum required version of node.js is now 22.0 or newer, so if you are using 18.0, you will have to upgrade it too.
|
||||||
|
|
||||||
Most distributions should have the possibility to get backports for PHP 8.4 and modern nodejs, so you should be able to
|
Most distributions should have the possibility to get backports for PHP 8.4 and modern nodejs, so you should be able to
|
||||||
easily upgrade your system to the new requirements. Otherwise, you can use the official Part-DB docker image, which
|
easily upgrade your system to the new requirements. Otherwise, you can use the official Part-DB docker image, which
|
||||||
|
|
@ -60,6 +60,8 @@ The `php bin/console partdb:backup` command can help you with this.
|
||||||
If you want to change them, you must migrate them to the settings interface as described below.
|
If you want to change them, you must migrate them to the settings interface as described below.
|
||||||
|
|
||||||
### Docker installation
|
### Docker installation
|
||||||
|
**When running the console commands from inside a docker container's shell as root, be sure to use `sudo -E` to preserve the environment variables, so that they are correctly passed to the command.**
|
||||||
|
|
||||||
1. Make a backup of your existing Part-DB installation, including the database, data directories and the configuration files and the file where you configure the docker environment variables.
|
1. Make a backup of your existing Part-DB installation, including the database, data directories and the configuration files and the file where you configure the docker environment variables.
|
||||||
2. Stop the existing Part-DB container with `docker compose down`
|
2. Stop the existing Part-DB container with `docker compose down`
|
||||||
3. Ensure that your docker compose file uses the new latest images (either `latest` or `2` tag).
|
3. Ensure that your docker compose file uses the new latest images (either `latest` or `2` tag).
|
||||||
|
|
|
||||||
18
package.json
18
package.json
|
|
@ -9,16 +9,16 @@
|
||||||
"@symfony/stimulus-bridge": "^4.0.0",
|
"@symfony/stimulus-bridge": "^4.0.0",
|
||||||
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
|
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
|
||||||
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets",
|
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets",
|
||||||
"@symfony/webpack-encore": "^5.1.0",
|
"@symfony/webpack-encore": "^6.0.0",
|
||||||
"bootstrap": "^5.1.3",
|
"bootstrap": "^5.1.3",
|
||||||
"core-js": "^3.38.0",
|
"core-js": "^3.38.0",
|
||||||
"intl-messageformat": "^10.2.5",
|
"intl-messageformat": "^10.5.11",
|
||||||
"jquery": "^3.5.1",
|
"jquery": "^3.5.1",
|
||||||
"popper.js": "^1.14.7",
|
"popper.js": "^1.14.7",
|
||||||
"regenerator-runtime": "^0.13.9",
|
"regenerator-runtime": "^0.14.1",
|
||||||
"webpack": "^5.74.0",
|
"webpack": "^5.74.0",
|
||||||
"webpack-bundle-analyzer": "^5.1.1",
|
"webpack-bundle-analyzer": "^5.1.1",
|
||||||
"webpack-cli": "^5.1.0",
|
"webpack-cli": "^6.0.0",
|
||||||
"webpack-notifier": "^1.15.0"
|
"webpack-notifier": "^1.15.0"
|
||||||
},
|
},
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
|
|
@ -30,14 +30,14 @@
|
||||||
"build": "encore production --progress"
|
"build": "encore production --progress"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=22.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@algolia/autocomplete-js": "^1.17.0",
|
"@algolia/autocomplete-js": "^1.17.0",
|
||||||
"@algolia/autocomplete-plugin-recent-searches": "^1.17.0",
|
"@algolia/autocomplete-plugin-recent-searches": "^1.17.0",
|
||||||
"@algolia/autocomplete-theme-classic": "^1.17.0",
|
"@algolia/autocomplete-theme-classic": "^1.17.0",
|
||||||
"@ckeditor/ckeditor5-dev-translations": "^43.0.1",
|
"@ckeditor/ckeditor5-dev-translations": "^53",
|
||||||
"@ckeditor/ckeditor5-dev-utils": "^43.0.1",
|
"@ckeditor/ckeditor5-dev-utils": "^53",
|
||||||
"@jbtronics/bs-treeview": "^1.0.1",
|
"@jbtronics/bs-treeview": "^1.0.1",
|
||||||
"@part-db/html5-qrcode": "^4.0.0",
|
"@part-db/html5-qrcode": "^4.0.0",
|
||||||
"@zxcvbn-ts/core": "^3.0.2",
|
"@zxcvbn-ts/core": "^3.0.2",
|
||||||
|
|
@ -69,11 +69,11 @@
|
||||||
"marked": "^17.0.1",
|
"marked": "^17.0.1",
|
||||||
"marked-gfm-heading-id": "^4.1.1",
|
"marked-gfm-heading-id": "^4.1.1",
|
||||||
"marked-mangle": "^1.0.1",
|
"marked-mangle": "^1.0.1",
|
||||||
"pdfmake": "^0.2.2",
|
"pdfmake": "^0.3.7",
|
||||||
"stimulus-use": "^0.52.0",
|
"stimulus-use": "^0.52.0",
|
||||||
"tom-select": "^2.1.0",
|
"tom-select": "^2.1.0",
|
||||||
"ts-loader": "^9.2.6",
|
"ts-loader": "^9.2.6",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^6.0.2"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"jquery": "^3.5.1"
|
"jquery": "^3.5.1"
|
||||||
|
|
|
||||||
|
|
@ -56,13 +56,16 @@ class LoadFixturesCommand extends Command
|
||||||
}
|
}
|
||||||
|
|
||||||
$factory = new ResetAutoIncrementPurgerFactory();
|
$factory = new ResetAutoIncrementPurgerFactory();
|
||||||
$purger = $factory->createForEntityManager(null, $this->entityManager);
|
|
||||||
|
//Use truncate purging to fix compatibility with postgresql
|
||||||
|
$purger = $factory->createForEntityManager(null, $this->entityManager, purgeWithTruncate: true);
|
||||||
|
|
||||||
$purger->purge();
|
$purger->purge();
|
||||||
|
|
||||||
//Afterwards run the load fixtures command as normal, but with the --append option
|
//Afterwards run the load fixtures command as normal, but with the --append option
|
||||||
$new_input = new ArrayInput([
|
$new_input = new ArrayInput([
|
||||||
'command' => 'doctrine:fixtures:load',
|
'command' => 'doctrine:fixtures:load',
|
||||||
|
'--purge-with-truncate' => true,
|
||||||
'--append' => true,
|
'--append' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -70,4 +73,4 @@ class LoadFixturesCommand extends Command
|
||||||
|
|
||||||
return $returnCode ?? Command::FAILURE;
|
return $returnCode ?? Command::FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
/**
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
*
|
*
|
||||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
|
@ -20,23 +17,28 @@ declare(strict_types=1);
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
* 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/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\DataTables;
|
namespace App\DataTables;
|
||||||
|
|
||||||
|
use App\DataTables\Adapters\TwoStepORMAdapter;
|
||||||
use App\DataTables\Column\EntityColumn;
|
use App\DataTables\Column\EntityColumn;
|
||||||
use App\DataTables\Column\EnumColumn;
|
use App\DataTables\Column\EnumColumn;
|
||||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||||
use App\DataTables\Column\MarkdownColumn;
|
use App\DataTables\Column\MarkdownColumn;
|
||||||
use App\DataTables\Helpers\PartDataTableHelper;
|
use App\DataTables\Helpers\PartDataTableHelper;
|
||||||
use App\Entity\Attachments\Attachment;
|
use App\Doctrine\Helpers\FieldHelper;
|
||||||
use App\Entity\Parts\Part;
|
use App\Entity\Parts\Part;
|
||||||
use App\Entity\Parts\ManufacturingStatus;
|
use App\Entity\Parts\ManufacturingStatus;
|
||||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||||
use App\Services\ElementTypeNameGenerator;
|
use App\Services\ElementTypeNameGenerator;
|
||||||
use App\Services\EntityURLGenerator;
|
use App\Services\EntityURLGenerator;
|
||||||
use App\Services\Formatters\AmountFormatter;
|
use App\Services\Formatters\AmountFormatter;
|
||||||
|
use Doctrine\ORM\AbstractQuery;
|
||||||
|
use Doctrine\ORM\Query;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
|
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
|
||||||
use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter;
|
|
||||||
use Omines\DataTablesBundle\Column\TextColumn;
|
use Omines\DataTablesBundle\Column\TextColumn;
|
||||||
use Omines\DataTablesBundle\DataTable;
|
use Omines\DataTablesBundle\DataTable;
|
||||||
use Omines\DataTablesBundle\DataTableTypeInterface;
|
use Omines\DataTablesBundle\DataTableTypeInterface;
|
||||||
|
|
@ -44,9 +46,12 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||||
{
|
{
|
||||||
public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper,
|
public function __construct(
|
||||||
protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter)
|
protected EntityURLGenerator $entityURLGenerator,
|
||||||
{
|
protected TranslatorInterface $translator,
|
||||||
|
protected AmountFormatter $amountFormatter,
|
||||||
|
protected PartDataTableHelper $partDataTableHelper
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -62,7 +67,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return $this->partDataTableHelper->renderPicture($context->getPart());
|
return $this->partDataTableHelper->renderPicture($context->getPart());
|
||||||
},
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
->add('id', TextColumn::class, [
|
->add('id', TextColumn::class, [
|
||||||
|
|
@ -133,23 +138,24 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||||
->add('category', EntityColumn::class, [
|
->add('category', EntityColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.category'),
|
'label' => $this->translator->trans('part.table.category'),
|
||||||
'property' => 'part.category',
|
'property' => 'part.category',
|
||||||
'orderField' => 'NATSORT(category.name)',
|
'orderField' => 'NATSORT(category.name)'
|
||||||
])
|
])
|
||||||
->add('footprint', EntityColumn::class, [
|
->add('footprint', EntityColumn::class, [
|
||||||
'property' => 'part.footprint',
|
'property' => 'part.footprint',
|
||||||
'label' => $this->translator->trans('part.table.footprint'),
|
'label' => $this->translator->trans('part.table.footprint'),
|
||||||
'orderField' => 'NATSORT(footprint.name)',
|
'orderField' => 'NATSORT(footprint.name)'
|
||||||
])
|
])
|
||||||
|
|
||||||
->add('manufacturer', EntityColumn::class, [
|
->add('manufacturer', EntityColumn::class, [
|
||||||
'property' => 'part.manufacturer',
|
'property' => 'part.manufacturer',
|
||||||
'label' => $this->translator->trans('part.table.manufacturer'),
|
'label' => $this->translator->trans('part.table.manufacturer'),
|
||||||
'orderField' => 'NATSORT(manufacturer.name)',
|
'orderField' => 'NATSORT(manufacturer.name)'
|
||||||
])
|
])
|
||||||
|
|
||||||
->add('manufacturing_status', EnumColumn::class, [
|
->add('manufacturing_status', EnumColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.manufacturingStatus'),
|
'label' => $this->translator->trans('part.table.manufacturingStatus'),
|
||||||
'data' => static fn(ProjectBOMEntry $context): ?ManufacturingStatus => $context->getPart()?->getManufacturingStatus(),
|
'data' => static fn(ProjectBOMEntry $context): ?ManufacturingStatus => $context->getPart()?->getManufacturingStatus(),
|
||||||
|
'orderField' => 'part.manufacturing_status',
|
||||||
'class' => ManufacturingStatus::class,
|
'class' => ManufacturingStatus::class,
|
||||||
'render' => function (?ManufacturingStatus $status, ProjectBOMEntry $context): string {
|
'render' => function (?ManufacturingStatus $status, ProjectBOMEntry $context): string {
|
||||||
if ($status === null) {
|
if ($status === null) {
|
||||||
|
|
@ -183,8 +189,10 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
->add('storageLocations', TextColumn::class, [
|
->add('storelocation', TextColumn::class, [
|
||||||
'label' => 'part.table.storeLocations',
|
'label' => $this->translator->trans('part.table.storeLocations'),
|
||||||
|
//We need to use a aggregate function to get the first store location, as we have a one-to-many relation
|
||||||
|
'orderField' => 'NATSORT(MIN(_storelocations.name))',
|
||||||
'visible' => false,
|
'visible' => false,
|
||||||
'render' => function ($value, ProjectBOMEntry $context) {
|
'render' => function ($value, ProjectBOMEntry $context) {
|
||||||
if ($context->getPart() !== null) {
|
if ($context->getPart() !== null) {
|
||||||
|
|
@ -207,11 +215,13 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||||
|
|
||||||
$dataTable->addOrderBy('name', DataTable::SORT_ASCENDING);
|
$dataTable->addOrderBy('name', DataTable::SORT_ASCENDING);
|
||||||
|
|
||||||
$dataTable->createAdapter(ORMAdapter::class, [
|
$dataTable->createAdapter(TwoStepORMAdapter::class, [
|
||||||
'entity' => Attachment::class,
|
'entity' => ProjectBOMEntry::class,
|
||||||
'query' => function (QueryBuilder $builder) use ($options): void {
|
'hydrate' => AbstractQuery::HYDRATE_OBJECT,
|
||||||
$this->getQuery($builder, $options);
|
'filter_query' => function (QueryBuilder $builder) use ($options): void {
|
||||||
|
$this->getFilterQuery($builder, $options);
|
||||||
},
|
},
|
||||||
|
'detail_query' => $this->getDetailQuery(...),
|
||||||
'criteria' => [
|
'criteria' => [
|
||||||
function (QueryBuilder $builder) use ($options): void {
|
function (QueryBuilder $builder) use ($options): void {
|
||||||
$this->buildCriteria($builder, $options);
|
$this->buildCriteria($builder, $options);
|
||||||
|
|
@ -221,20 +231,71 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getQuery(QueryBuilder $builder, array $options): void
|
private function getFilterQuery(QueryBuilder $builder, array $options): void
|
||||||
{
|
{
|
||||||
$builder->select('bom_entry')
|
$builder
|
||||||
->addSelect('part')
|
->select('bom_entry.id')
|
||||||
->from(ProjectBOMEntry::class, 'bom_entry')
|
->from(ProjectBOMEntry::class, 'bom_entry')
|
||||||
->leftJoin('bom_entry.part', 'part')
|
->leftJoin('bom_entry.part', 'part')
|
||||||
->leftJoin('part.category', 'category')
|
->leftJoin('part.category', 'category')
|
||||||
|
->leftJoin('part.partLots', '_partLots')
|
||||||
|
->leftJoin('_partLots.storage_location', '_storelocations')
|
||||||
->leftJoin('part.footprint', 'footprint')
|
->leftJoin('part.footprint', 'footprint')
|
||||||
->leftJoin('part.manufacturer', 'manufacturer')
|
->leftJoin('part.manufacturer', 'manufacturer')
|
||||||
|
->leftJoin('part.partCustomState', 'partCustomState')
|
||||||
->where('bom_entry.project = :project')
|
->where('bom_entry.project = :project')
|
||||||
->setParameter('project', $options['project'])
|
->setParameter('project', $options['project'])
|
||||||
|
->addGroupBy('bom_entry')
|
||||||
|
->addGroupBy('part')
|
||||||
|
->addGroupBy('category')
|
||||||
|
->addGroupBy('footprint')
|
||||||
|
->addGroupBy('manufacturer')
|
||||||
|
->addGroupBy('partCustomState')
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getDetailQuery(QueryBuilder $builder, array $filter_results): void
|
||||||
|
{
|
||||||
|
$ids = array_map(static fn (array $row) => $row['id'], $filter_results);
|
||||||
|
if ($ids === []) {
|
||||||
|
$ids = [-1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder
|
||||||
|
->select('bom_entry')
|
||||||
|
->addSelect('part')
|
||||||
|
->addSelect('category')
|
||||||
|
->addSelect('partLots')
|
||||||
|
->addSelect('storelocations')
|
||||||
|
->addSelect('footprint')
|
||||||
|
->addSelect('manufacturer')
|
||||||
|
->addSelect('partCustomState')
|
||||||
|
->from(ProjectBOMEntry::class, 'bom_entry')
|
||||||
|
->leftJoin('bom_entry.part', 'part')
|
||||||
|
->leftJoin('part.category', 'category')
|
||||||
|
->leftJoin('part.partLots', 'partLots')
|
||||||
|
->leftJoin('partLots.storage_location', 'storelocations')
|
||||||
|
->leftJoin('part.footprint', 'footprint')
|
||||||
|
->leftJoin('part.manufacturer', 'manufacturer')
|
||||||
|
->leftJoin('part.partCustomState', 'partCustomState')
|
||||||
|
->where('bom_entry.id IN (:ids)')
|
||||||
|
->setParameter('ids', $ids)
|
||||||
|
->addGroupBy('bom_entry')
|
||||||
|
->addGroupBy('part')
|
||||||
|
->addGroupBy('partLots')
|
||||||
|
->addGroupBy('category')
|
||||||
|
->addGroupBy('storelocations')
|
||||||
|
->addGroupBy('footprint')
|
||||||
|
->addGroupBy('manufacturer')
|
||||||
|
->addGroupBy('partCustomState')
|
||||||
|
|
||||||
|
->setHint(Query::HINT_READ_ONLY, true)
|
||||||
|
->setHint(Query::HINT_FORCE_PARTIAL_LOAD, false)
|
||||||
|
;
|
||||||
|
|
||||||
|
FieldHelper::addOrderByFieldParam($builder, 'bom_entry.id', 'ids');
|
||||||
|
}
|
||||||
|
|
||||||
private function buildCriteria(QueryBuilder $builder, array $options): void
|
private function buildCriteria(QueryBuilder $builder, array $options): void
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ class TypeSynonymRowType extends AbstractType
|
||||||
*/
|
*/
|
||||||
private function getPreferredLocales(): array
|
private function getPreferredLocales(): array
|
||||||
{
|
{
|
||||||
$fromSettings = $this->localizationSettings->languageMenuEntries ?? [];
|
$fromSettings = $this->localizationSettings->languageMenuEntries;
|
||||||
return !empty($fromSettings) ? array_values($fromSettings) : array_values($this->preferredLanguagesParam);
|
return !empty($fromSettings) ? array_values($fromSettings) : array_values($this->preferredLanguagesParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ use App\Exceptions\AttachmentDownloadException;
|
||||||
use App\Settings\SystemSettings\AttachmentsSettings;
|
use App\Settings\SystemSettings\AttachmentsSettings;
|
||||||
use Hshn\Base64EncodedFile\HttpFoundation\File\Base64EncodedFile;
|
use Hshn\Base64EncodedFile\HttpFoundation\File\Base64EncodedFile;
|
||||||
use Hshn\Base64EncodedFile\HttpFoundation\File\UploadedBase64EncodedFile;
|
use Hshn\Base64EncodedFile\HttpFoundation\File\UploadedBase64EncodedFile;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient;
|
||||||
use const DIRECTORY_SEPARATOR;
|
use const DIRECTORY_SEPARATOR;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
@ -76,6 +78,8 @@ class AttachmentSubmitHandler
|
||||||
protected FileTypeFilterTools $filterTools,
|
protected FileTypeFilterTools $filterTools,
|
||||||
protected AttachmentsSettings $settings,
|
protected AttachmentsSettings $settings,
|
||||||
protected readonly SVGSanitizer $SVGSanitizer,
|
protected readonly SVGSanitizer $SVGSanitizer,
|
||||||
|
#[Autowire(env: "bool:ALLOW_ATTACHMENT_DOWNLOADS_FROM_LOCALNETWORK")]
|
||||||
|
private readonly bool $allow_local_network_downloads = false,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
//The mapping used to determine which folder will be used for an attachment type
|
//The mapping used to determine which folder will be used for an attachment type
|
||||||
|
|
@ -95,6 +99,10 @@ class AttachmentSubmitHandler
|
||||||
UserAttachment::class => 'user',
|
UserAttachment::class => 'user',
|
||||||
LabelAttachment::class => 'label_profile',
|
LabelAttachment::class => 'label_profile',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!$this->allow_local_network_downloads) {
|
||||||
|
$this->httpClient = new NoPrivateNetworkHttpClient($this->httpClient);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -373,6 +381,7 @@ class AttachmentSubmitHandler
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->httpClient->request('GET', $url, $opts);
|
$response = $this->httpClient->request('GET', $url, $opts);
|
||||||
//Digikey wants TLSv1.3, so try again with that if we get a 403
|
//Digikey wants TLSv1.3, so try again with that if we get a 403
|
||||||
if ($response->getStatusCode() === 403) {
|
if ($response->getStatusCode() === 403) {
|
||||||
|
|
@ -434,8 +443,8 @@ class AttachmentSubmitHandler
|
||||||
$new_path = $this->pathResolver->realPathToPlaceholder($new_path);
|
$new_path = $this->pathResolver->realPathToPlaceholder($new_path);
|
||||||
//Save the path to the attachment
|
//Save the path to the attachment
|
||||||
$attachment->setInternalPath($new_path);
|
$attachment->setInternalPath($new_path);
|
||||||
} catch (TransportExceptionInterface) {
|
} catch (TransportExceptionInterface $exception) {
|
||||||
throw new AttachmentDownloadException('Transport error!');
|
throw new AttachmentDownloadException('Transport error: '.$exception->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return $attachment;
|
return $attachment;
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ use Brick\Schema\Interfaces\Thing;
|
||||||
use Brick\Schema\SchemaReader;
|
use Brick\Schema\SchemaReader;
|
||||||
use Brick\Schema\SchemaTypeList;
|
use Brick\Schema\SchemaTypeList;
|
||||||
use Symfony\Component\DomCrawler\Crawler;
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
class GenericWebProvider implements InfoProviderInterface
|
class GenericWebProvider implements InfoProviderInterface
|
||||||
|
|
@ -55,7 +56,8 @@ class GenericWebProvider implements InfoProviderInterface
|
||||||
private readonly ProviderRegistry $providerRegistry, private readonly PartInfoRetriever $infoRetriever,
|
private readonly ProviderRegistry $providerRegistry, private readonly PartInfoRetriever $infoRetriever,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
$this->httpClient = (new RandomizeUseragentHttpClient($httpClient))->withOptions(
|
//Use NoPrivateNetworkHttpClient to prevent SSRF vulnerabilities, and RandomizeUseragentHttpClient to make it harder for servers to block us
|
||||||
|
$this->httpClient = (new RandomizeUseragentHttpClient(new NoPrivateNetworkHttpClient($httpClient)))->withOptions(
|
||||||
[
|
[
|
||||||
'timeout' => 15,
|
'timeout' => 15,
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -280,9 +280,13 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
|
||||||
{
|
{
|
||||||
//If a URL starts with // we assume that it is a relative URL and we add the protocol
|
//If a URL starts with // we assume that it is a relative URL and we add the protocol
|
||||||
if (str_starts_with($url, '//')) {
|
if (str_starts_with($url, '//')) {
|
||||||
return 'https:' . $url;
|
$url = 'https:' . $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Encode bare % signs that are not already part of a valid percent-encoded sequence
|
||||||
|
//Fixes part numbers with % in them e.g. SMD0603-5K1-1%
|
||||||
|
$url = preg_replace('/%(?![0-9A-Fa-f]{2})/', '%25', $url);
|
||||||
|
|
||||||
return $url;
|
return $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,10 @@ 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);
|
||||||
|
|
||||||
|
|
@ -144,6 +148,11 @@ 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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,6 +171,7 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,6 +240,26 @@ final readonly class BarcodeScanResultHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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($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.
|
||||||
|
|
@ -247,6 +271,20 @@ 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 [
|
||||||
|
|
|
||||||
|
|
@ -52,4 +52,7 @@ 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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,12 +254,16 @@ readonly class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
||||||
*/
|
*/
|
||||||
public static function isFormat06Code(string $input): bool
|
public static function isFormat06Code(string $input): bool
|
||||||
{
|
{
|
||||||
//Code must begin with [)><RS>06<GS>
|
//Code should begin with [)><RS>06<GS> as per the standard
|
||||||
if(!str_starts_with($input, "[)>\u{1E}06\u{1D}")){
|
if(!str_starts_with($input, "[)>\u{1E}06\u{1D}")
|
||||||
return false;
|
// some codes don't contain record separators
|
||||||
|
&& !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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
143
src/Services/LabelSystem/BarcodeScanner/TMEBarcodeScanResult.php
Normal file
143
src/Services/LabelSystem/BarcodeScanner/TMEBarcodeScanResult.php
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
tests/Doctrine/Functions/ArrayPositionTest.php
Normal file
42
tests/Doctrine/Functions/ArrayPositionTest.php
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
45
tests/Doctrine/Functions/Field2Test.php
Normal file
45
tests/Doctrine/Functions/Field2Test.php
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
66
tests/Doctrine/Functions/ILikeTest.php
Normal file
66
tests/Doctrine/Functions/ILikeTest.php
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
95
tests/Doctrine/Functions/NatsortTest.php
Normal file
95
tests/Doctrine/Functions/NatsortTest.php
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
66
tests/Doctrine/Functions/RegexpTest.php
Normal file
66
tests/Doctrine/Functions/RegexpTest.php
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
391
tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php
Normal file
391
tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php
Normal file
|
|
@ -0,0 +1,391 @@
|
||||||
|
<?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\Tests\Services\InfoProviderSystem\Providers;
|
||||||
|
|
||||||
|
use App\Entity\Parts\ManufacturingStatus;
|
||||||
|
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||||
|
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||||
|
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||||
|
use App\Services\InfoProviderSystem\Providers\ProviderCapabilities;
|
||||||
|
use App\Services\InfoProviderSystem\Providers\TMEClient;
|
||||||
|
use App\Services\InfoProviderSystem\Providers\TMEProvider;
|
||||||
|
use App\Settings\InfoProviderSystem\TMESettings;
|
||||||
|
use App\Tests\SettingsTestHelper;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||||
|
|
||||||
|
final class TMEProviderTest extends TestCase
|
||||||
|
{
|
||||||
|
private TMESettings $settings;
|
||||||
|
private TMEProvider $provider;
|
||||||
|
private MockHttpClient $httpClient;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->httpClient = new MockHttpClient();
|
||||||
|
$this->settings = SettingsTestHelper::createSettingsDummy(TMESettings::class);
|
||||||
|
// Use a short (anonymous-style) token so grossPrices is read from settings
|
||||||
|
$this->settings->apiToken = 'test_token_000000000000000000000000000000000000000';
|
||||||
|
$this->settings->apiSecret = 'test_secret';
|
||||||
|
$this->settings->currency = 'EUR';
|
||||||
|
$this->settings->language = 'en';
|
||||||
|
$this->settings->country = 'DE';
|
||||||
|
$this->settings->grossPrices = false;
|
||||||
|
$this->provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings), $this->settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mock response helpers ---
|
||||||
|
// Only fields actually read by TMEProvider are included.
|
||||||
|
|
||||||
|
private function mockProductList(array $products): MockResponse
|
||||||
|
{
|
||||||
|
return new MockResponse(json_encode([
|
||||||
|
'Status' => 'OK',
|
||||||
|
'Data' => ['ProductList' => $products],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockFilesList(array $products): MockResponse
|
||||||
|
{
|
||||||
|
return new MockResponse(json_encode([
|
||||||
|
'Status' => 'OK',
|
||||||
|
'Data' => ['ProductList' => $products],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockParametersList(array $products): MockResponse
|
||||||
|
{
|
||||||
|
return new MockResponse(json_encode([
|
||||||
|
'Status' => 'OK',
|
||||||
|
'Data' => ['ProductList' => $products],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockPrices(string $currency, string $priceType, array $products): MockResponse
|
||||||
|
{
|
||||||
|
return new MockResponse(json_encode([
|
||||||
|
'Status' => 'OK',
|
||||||
|
'Data' => [
|
||||||
|
'Currency' => $currency,
|
||||||
|
'PriceType' => $priceType,
|
||||||
|
'ProductList' => $products,
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mock data ---
|
||||||
|
|
||||||
|
private function smd0603Products(): MockResponse
|
||||||
|
{
|
||||||
|
return $this->mockProductList([[
|
||||||
|
'Symbol' => 'SMD0603-5K1-1%',
|
||||||
|
'OriginalSymbol' => '0603SAF5101T5E',
|
||||||
|
'Producer' => 'ROYALOHM',
|
||||||
|
'Description' => 'Resistor: thick film; SMD; 0603; 5.1kΩ; 0.1W; ±1%; 50V; -55÷155°C',
|
||||||
|
'Category' => 'SMD resistors',
|
||||||
|
'Photo' => '//ce8dc832c.cloudimg.io/v7/_cdn_/E9/C2/B0/00/0/732318_1.jpg',
|
||||||
|
'ProductStatusList' => [],
|
||||||
|
'ProductInformationPage' => '//www.tme.eu/en/details/smd0603-5k1-1%/smd-resistors/royalohm/0603saf5101t5e/',
|
||||||
|
'Weight' => 0.021,
|
||||||
|
'WeightUnit' => 'g',
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function smd0603Files(): MockResponse
|
||||||
|
{
|
||||||
|
return $this->mockFilesList([[
|
||||||
|
'Symbol' => 'SMD0603-5K1-1%',
|
||||||
|
'Files' => [
|
||||||
|
'AdditionalPhotoList' => [],
|
||||||
|
'DocumentList' => [
|
||||||
|
['DocumentUrl' => '//www.tme.eu/Document/b315665a56acbc42df513c99b390ad98/ROYALOHM-THICKFILM.pdf'],
|
||||||
|
['DocumentUrl' => '//www.tme.eu/Document/c283990e907c122bb808207d1578ac7f/POWER_RATING-DTE.pdf'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function smd0603Parameters(): MockResponse
|
||||||
|
{
|
||||||
|
return $this->mockParametersList([[
|
||||||
|
'Symbol' => 'SMD0603-5K1-1%',
|
||||||
|
'ParameterList' => [
|
||||||
|
['ParameterId' => 34, 'ParameterName' => 'Type of resistor', 'ParameterValue' => 'thick film'],
|
||||||
|
['ParameterId' => 35, 'ParameterName' => 'Case - mm', 'ParameterValue' => '1608'],
|
||||||
|
['ParameterId' => 38, 'ParameterName' => 'Resistance', 'ParameterValue' => '5.1kΩ'],
|
||||||
|
['ParameterId' => 39, 'ParameterName' => 'Tolerance', 'ParameterValue' => '±1%'],
|
||||||
|
['ParameterId' => 120, 'ParameterName' => 'Operating voltage', 'ParameterValue' => '50V'],
|
||||||
|
],
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function smd0603Prices(): MockResponse
|
||||||
|
{
|
||||||
|
return $this->mockPrices('EUR', 'NET', [[
|
||||||
|
'Symbol' => 'SMD0603-5K1-1%',
|
||||||
|
'PriceList' => [
|
||||||
|
['Amount' => 100, 'PriceValue' => 0.01077],
|
||||||
|
['Amount' => 1000, 'PriceValue' => 0.00291],
|
||||||
|
['Amount' => 5000, 'PriceValue' => 0.00150],
|
||||||
|
],
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function etqp3mProducts(): MockResponse
|
||||||
|
{
|
||||||
|
return $this->mockProductList([[
|
||||||
|
'Symbol' => 'ETQP3M6R8KVP',
|
||||||
|
'OriginalSymbol' => 'ETQP3M6R8KVP',
|
||||||
|
'Producer' => 'PANASONIC',
|
||||||
|
'Description' => 'Inductor: wire; SMD; 6.8uH; 2.9A; R: 65.7mΩ; ±20%; ETQP3M; 5.5x5x3mm',
|
||||||
|
'Category' => 'Inductors',
|
||||||
|
'Photo' => '//ce8dc832c.cloudimg.io/v7/_cdn_/9E/27/A0/00/0/684777_1.jpg',
|
||||||
|
'ProductStatusList' => [],
|
||||||
|
'ProductInformationPage' => '//www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/',
|
||||||
|
'Weight' => 0.44,
|
||||||
|
'WeightUnit' => 'g',
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function etqp3mFiles(): MockResponse
|
||||||
|
{
|
||||||
|
return $this->mockFilesList([[
|
||||||
|
'Symbol' => 'ETQP3M6R8KVP',
|
||||||
|
'Files' => [
|
||||||
|
'AdditionalPhotoList' => [],
|
||||||
|
'DocumentList' => [
|
||||||
|
['DocumentUrl' => '//www.tme.eu/Document/50a845881f09d8a2248350946e11df38/AGL0000C63.pdf'],
|
||||||
|
['DocumentUrl' => '//www.tme.eu/Document/8480690a42fa577214e35e33d3fc8d77/ETQP3M100KVN-LNK.txt'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function etqp3mParameters(): MockResponse
|
||||||
|
{
|
||||||
|
return $this->mockParametersList([[
|
||||||
|
'Symbol' => 'ETQP3M6R8KVP',
|
||||||
|
'ParameterList' => [
|
||||||
|
['ParameterId' => 566, 'ParameterName' => 'Inductance', 'ParameterValue' => '6.8µH'],
|
||||||
|
['ParameterId' => 370, 'ParameterName' => 'Operating current', 'ParameterValue' => '2.9A'],
|
||||||
|
['ParameterId' => 39, 'ParameterName' => 'Tolerance', 'ParameterValue' => '±20%'],
|
||||||
|
],
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function etqp3mPrices(): MockResponse
|
||||||
|
{
|
||||||
|
return $this->mockPrices('EUR', 'NET', [[
|
||||||
|
'Symbol' => 'ETQP3M6R8KVP',
|
||||||
|
'PriceList' => [
|
||||||
|
['Amount' => 1, 'PriceValue' => 0.589],
|
||||||
|
['Amount' => 5, 'PriceValue' => 0.429],
|
||||||
|
['Amount' => 10, 'PriceValue' => 0.399],
|
||||||
|
],
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
public function testGetProviderInfo(): void
|
||||||
|
{
|
||||||
|
$info = $this->provider->getProviderInfo();
|
||||||
|
|
||||||
|
$this->assertIsArray($info);
|
||||||
|
$this->assertArrayHasKey('name', $info);
|
||||||
|
$this->assertArrayHasKey('description', $info);
|
||||||
|
$this->assertArrayHasKey('url', $info);
|
||||||
|
$this->assertEquals('TME', $info['name']);
|
||||||
|
$this->assertEquals('https://tme.eu/', $info['url']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetProviderKey(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('tme', $this->provider->getProviderKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsActiveWithCredentials(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue($this->provider->isActive());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsActiveWithoutCredentials(): void
|
||||||
|
{
|
||||||
|
$this->settings->apiToken = null;
|
||||||
|
$provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings), $this->settings);
|
||||||
|
$this->assertFalse($provider->isActive());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCapabilities(): void
|
||||||
|
{
|
||||||
|
$capabilities = $this->provider->getCapabilities();
|
||||||
|
|
||||||
|
$this->assertIsArray($capabilities);
|
||||||
|
$this->assertContains(ProviderCapabilities::BASIC, $capabilities);
|
||||||
|
$this->assertContains(ProviderCapabilities::PICTURE, $capabilities);
|
||||||
|
$this->assertContains(ProviderCapabilities::DATASHEET, $capabilities);
|
||||||
|
$this->assertContains(ProviderCapabilities::PRICE, $capabilities);
|
||||||
|
$this->assertContains(ProviderCapabilities::FOOTPRINT, $capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetHandledDomains(): void
|
||||||
|
{
|
||||||
|
$this->assertContains('tme.eu', $this->provider->getHandledDomains());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetIDFromURL(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('fi321_se', $this->provider->getIDFromURL('https://www.tme.eu/de/details/fi321_se/kuhler/alutronic/'));
|
||||||
|
$this->assertSame('smd0603-5k1-1%25', $this->provider->getIDFromURL('https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/'));
|
||||||
|
$this->assertNull($this->provider->getIDFromURL('https://www.tme.eu/en/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSearchByKeyword(): void
|
||||||
|
{
|
||||||
|
$this->httpClient->setResponseFactory([$this->smd0603Products()]);
|
||||||
|
|
||||||
|
$results = $this->provider->searchByKeyword('SMD0603-5K1-1%');
|
||||||
|
|
||||||
|
$this->assertIsArray($results);
|
||||||
|
$this->assertCount(1, $results);
|
||||||
|
$this->assertInstanceOf(SearchResultDTO::class, $results[0]);
|
||||||
|
$this->assertSame('SMD0603-5K1-1%', $results[0]->provider_id);
|
||||||
|
$this->assertSame('0603SAF5101T5E', $results[0]->name);
|
||||||
|
$this->assertSame('ROYALOHM', $results[0]->manufacturer);
|
||||||
|
$this->assertSame('SMD resistors', $results[0]->category);
|
||||||
|
$this->assertSame(ManufacturingStatus::ACTIVE, $results[0]->manufacturing_status);
|
||||||
|
$this->assertSame(
|
||||||
|
'https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/',
|
||||||
|
$results[0]->provider_url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetDetailsWithPercentInPartNumber(): void
|
||||||
|
{
|
||||||
|
$this->httpClient->setResponseFactory([
|
||||||
|
$this->smd0603Products(),
|
||||||
|
$this->smd0603Files(),
|
||||||
|
$this->smd0603Parameters(),
|
||||||
|
$this->smd0603Prices(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->provider->getDetails('SMD0603-5K1-1%');
|
||||||
|
|
||||||
|
$this->assertInstanceOf(PartDetailDTO::class, $result);
|
||||||
|
$this->assertSame('SMD0603-5K1-1%', $result->provider_id);
|
||||||
|
$this->assertSame('0603SAF5101T5E', $result->name);
|
||||||
|
$this->assertSame('Resistor: thick film; SMD; 0603; 5.1kΩ; 0.1W; ±1%; 50V; -55÷155°C', $result->description);
|
||||||
|
$this->assertSame('ROYALOHM', $result->manufacturer);
|
||||||
|
$this->assertSame('0603SAF5101T5E', $result->mpn);
|
||||||
|
$this->assertSame('SMD resistors', $result->category);
|
||||||
|
$this->assertSame(ManufacturingStatus::ACTIVE, $result->manufacturing_status);
|
||||||
|
$this->assertSame(0.021, $result->mass);
|
||||||
|
$this->assertSame('1608', $result->footprint);
|
||||||
|
$this->assertSame(
|
||||||
|
'https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/',
|
||||||
|
$result->provider_url
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertCount(2, $result->datasheets);
|
||||||
|
$this->assertSame('https://www.tme.eu/Document/b315665a56acbc42df513c99b390ad98/ROYALOHM-THICKFILM.pdf', $result->datasheets[0]->url);
|
||||||
|
$this->assertCount(0, $result->images);
|
||||||
|
|
||||||
|
$this->assertCount(1, $result->vendor_infos);
|
||||||
|
$vendorInfo = $result->vendor_infos[0];
|
||||||
|
$this->assertInstanceOf(PurchaseInfoDTO::class, $vendorInfo);
|
||||||
|
$this->assertSame('TME', $vendorInfo->distributor_name);
|
||||||
|
$this->assertSame('SMD0603-5K1-1%', $vendorInfo->order_number);
|
||||||
|
$this->assertSame(
|
||||||
|
'https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/',
|
||||||
|
$vendorInfo->product_url
|
||||||
|
);
|
||||||
|
$this->assertCount(3, $vendorInfo->prices);
|
||||||
|
$this->assertSame(100.0, $vendorInfo->prices[0]->minimum_discount_amount);
|
||||||
|
$this->assertSame('0.01077', $vendorInfo->prices[0]->price);
|
||||||
|
$this->assertSame('EUR', $vendorInfo->prices[0]->currency_iso_code);
|
||||||
|
$this->assertFalse($vendorInfo->prices[0]->includes_tax);
|
||||||
|
|
||||||
|
$this->assertCount(5, $result->parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetDetailsForEtqp3m6r8kvp(): void
|
||||||
|
{
|
||||||
|
$this->httpClient->setResponseFactory([
|
||||||
|
$this->etqp3mProducts(),
|
||||||
|
$this->etqp3mFiles(),
|
||||||
|
$this->etqp3mParameters(),
|
||||||
|
$this->etqp3mPrices(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->provider->getDetails('ETQP3M6R8KVP');
|
||||||
|
|
||||||
|
$this->assertInstanceOf(PartDetailDTO::class, $result);
|
||||||
|
$this->assertSame('ETQP3M6R8KVP', $result->provider_id);
|
||||||
|
$this->assertSame('ETQP3M6R8KVP', $result->name);
|
||||||
|
$this->assertSame('Inductor: wire; SMD; 6.8uH; 2.9A; R: 65.7mΩ; ±20%; ETQP3M; 5.5x5x3mm', $result->description);
|
||||||
|
$this->assertSame('PANASONIC', $result->manufacturer);
|
||||||
|
$this->assertSame('ETQP3M6R8KVP', $result->mpn);
|
||||||
|
$this->assertSame('Inductors', $result->category);
|
||||||
|
$this->assertSame(ManufacturingStatus::ACTIVE, $result->manufacturing_status);
|
||||||
|
$this->assertSame(0.44, $result->mass);
|
||||||
|
$this->assertNull($result->footprint);
|
||||||
|
$this->assertSame('https://www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/', $result->provider_url);
|
||||||
|
|
||||||
|
$this->assertCount(2, $result->datasheets);
|
||||||
|
$this->assertSame('https://www.tme.eu/Document/50a845881f09d8a2248350946e11df38/AGL0000C63.pdf', $result->datasheets[0]->url);
|
||||||
|
$this->assertCount(0, $result->images);
|
||||||
|
|
||||||
|
$this->assertCount(1, $result->vendor_infos);
|
||||||
|
$vendorInfo = $result->vendor_infos[0];
|
||||||
|
$this->assertSame('TME', $vendorInfo->distributor_name);
|
||||||
|
$this->assertSame('ETQP3M6R8KVP', $vendorInfo->order_number);
|
||||||
|
$this->assertSame('https://www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/', $vendorInfo->product_url);
|
||||||
|
$this->assertCount(3, $vendorInfo->prices);
|
||||||
|
$this->assertSame(1.0, $vendorInfo->prices[0]->minimum_discount_amount);
|
||||||
|
$this->assertSame('0.589', $vendorInfo->prices[0]->price);
|
||||||
|
$this->assertSame('EUR', $vendorInfo->prices[0]->currency_iso_code);
|
||||||
|
$this->assertFalse($vendorInfo->prices[0]->includes_tax);
|
||||||
|
|
||||||
|
$this->assertCount(3, $result->parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNormalizeURLEncodesBarePctSign(): void
|
||||||
|
{
|
||||||
|
$method = (new \ReflectionClass($this->provider))->getMethod('normalizeURL');
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/',
|
||||||
|
$method->invoke($this->provider, '//www.tme.eu/en/details/smd0603-5k1-1%/smd-resistors/royalohm/0603saf5101t5e/')
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/',
|
||||||
|
$method->invoke($this->provider, '//www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/')
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'https://www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/',
|
||||||
|
$method->invoke($this->provider, '//www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/')
|
||||||
|
);
|
||||||
|
$this->assertSame('https://example.com/path', $method->invoke($this->provider, 'https://example.com/path'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -93,6 +93,13 @@ 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
|
||||||
|
|
@ -101,6 +108,32 @@ 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");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.en">
|
<file id="messages.de">
|
||||||
<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,6 +12861,12 @@ 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>
|
||||||
|
|
|
||||||
|
|
@ -12947,6 +12947,12 @@ 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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue