Compare commits

...

6 commits

Author SHA1 Message Date
Jan Böhmer
ad0c60f766 Fixed BigDecimal::of error
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
2026-05-19 22:02:15 +02:00
Jan Böhmer
527c42c227 Upgraded brick/math to latest version 2026-05-19 21:18:17 +02:00
Jan Böhmer
506d5f8173 Updated symfony/ai bundle 2026-05-19 19:52:45 +02:00
Jan Böhmer
846ecdf02e Mention browser plugin in documentation 2026-05-19 17:18:21 +02:00
Jan Böhmer
a9fa92c98e Updated dependencies 2026-05-19 16:32:48 +02:00
Jan Böhmer
d237446334 Updated permissions 2026-05-15 01:00:52 +02:00
17 changed files with 1042 additions and 999 deletions

View file

@ -61,7 +61,8 @@ for the first time.
* Automatic thumbnail generation for pictures * Automatic thumbnail generation for pictures
* Use cloud providers (like Octopart, Digikey, Farnell, LCSC or TME) to automatically get part information, datasheets, and * Use cloud providers (like Octopart, Digikey, Farnell, LCSC or TME) to automatically get part information, datasheets, and
prices for parts prices for parts
* Retrieve part information from arbitrary shop websites, using either conventional data extraction from structured metadata, or AI based data extraction * Retrieve part information from arbitrary shop websites, using either conventional data extraction from structured metadata, or AI based data extraction.
A browser plugin allows to quickly submit parts from any website to your Part-DB instance, and even allows to circumvent anti-bot measures on shop websites.
* API to access Part-DB from other applications/scripts * API to access Part-DB from other applications/scripts
* [Integration with KiCad](https://docs.part-db.de/usage/eda_integration.html): Use Part-DB as the central datasource for your * [Integration with KiCad](https://docs.part-db.de/usage/eda_integration.html): Use Part-DB as the central datasource for your
KiCad and see available parts from Part-DB directly inside KiCad. KiCad and see available parts from Part-DB directly inside KiCad.

View file

@ -17,7 +17,7 @@
"api-platform/json-api": "^4.0.0", "api-platform/json-api": "^4.0.0",
"api-platform/symfony": "^4.0.0", "api-platform/symfony": "^4.0.0",
"beberlei/doctrineextensions": "^1.2", "beberlei/doctrineextensions": "^1.2",
"brick/math": "^0.14.8", "brick/math": "^0.17.0",
"brick/schema": "^0.2.0", "brick/schema": "^0.2.0",
"composer/ca-bundle": "^1.5", "composer/ca-bundle": "^1.5",
"composer/package-versions-deprecated": "^1.11.99.5", "composer/package-versions-deprecated": "^1.11.99.5",
@ -57,9 +57,9 @@
"scheb/2fa-trusted-device": "^v7.11.0", "scheb/2fa-trusted-device": "^v7.11.0",
"shivas/versioning-bundle": "^4.0", "shivas/versioning-bundle": "^4.0",
"spatie/db-dumper": "^3.3.1", "spatie/db-dumper": "^3.3.1",
"symfony/ai-bundle": "^0.8.0", "symfony/ai-bundle": "^0.9.0",
"symfony/ai-lm-studio-platform": "^0.8.0", "symfony/ai-lm-studio-platform": "^0.9.0",
"symfony/ai-open-router-platform": "^0.8.0", "symfony/ai-open-router-platform": "^0.9.0",
"symfony/apache-pack": "^1.0", "symfony/apache-pack": "^1.0",
"symfony/asset": "7.4.*", "symfony/asset": "7.4.*",
"symfony/console": "7.4.*", "symfony/console": "7.4.*",

611
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2823,6 +2823,13 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* region?: scalar|Param|null, // The region for OpenAI API (EU, US, or null for default) // Default: null * region?: scalar|Param|null, // The region for OpenAI API (EU, US, or null for default) // Default: null
* http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client"
* }, * },
* openresponses?: array<string, array{ // Default: []
* base_url?: string|Param,
* api_key?: string|Param,
* http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client"
* model_catalog?: string|Param, // Service ID of the model catalog to use
* responses_path?: string|Param, // Default: "/v1/responses"
* }>,
* openrouter?: array{ * openrouter?: array{
* api_key?: string|Param, * api_key?: string|Param,
* http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client"
@ -2957,6 +2964,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* endpoint?: string|Param, * endpoint?: string|Param,
* api_key?: string|Param, * api_key?: string|Param,
* index_name?: string|Param, * index_name?: string|Param,
* http_client?: string|Param, // Default: "http_client"
* embedder?: string|Param, // Default: "default" * embedder?: string|Param, // Default: "default"
* vector_field?: string|Param, // Default: "_vectors" * vector_field?: string|Param, // Default: "_vectors"
* dimensions?: int|Param, // Default: 1536 * dimensions?: int|Param, // Default: 1536
@ -3019,6 +3027,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* table_name?: string|Param, * table_name?: string|Param,
* vector_field?: string|Param, // Default: "embedding" * vector_field?: string|Param, // Default: "embedding"
* distance?: "cosine"|"inner_product"|"l1"|"l2"|Param, // Distance metric to use for vector similarity search // Default: "l2" * distance?: "cosine"|"inner_product"|"l1"|"l2"|Param, // Distance metric to use for vector similarity search // Default: "l2"
* lang?: string|Param, // Default: "english"
* dbal_connection?: string|Param, * dbal_connection?: string|Param,
* setup_options?: array{ * setup_options?: array{
* vector_type?: string|Param, // Default: "vector" * vector_type?: string|Param, // Default: "vector"

View file

@ -75,6 +75,15 @@ the parts you want to update. In the bulk actions dropdown select "Bulk info pro
You will be redirected to a page, where you can select how part fields should be mapped to info provider fields, and the You will be redirected to a page, where you can select how part fields should be mapped to info provider fields, and the
results will be shown. results will be shown.
## Browser plugin
There is a browser plugin available for [Chrome](https://chromewebstore.google.com/detail/part-db-page-submitter/bckkfkpidiiibmjdhjakleoagjmepioi) and [Firefox](https://addons.mozilla.org/de/firefox/addon/part-db-page-submitter/)
that allows to submit a website from your browser with one click to Part-DB, which then utilizes the Generic Web URL or the AI Web Provider to extract the part information from the page and pre-fill the part creation form.
The advantage is that it also works for pages behind logins, CAPTCHAs, or bot-blocking sites, as the plugin sends the already loaded page HTML to Part-DB.
The plugin is open source and available on [GitHub](https://github.com/Part-DB/browser-plugin).
To use it install it in your browser, enable one or more of the web page providers in Part-DB and allow the plugin support
in Part-DB settings. After that you can submit any product page to Part-DB with one click and the part creation form will be pre-filled with the information from the page.
## Data providers ## Data providers
The system tries to be as flexible as possible, so many different information sources can be used. The system tries to be as flexible as possible, so many different information sources can be used.

View file

@ -37,6 +37,7 @@ use App\Services\EntityURLGenerator;
use App\Services\Formatters\AmountFormatter; use App\Services\Formatters\AmountFormatter;
use App\Services\Formatters\MoneyFormatter; use App\Services\Formatters\MoneyFormatter;
use App\Services\ProjectSystem\ProjectBuildHelper; use App\Services\ProjectSystem\ProjectBuildHelper;
use Brick\Math\BigDecimal;
use Brick\Math\RoundingMode; use Brick\Math\RoundingMode;
use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
@ -93,14 +94,14 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
return htmlspecialchars($this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit())); return htmlspecialchars($this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit()));
}, },
]) ])
->add('partId', TextColumn::class, [ ->add('partId', TextColumn::class, [
'label' => $this->translator->trans('project.bom.part_id'), 'label' => $this->translator->trans('project.bom.part_id'),
'visible' => true, 'visible' => true,
'orderField' => 'part.id', 'orderField' => 'part.id',
'render' => function ($value, ProjectBOMEntry $context) { 'render' => function ($value, ProjectBOMEntry $context) {
return $context->getPart() instanceof Part ? (string) $context->getPart()->getId() : ''; return $context->getPart() instanceof Part ? (string) $context->getPart()->getId() : '';
}, },
]) ])
->add('name', TextColumn::class, [ ->add('name', TextColumn::class, [
'label' => $this->translator->trans('part.table.name'), 'label' => $this->translator->trans('part.table.name'),
'orderField' => 'NATSORT(part.name)', 'orderField' => 'NATSORT(part.name)',
@ -161,7 +162,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
'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', '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) {
return ''; return '';
@ -212,7 +213,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
'visible' => false, 'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) { 'render' => function ($value, ProjectBOMEntry $context) {
$price = $this->projectBuildHelper->getEntryUnitPrice($context); $price = $this->projectBuildHelper->getEntryUnitPrice($context);
return $this->moneyFormatter->format($price->toScale(2, RoundingMode::UP)->toFloat(), null, 2, true); return $this->moneyFormatter->format($price->toScale(2, RoundingMode::Up)->toFloat(), null, 2, true);
}, },
]) ])
->add('ext_price', TextColumn::class, [ ->add('ext_price', TextColumn::class, [
@ -221,7 +222,8 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
'render' => function ($value, ProjectBOMEntry $context) { 'render' => function ($value, ProjectBOMEntry $context) {
$price = $this->projectBuildHelper->getEntryUnitPrice($context); $price = $this->projectBuildHelper->getEntryUnitPrice($context);
return $this->moneyFormatter->format( return $this->moneyFormatter->format(
$price->multipliedBy($context->getQuantity())->toScale(2, RoundingMode::UP)->toFloat(), $price->multipliedBy(BigDecimal::fromFloatShortest($context->getQuantity()))
->toScale(2, RoundingMode::Up)->toFloat(),
null, null,
2, 2,
true true

View file

@ -44,7 +44,7 @@ class BigDecimalType extends Type
return BigDecimal::of($value); return BigDecimal::of(is_float($value) ? BigDecimal::fromFloatShortest($value) : $value);
} }
/** /**

View file

@ -204,7 +204,7 @@ class Currency extends AbstractStructuralDBElement
return null; return null;
} }
return BigDecimal::one()->dividedBy($tmp, $tmp->getScale(), RoundingMode::HALF_UP); return BigDecimal::one()->dividedBy($tmp, $tmp->getScale(), RoundingMode::HalfUp);
} }
/** /**

View file

@ -195,10 +195,10 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface
#[SerializedName('price_per_unit')] #[SerializedName('price_per_unit')]
public function getPricePerUnit(float|string|BigDecimal $multiplier = 1.0): BigDecimal public function getPricePerUnit(float|string|BigDecimal $multiplier = 1.0): BigDecimal
{ {
$tmp = BigDecimal::of($multiplier); $tmp = is_float($multiplier) ? BigDecimal::fromFloatShortest($multiplier) : BigDecimal::of($multiplier);
$tmp = $tmp->multipliedBy($this->price); $tmp = $tmp->multipliedBy($this->price);
return $tmp->dividedBy($this->price_related_quantity, static::PRICE_PRECISION, RoundingMode::HALF_UP); return $tmp->dividedBy(BigDecimal::fromFloatShortest($this->price_related_quantity), static::PRICE_PRECISION, RoundingMode::HalfUp);
} }
/** /**
@ -317,7 +317,7 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface
*/ */
public function setPrice(BigDecimal $new_price): self public function setPrice(BigDecimal $new_price): self
{ {
$tmp = $new_price->toScale(self::PRICE_PRECISION, RoundingMode::HALF_UP); $tmp = $new_price->toScale(self::PRICE_PRECISION, RoundingMode::HalfUp);
//Only change the object, if the value changes, so that doctrine does not detect it as changed. //Only change the object, if the value changes, so that doctrine does not detect it as changed.
if ((string) $tmp !== (string) $this->price) { if ((string) $tmp !== (string) $this->price) {
$this->price = $tmp; $this->price = $tmp;

View file

@ -59,6 +59,16 @@ class BigDecimalMoneyType extends AbstractType implements DataTransformerInterfa
return null; return null;
} }
return BigDecimal::of($value); if ($value instanceof BigDecimal) {
return $value;
}
if (is_float($value)) {
return BigDecimal::fromFloatShortest($value);
}
if (is_string($value)) {
return BigDecimal::of($value);
}
throw new \InvalidArgumentException(sprintf('Expected a string, float or BigDecimal, got %s', get_debug_type($value)));
} }
} }

View file

@ -59,6 +59,17 @@ class BigDecimalNumberType extends AbstractType implements DataTransformerInterf
return null; return null;
} }
return BigDecimal::of($value); if ($value instanceof BigDecimal) {
return $value;
}
if (is_float($value)) {
return BigDecimal::fromFloatShortest($value);
}
if (is_string($value)) {
return BigDecimal::of($value);
}
throw new \InvalidArgumentException(sprintf('Expected a string, float or BigDecimal, got %s', get_debug_type($value)));
} }
} }

View file

@ -286,7 +286,7 @@ class PKPartImporter
//Partkeepr stores the price per item, we need to convert it to the price per packaging unit //Partkeepr stores the price per item, we need to convert it to the price per packaging unit
$price_per_item = BigDecimal::of($partdistributor['price']); $price_per_item = BigDecimal::of($partdistributor['price']);
$packaging_unit = (float) ($partdistributor['packagingUnit'] ?? 1); $packaging_unit = (float) ($partdistributor['packagingUnit'] ?? 1);
$pricedetail->setPrice($price_per_item->multipliedBy($packaging_unit)); $pricedetail->setPrice($price_per_item->multipliedBy(BigDecimal::fromFloatShortest($packaging_unit)));
$pricedetail->setPriceRelatedQuantity($packaging_unit); $pricedetail->setPriceRelatedQuantity($packaging_unit);
//We have to set the minimum discount quantity to the packaging unit (PartKeepr does not know this concept) //We have to set the minimum discount quantity to the packaging unit (PartKeepr does not know this concept)
//But in Part-DB the minimum discount qty have to be unique across a orderdetail //But in Part-DB the minimum discount qty have to be unique across a orderdetail

View file

@ -222,7 +222,7 @@ class TimeTravel
if (isset($metadata->fieldMappings[$field])) { if (isset($metadata->fieldMappings[$field])) {
//We need to convert the string to a BigDecimal first //We need to convert the string to a BigDecimal first
if (!$data instanceof BigDecimal && ('big_decimal' === $metadata->getFieldMapping($field)->type)) { if (!$data instanceof BigDecimal && ('big_decimal' === $metadata->getFieldMapping($field)->type)) {
$data = BigDecimal::of($data); $data = is_float($data) ? BigDecimal::fromFloatShortest($data) : BigDecimal::of($data);
} }
if (!$data instanceof \DateTimeInterface if (!$data instanceof \DateTimeInterface

View file

@ -170,7 +170,7 @@ class PricedetailHelper
return null; return null;
} }
return $avg->dividedBy($count, Pricedetail::PRICE_PRECISION, RoundingMode::HALF_UP); return $avg->dividedBy($count, Pricedetail::PRICE_PRECISION, RoundingMode::HalfUp);
} }
/** /**
@ -213,6 +213,6 @@ class PricedetailHelper
$val_target = $val_base->multipliedBy($targetCurrency->getInverseExchangeRate()); $val_target = $val_base->multipliedBy($targetCurrency->getInverseExchangeRate());
} }
return $val_target->toScale(Pricedetail::PRICE_PRECISION, RoundingMode::HALF_UP); return $val_target->toScale(Pricedetail::PRICE_PRECISION, RoundingMode::HalfUp);
} }
} }

View file

@ -190,7 +190,7 @@ final readonly class ProjectBuildHelper
continue; continue;
} }
$has_price = true; $has_price = true;
$total = $total->plus($unit_price->multipliedBy($entry->getQuantity())->multipliedBy($number_of_builds)); $total = $total->plus($unit_price->multipliedBy(BigDecimal::fromFloatShortest($entry->getQuantity()))->multipliedBy($number_of_builds));
} }
return $has_price ? $total : null; return $has_price ? $total : null;
@ -206,7 +206,7 @@ final readonly class ProjectBuildHelper
if ($total === null) { if ($total === null) {
return null; return null;
} }
return $total->dividedBy($number_of_builds, 10, RoundingMode::HALF_UP); return $total->dividedBy($number_of_builds, 10, RoundingMode::HalfUp);
} }
/** /**
@ -215,7 +215,7 @@ final readonly class ProjectBuildHelper
public function roundedTotalBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal public function roundedTotalBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
{ {
return $this->calculateTotalBuildPrice($project, $number_of_builds, $currency) return $this->calculateTotalBuildPrice($project, $number_of_builds, $currency)
?->toScale(2, RoundingMode::UP); ?->toScale(2, RoundingMode::Up);
} }
/** /**
@ -224,7 +224,7 @@ final readonly class ProjectBuildHelper
public function roundedUnitBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal public function roundedUnitBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
{ {
return $this->calculateUnitBuildPrice($project, $number_of_builds, $currency) return $this->calculateUnitBuildPrice($project, $number_of_builds, $currency)
?->toScale(2, RoundingMode::UP); ?->toScale(2, RoundingMode::Up);
} }
/** /**

View file

@ -44,15 +44,15 @@ class ExchangeRateUpdater
try { try {
//Try it in the direction QUOTE/BASE first, as most providers provide rates in this direction //Try it in the direction QUOTE/BASE first, as most providers provide rates in this direction
$rate = $this->swap->latest($currency->getIsoCode().'/'.$this->localizationSettings->baseCurrency); $rate = $this->swap->latest($currency->getIsoCode().'/'.$this->localizationSettings->baseCurrency);
$effective_rate = BigDecimal::of($rate->getValue()); $effective_rate = BigDecimal::fromFloatShortest($rate->getValue());
} catch (UnsupportedCurrencyPairException|UnsupportedExchangeQueryException $exception) { } catch (UnsupportedCurrencyPairException|UnsupportedExchangeQueryException $exception) {
//Otherwise try to get it inverse and calculate it ourselfes, from the format "BASE/QUOTE" //Otherwise try to get it inverse and calculate it ourselfes, from the format "BASE/QUOTE"
$rate = $this->swap->latest($this->localizationSettings->baseCurrency.'/'.$currency->getIsoCode()); $rate = $this->swap->latest($this->localizationSettings->baseCurrency.'/'.$currency->getIsoCode());
//The rate says how many quote units are worth one base unit //The rate says how many quote units are worth one base unit
//So we need to invert it to get the exchange rate //So we need to invert it to get the exchange rate
$rate_bd = BigDecimal::of($rate->getValue()); $rate_bd = BigDecimal::fromFloatShortest($rate->getValue());
$effective_rate = BigDecimal::one()->dividedBy($rate_bd, Currency::PRICE_SCALE, RoundingMode::HALF_UP); $effective_rate = BigDecimal::one()->dividedBy($rate_bd, Currency::PRICE_SCALE, RoundingMode::HalfUp);
} }
$currency->setExchangeRate($effective_rate); $currency->setExchangeRate($effective_rate);

1320
yarn.lock

File diff suppressed because it is too large Load diff